UNPKG

7.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 {methodArguments} = require('./utils/vfs');
32const systemAdapter = require('./adapters/vfs/system');
33const uuid = require('uuid/v1');
34const mime = require('mime');
35const path = require('path');
36const vfs = require('./vfs');
37const {closeWatches} = require('./utils/core.js');
38const consola = require('consola');
39const logger = consola.withTag('Filesystem');
40
41/**
42 * OS.js Virtual Filesystem
43 */
44class Filesystem {
45
46 /**
47 * Create new instance
48 * @param {Core} core Core reference
49 * @param {object} [options] Instance options
50 */
51 constructor(core, options = {}) {
52 this.core = core;
53 this.mountpoints = [];
54 this.adapters = {};
55 this.watches = [];
56 this.router = null;
57 this.methods = {};
58 this.options = {
59 adapters: {},
60 ...options
61 };
62 }
63
64 /**
65 * Destroys instance
66 */
67 async destroy() {
68 const watches = this.watches.filter(({watch}) => {
69 return watch && typeof watch.close === 'function';
70 }).map(({watch}) => watch);
71
72 await closeWatches(watches);
73
74 this.watches = [];
75 }
76
77 /**
78 * Initializes Filesystem
79 * @return {Promise<boolean>}
80 */
81 async init() {
82 const adapters = {
83 system: systemAdapter,
84 ...this.options.adapters
85 };
86
87 this.adapters = Object.keys(adapters).reduce((result, iter) => {
88 return {
89 [iter]: adapters[iter](this.core),
90 ...result
91 };
92 }, {});
93
94 // Routes
95 const {router, methods} = vfs(this.core);
96 this.router = router;
97 this.methods = methods;
98
99 // Mimes
100 const {define} = this.core.config('mime', {define: {}, filenames: {}});
101 mime.define(define, {force: true});
102
103 // Mountpoints
104 await Promise.all(this.core.config('vfs.mountpoints')
105 .map(mount => this.mount(mount)));
106
107 return true;
108 }
109
110 /**
111 * Gets MIME
112 * @param {string} filename Input filename or path
113 * @return {string}
114 */
115 mime(filename) {
116 const {filenames} = this.core.config('mime', {
117 define: {},
118 filenames: {}
119 });
120
121 return filenames[path.basename(filename)]
122 ? filenames[path.basename(filename)]
123 : mime.getType(filename) || 'application/octet-stream';
124 }
125
126 /**
127 * Crates a VFS request
128 * @param {Request|object} req HTTP Request object
129 * @param {Response|object} [res] HTTP Response object
130 * @return {Promise<*>}
131 */
132 request(name, req, res = {}) {
133 return this.methods[name](req, res);
134 }
135
136 /**
137 * Performs a VFS request with simulated HTTP request
138 * @param {object} options Request options
139 * @param {string} options.method VFS Method name
140 * @param {object} [options.user] User session data
141 * @param {*} ...args Arguments to pass to VFS method
142 * @return {Promise<*>}
143 */
144 call(options, ...args) {
145 const {method, user} = {
146 user: {},
147 ...options
148 };
149
150 const req = methodArguments[method]
151 .reduce(({fields, files}, key, index) => {
152 const arg = args[index];
153 if (typeof key === 'function') {
154 files = Object.assign(key(arg), files);
155 } else {
156 fields = {
157 [key]: arg,
158 ...fields
159 };
160 }
161
162 return {fields, files};
163 }, {fields: {}, files: {}});
164
165 req.session = {user};
166
167 return this.request(method, req);
168 }
169
170 /**
171 * Creates realpath VFS request
172 * @param {string} filename The path
173 * @param {object} [user] User session object
174 * @return {Promise<string>}
175 */
176 realpath(filename, user = {}) {
177 return this.methods.realpath({
178 session: {
179 user: {
180 groups: [],
181 ...user
182 }
183 },
184 fields: {
185 path: filename
186 }
187 });
188 }
189
190 /**
191 * Mounts given mountpoint
192 * @param {object} mount Mountpoint
193 * @return {object} the mountpoint
194 */
195 async mount(mount) {
196 const mountpoint = {
197 id: uuid(),
198 root: `${mount.name}:/`,
199 attributes: {},
200 ...mount
201 };
202
203 this.mountpoints.push(mountpoint);
204
205 logger.success('Mounted', mountpoint.name);
206
207 await this.watch(mountpoint);
208
209 return mountpoint;
210 }
211
212 /**
213 * Unmounts given mountpoint
214 * @param {object} mount Mountpoint
215 * @return {Promise<boolean>}
216 */
217 async unmount(mountpoint) {
218 const found = this.watches.find(w => w.id === mountpoint.id);
219
220 if (found && found.watch) {
221 await found.watch.close();
222 }
223
224 const index = this.mountpoints.indexOf(mountpoint);
225
226 if (index !== -1) {
227 this.mountpoints.splice(index, 1);
228
229 return true;
230 }
231
232 return false;
233 }
234
235 /**
236 * Set up a watch for given mountpoint
237 * @param {object} mountpoint The mountpoint
238 */
239 async watch(mountpoint) {
240 if (
241 !mountpoint.attributes.watch ||
242 this.core.config('vfs.watch') === false ||
243 !mountpoint.attributes.root
244 ) {
245 return;
246 }
247
248 const adapter = await (mountpoint.adapter
249 ? this.adapters[mountpoint.adapter]
250 : this.adapters.system);
251
252 if (typeof adapter.watch === 'function') {
253 await this._watch(mountpoint, adapter);
254 }
255 }
256
257 /**
258 * Internal method for setting up watch for given mountpoint adapter
259 * @param {object} mountpoint The mountpoint
260 * @param {object} adapter The adapter
261 */
262 async _watch(mountpoint, adapter) {
263 const watch = await adapter.watch(mountpoint, (args, dir, type) => {
264 const target = mountpoint.name + ':/' + dir;
265 const keys = Object.keys(args);
266 const filter = keys.length === 0
267 ? () => true
268 : ws => keys.every(k => ws._osjs_client[k] === args[k]);
269
270 this.core.emit('osjs/vfs:watch:change', {
271 mountpoint,
272 target,
273 type
274 });
275
276 this.core.broadcast('osjs/vfs:watch:change', [{
277 path: target,
278 type
279 }, args], filter);
280 });
281
282 watch.on('error', error => logger.warn('Mountpoint watch error', error));
283
284 this.watches.push({
285 id: mountpoint.id,
286 watch
287 });
288
289 logger.info('Watching mountpoint', mountpoint.name);
290 }
291}
292
293module.exports = Filesystem;