UNPKG

9.54 kBJavaScriptView Raw
1/*
2 * OS.js - JavaScript Cloud/Web Desktop Platform
3 *
4 * Copyright (c) 2011-2020, Anders Evenrud <andersevenrud@gmail.com>
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright notice, this
11 * list of conditions and the following disclaimer
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 *
27 * @author Anders Evenrud <andersevenrud@gmail.com>
28 * @licence Simplified BSD License
29 */
30
31const fs = require('fs-extra');
32const path = require('path');
33const express = require('express');
34const {Stream} = require('stream');
35const {
36 mountpointResolver,
37 checkMountpointPermission,
38 streamFromRequest,
39 sanitize,
40 parseFields,
41 errorCodes
42} = require('./utils/vfs');
43
44const respondNumber = result => typeof result === 'number' ? result : -1;
45const respondBoolean = result => typeof result === 'boolean' ? result : !!result;
46const requestPath = req => ([sanitize(req.fields.path)]);
47const requestSearch = req => ([sanitize(req.fields.root), req.fields.pattern]);
48const requestCross = req => ([sanitize(req.fields.from), sanitize(req.fields.to)]);
49const requestFile = req => ([sanitize(req.fields.path), streamFromRequest(req)]);
50
51/*
52 * Parses the range request headers
53 */
54const parseRangeHeader = (range, size) => {
55 const [pstart, pend] = range.replace(/bytes=/, '').split('-');
56 const start = parseInt(pstart, 10);
57 const end = pend ? parseInt(pend, 10) : undefined;
58 return [start, end];
59};
60
61/**
62 * A "finally" for our chain
63 */
64const onDone = (req, res) => {
65 if (req.files) {
66 for (let fieldname in req.files) {
67 fs.unlink(req.files[fieldname].path, () => ({}));
68 }
69 }
70};
71
72/**
73 * Wraps a vfs adapter request
74 */
75const wrapper = fn => (req, res, next) => fn(req, res)
76 .then(result => {
77 if (result instanceof Stream) {
78 result.pipe(res);
79 } else {
80 res.json(result);
81 }
82
83 onDone(req, res);
84 })
85 .catch(error => {
86 onDone(req, res);
87
88 next(error);
89 });
90
91/**
92 * Creates the middleware
93 */
94const createMiddleware = core => {
95 const parse = parseFields(core.config('express'));
96
97 return (req, res, next) => parse(req, res)
98 .then(({fields, files}) => {
99 req.fields = fields;
100 req.files = files;
101
102 next();
103 })
104 .catch(error => {
105 core.logger.warn(error);
106 req.fields = {};
107 req.files = {};
108
109 next(error);
110 });
111};
112
113const createOptions = req => {
114 const options = req.fields.options;
115 const range = req.headers && req.headers.range;
116 const session = {...req.session || {}};
117 let result = options || {};
118
119 if (typeof options === 'string') {
120 try {
121 result = JSON.parse(req.fields.options) || {};
122 } catch (e) {
123 // Allow to fall through
124 }
125 }
126
127 if (range) {
128 result.range = parseRangeHeader(req.headers.range);
129 }
130
131 return {
132 ...result,
133 session
134 };
135};
136
137// Standard request with only a target
138const createRequestFactory = findMountpoint => (getter, method, readOnly, respond) => async (req, res) => {
139 const options = createOptions(req);
140 const args = [...getter(req, res), options];
141
142 const found = await findMountpoint(args[0]);
143 if (method === 'search') {
144 if (found.mount.attributes && found.mount.attributes.searchable === false) {
145 return [];
146 }
147 }
148
149 const {attributes} = found.mount;
150 const strict = attributes.strictGroups !== false;
151 const ranges = (!attributes.adapter || attributes.adapter === 'system') || attributes.ranges === true;
152 const vfsMethodWrapper = m => found.adapter[m]
153 ? found.adapter[m](found)(...args)
154 : Promise.reject(new Error(`Adapter does not support ${m}`));
155 const readstat = () => vfsMethodWrapper('stat').catch(() => ({}));
156 await checkMountpointPermission(req, res, method, readOnly, strict)(found);
157
158 const result = await vfsMethodWrapper(method);
159 if (method === 'readfile') {
160 const stat = await readstat();
161
162 if (ranges && options.range) {
163 try {
164 if (stat.size) {
165 const size = stat.size;
166 const [start, end] = options.range;
167 const realEnd = end ? end : size - 1;
168 const chunksize = (realEnd - start) + 1;
169
170 res.writeHead(206, {
171 'Content-Range': `bytes ${start}-${realEnd}/${size}`,
172 'Accept-Ranges': 'bytes',
173 'Content-Length': chunksize,
174 'Content-Type': stat.mime
175 });
176 }
177 } catch (e) {
178 console.warn('Failed to send a ranged response', e);
179 }
180 } else if (stat.mime) {
181 res.append('Content-Type', stat.mime);
182 }
183
184 if (options.download) {
185 const filename = path.basename(args[0]);
186 res.append('Content-Disposition', `attachment; filename="${filename}"`);
187 }
188 }
189
190 return respond ? respond(result) : result;
191};
192
193// Request that has a source and target
194const createCrossRequestFactory = findMountpoint => (getter, method, respond) => async (req, res) => {
195 const [from, to, options] = [...getter(req, res), createOptions(req)];
196
197 const srcMount = await findMountpoint(from);
198 const destMount = await findMountpoint(to);
199 const sameAdapter = srcMount.adapter === destMount.adapter;
200
201 const srcStrict = srcMount.mount.attributes.strictGroups !== false;
202 const destStrict = destMount.mount.attributes.strictGroups !== false;
203 await checkMountpointPermission(req, res, 'readfile', false, srcStrict)(srcMount);
204 await checkMountpointPermission(req, res, 'writefile', true, destStrict)(destMount);
205
206 if (sameAdapter) {
207 const result = await srcMount
208 .adapter[method](srcMount, destMount)(from, to, options);
209
210 return !!result;
211 }
212
213 // Simulates a copy/move
214 const stream = await srcMount.adapter
215 .readfile(srcMount)(from, options);
216
217 const result = await destMount.adapter
218 .writefile(destMount)(to, stream, options);
219
220 if (method === 'rename') {
221 await srcMount.adapter
222 .unlink(srcMount)(from, options);
223 }
224
225 return !!result;
226};
227
228/*
229 * VFS Methods
230 */
231const vfs = core => {
232 const findMountpoint = mountpointResolver(core);
233 const createRequest = createRequestFactory(findMountpoint);
234 const createCrossRequest = createCrossRequestFactory(findMountpoint);
235
236 // Wire up all available VFS events
237 return {
238 realpath: createRequest(requestPath, 'realpath', false),
239 exists: createRequest(requestPath, 'exists', false, respondBoolean),
240 stat: createRequest(requestPath, 'stat', false),
241 readdir: createRequest(requestPath, 'readdir', false),
242 readfile: createRequest(requestPath, 'readfile', false),
243 writefile: createRequest(requestFile, 'writefile', true, respondNumber),
244 mkdir: createRequest(requestPath, 'mkdir', true, respondBoolean),
245 unlink: createRequest(requestPath, 'unlink', true, respondBoolean),
246 touch: createRequest(requestPath, 'touch', true, respondBoolean),
247 search: createRequest(requestSearch, 'search', false),
248 copy: createCrossRequest(requestCross, 'copy'),
249 rename: createCrossRequest(requestCross, 'rename')
250 };
251};
252
253/*
254 * Creates a new VFS Express router
255 */
256module.exports = core => {
257 const router = express.Router();
258 const methods = vfs(core);
259 const middleware = createMiddleware(core);
260 const {isAuthenticated} = core.make('osjs/express');
261 const vfsGroups = core.config('auth.vfsGroups', []);
262 const logEnabled = core.config('development');
263
264 // Middleware first
265 router.use(isAuthenticated(vfsGroups));
266 router.use(middleware);
267
268 // Then all VFS routes (needs implementation above)
269 router.get('/exists', wrapper(methods.exists));
270 router.get('/stat', wrapper(methods.stat));
271 router.get('/readdir', wrapper(methods.readdir));
272 router.get('/readfile', wrapper(methods.readfile));
273 router.post('/writefile', wrapper(methods.writefile));
274 router.post('/rename', wrapper(methods.rename));
275 router.post('/copy', wrapper(methods.copy));
276 router.post('/mkdir', wrapper(methods.mkdir));
277 router.post('/unlink', wrapper(methods.unlink));
278 router.post('/touch', wrapper(methods.touch));
279 router.post('/search', wrapper(methods.search));
280
281 // Finally catch promise exceptions
282 router.use((error, req, res, next) => {
283 // TODO: Better error messages
284 const code = typeof error.code === 'number'
285 ? error.code
286 : (errorCodes[error.code] || 400);
287
288 if (logEnabled) {
289 console.error(error);
290 }
291
292 res.status(code)
293 .json({
294 error: error.toString(),
295 stack: logEnabled ? error.stack : undefined
296 });
297 });
298
299 return {router, methods};
300};