1 | 1 | var fs = require('fs'); |
2 | 1 | var net = require('net'); |
3 | 1 | var childProcess = require('child_process'); |
4 | 1 | var join = require('path').join; |
5 | 1 | var pathResolve = require('path').resolve; |
6 | 1 | var dirname = require('path').dirname; |
7 | 1 | var basename = require('path').basename; |
8 | 1 | var Stream = require('stream').Stream; |
9 | 1 | var getMime = require('simple-mime')("application/octet-stream"); |
10 | 1 | var vm = require('vm'); |
11 | | |
12 | 1 | module.exports = function setup(fsOptions) { |
13 | | |
14 | | // Check and configure options |
15 | 2 | var root = fsOptions.root; |
16 | 2 | if (!root) throw new Error("root is a required option"); |
17 | 2 | if (root[0] !== "/") throw new Error("root path must start in /"); |
18 | 2 | if (root[root.length - 1] !== "/") root += "/"; |
19 | 2 | var base = root.substr(0, root.length - 1); |
20 | 2 | var umask = fsOptions.umask || 0750; |
21 | 2 | if (fsOptions.hasOwnProperty('defaultEnv')) { |
22 | 1 | fsOptions.defaultEnv.__proto__ = process.env; |
23 | | } else { |
24 | 1 | fsOptions.defaultEnv = process.env; |
25 | | } |
26 | | |
27 | | // Storage for extension APIs |
28 | 2 | var apis = {}; |
29 | | // Storage for event handlers |
30 | 2 | var handlers = {}; |
31 | | |
32 | | // Export the API |
33 | 2 | var vfs = { |
34 | | // File management |
35 | | resolve: resolve, |
36 | | stat: stat, |
37 | | readfile: readfile, |
38 | | readdir: readdir, |
39 | | mkfile: mkfile, |
40 | | mkdir: mkdir, |
41 | | rmfile: rmfile, |
42 | | rmdir: rmdir, |
43 | | rename: rename, |
44 | | copy: copy, |
45 | | symlink: symlink, |
46 | | |
47 | | // Wrapper around fs.watch or fs.watchFile |
48 | | watch: watch, |
49 | | |
50 | | // Network connection |
51 | | connect: connect, |
52 | | |
53 | | // Process Management |
54 | | spawn: spawn, |
55 | | execFile: execFile, |
56 | | |
57 | | // Basic async event emitter style API |
58 | | on: on, |
59 | | off: off, |
60 | | emit: emit, |
61 | | |
62 | | // Extending the API |
63 | | extend: extend, |
64 | | unextend: unextend, |
65 | | use: use |
66 | | }; |
67 | | |
68 | | //////////////////////////////////////////////////////////////////////////////// |
69 | | |
70 | | // Realpath a file and check for access |
71 | | // callback(err, path) |
72 | 2 | function resolvePath(path, callback, alreadyRooted) { |
73 | 111 | if (!alreadyRooted) path = join(root, path); |
74 | 113 | if (fsOptions.checkSymlinks) fs.realpath(path, check); |
75 | 1 | else check(null, path); |
76 | | |
77 | 57 | function check(err, path) { |
78 | 68 | if (err) return callback(err); |
79 | 46 | if (!(path === base || path.substr(0, root.length) === root)) { |
80 | 1 | err = new Error("EACCESS: '" + path + "' not in '" + root + "'"); |
81 | 1 | err.code = "EACCESS"; |
82 | 1 | return callback(err); |
83 | | } |
84 | 45 | callback(null, path); |
85 | | } |
86 | | } |
87 | | |
88 | | // A wrapper around fs.open that enforces permissions and gives extra data in |
89 | | // the callback. (err, path, fd, stat) |
90 | 2 | function open(path, flags, mode, callback) { |
91 | 13 | resolvePath(path, function (err, path) { |
92 | 15 | if (err) return callback(err); |
93 | 11 | fs.open(path, flags, mode, function (err, fd) { |
94 | 11 | if (err) return callback(err); |
95 | 11 | fs.fstat(fd, function (err, stat) { |
96 | 11 | if (err) return callback(err); |
97 | 11 | callback(null, path, fd, stat); |
98 | | }); |
99 | | }); |
100 | | }); |
101 | | } |
102 | | |
103 | | // This helper function doesn't follow node conventions in the callback, |
104 | | // there is no err, only entry. |
105 | 2 | function createStatEntry(file, fullpath, callback) { |
106 | 9 | fs.lstat(fullpath, function (err, stat) { |
107 | 9 | var entry = { |
108 | | name: file |
109 | | }; |
110 | | |
111 | 9 | if (err) { |
112 | 1 | entry.err = err; |
113 | 1 | return callback(entry); |
114 | | } else { |
115 | 8 | entry.size = stat.size; |
116 | 8 | entry.mtime = stat.mtime.valueOf(); |
117 | | |
118 | 8 | if (stat.isDirectory()) { |
119 | 2 | entry.mime = "inode/directory"; |
120 | 6 | } else if (stat.isBlockDevice()) entry.mime = "inode/blockdevice"; |
121 | 6 | else if (stat.isCharacterDevice()) entry.mime = "inode/chardevice"; |
122 | 8 | else if (stat.isSymbolicLink()) entry.mime = "inode/symlink"; |
123 | 4 | else if (stat.isFIFO()) entry.mime = "inode/fifo"; |
124 | 4 | else if (stat.isSocket()) entry.mime = "inode/socket"; |
125 | | else { |
126 | 4 | entry.mime = getMime(fullpath); |
127 | | } |
128 | | |
129 | 8 | if (!stat.isSymbolicLink()) { |
130 | 6 | return callback(entry); |
131 | | } |
132 | 2 | fs.readlink(fullpath, function (err, link) { |
133 | 2 | if (err) { |
134 | 0 | entry.linkErr = err.stack; |
135 | 0 | return callback(entry); |
136 | | } |
137 | 2 | entry.link = link; |
138 | 2 | resolvePath(pathResolve(dirname(fullpath), link), function (err, newpath) { |
139 | 2 | if (err) { |
140 | 0 | entry.linkStatErr = err; |
141 | 0 | return callback(entry); |
142 | | } |
143 | 2 | createStatEntry(basename(newpath), newpath, function (linkStat) { |
144 | 2 | entry.linkStat = linkStat; |
145 | 2 | linkStat.fullPath = newpath.substr(base.length) || "/"; |
146 | 2 | return callback(entry); |
147 | | }); |
148 | | }, true/*alreadyRooted*/); |
149 | | }); |
150 | | } |
151 | | }); |
152 | | } |
153 | | |
154 | | // Common logic used by rmdir and rmfile |
155 | 2 | function remove(path, fn, callback) { |
156 | 7 | var meta = {}; |
157 | 7 | resolvePath(path, function (err, path) { |
158 | 9 | if (err) return callback(err); |
159 | 5 | fn(path, function (err) { |
160 | 7 | if (err) return callback(err); |
161 | 3 | return callback(null, meta); |
162 | | }); |
163 | | }); |
164 | | } |
165 | | |
166 | | //////////////////////////////////////////////////////////////////////////////// |
167 | | |
168 | 2 | function resolve(path, options, callback) { |
169 | 5 | resolvePath(path, function (err, path) { |
170 | 7 | if (err) return callback(err); |
171 | 3 | callback(null, { path: path }); |
172 | | }, options.alreadyRooted); |
173 | | } |
174 | | |
175 | 2 | function stat(path, options, callback) { |
176 | | |
177 | | // Make sure the parent directory is accessable |
178 | 2 | resolvePath(dirname(path), function (err, dir) { |
179 | 2 | if (err) return callback(err); |
180 | 2 | var file = basename(path); |
181 | 2 | path = join(dir, file); |
182 | 2 | createStatEntry(file, path, function (entry) { |
183 | 2 | if (entry.err) { |
184 | 1 | return callback(entry.err); |
185 | | } |
186 | 1 | callback(null, entry); |
187 | | }); |
188 | | }); |
189 | | } |
190 | | |
191 | 2 | function readfile(path, options, callback) { |
192 | | |
193 | 13 | var meta = {}; |
194 | | |
195 | 13 | open(path, "r", umask & 0666, function (err, path, fd, stat) { |
196 | 15 | if (err) return callback(err); |
197 | 11 | if (stat.isDirectory()) { |
198 | 1 | fs.close(fd); |
199 | 1 | var err = new Error("EISDIR: Requested resource is a directory"); |
200 | 1 | err.code = "EISDIR"; |
201 | 1 | return callback(err); |
202 | | } |
203 | | |
204 | | // Basic file info |
205 | 10 | meta.mime = getMime(path); |
206 | 10 | meta.size = stat.size; |
207 | 10 | meta.etag = calcEtag(stat); |
208 | | |
209 | | // ETag support |
210 | 10 | if (options.etag === meta.etag) { |
211 | 1 | meta.notModified = true; |
212 | 1 | fs.close(fd); |
213 | 1 | return callback(null, meta); |
214 | | } |
215 | | |
216 | | // Range support |
217 | 9 | if (options.hasOwnProperty('range') && !(options.range.etag && options.range.etag !== meta.etag)) { |
218 | 4 | var range = options.range; |
219 | 4 | var start, end; |
220 | 4 | if (range.hasOwnProperty("start")) { |
221 | 2 | start = range.start; |
222 | 2 | end = range.hasOwnProperty("end") ? range.end : meta.size - 1; |
223 | | } |
224 | | else { |
225 | 2 | if (range.hasOwnProperty("end")) { |
226 | 1 | start = meta.size - range.end; |
227 | 1 | end = meta.size - 1; |
228 | | } |
229 | | else { |
230 | 1 | meta.rangeNotSatisfiable = "Invalid Range"; |
231 | 1 | fs.close(fd); |
232 | 1 | return callback(null, meta); |
233 | | } |
234 | | } |
235 | 3 | if (end < start || start < 0 || end >= stat.size) { |
236 | 1 | meta.rangeNotSatisfiable = "Range out of bounds"; |
237 | 1 | fs.close(fd); |
238 | 1 | return callback(null, meta); |
239 | | } |
240 | 2 | options.start = start; |
241 | 2 | options.end = end; |
242 | 2 | meta.size = end - start + 1; |
243 | 2 | meta.partialContent = { start: start, end: end, size: stat.size }; |
244 | | } |
245 | | |
246 | | // HEAD request support |
247 | 7 | if (options.hasOwnProperty("head")) { |
248 | 3 | fs.close(fd); |
249 | 3 | return callback(null, meta); |
250 | | } |
251 | | |
252 | | // Read the file as a stream |
253 | 4 | try { |
254 | 4 | options.fd = fd; |
255 | 4 | meta.stream = new fs.ReadStream(path, options); |
256 | | } catch (err) { |
257 | 0 | fs.close(fd); |
258 | 0 | return callback(err); |
259 | | } |
260 | 4 | callback(null, meta); |
261 | | }); |
262 | | } |
263 | | |
264 | 2 | function readdir(path, options, callback) { |
265 | | |
266 | 6 | var meta = {}; |
267 | | |
268 | 6 | resolvePath(path, function (err, path) { |
269 | 7 | if (err) return callback(err); |
270 | 5 | fs.stat(path, function (err, stat) { |
271 | 5 | if (err) return callback(err); |
272 | 5 | if (!stat.isDirectory()) { |
273 | 1 | err = new Error("ENOTDIR: Requested resource is not a directory"); |
274 | 1 | err.code = "ENOTDIR"; |
275 | 1 | return callback(err); |
276 | | } |
277 | | |
278 | | // ETag support |
279 | 4 | meta.etag = calcEtag(stat); |
280 | 4 | if (options.etag === meta.etag) { |
281 | 1 | meta.notModified = true; |
282 | 1 | return callback(null, meta); |
283 | | } |
284 | | |
285 | 3 | fs.readdir(path, function (err, files) { |
286 | 3 | if (err) return callback(err); |
287 | 3 | if (options.head) { |
288 | 2 | return callback(null, meta); |
289 | | } |
290 | 1 | var stream = new Stream(); |
291 | 1 | stream.readable = true; |
292 | 1 | var paused; |
293 | 1 | stream.pause = function () { |
294 | 0 | if (paused === true) return; |
295 | 0 | paused = true; |
296 | | }; |
297 | 1 | stream.resume = function () { |
298 | 1 | if (paused === false) return; |
299 | 1 | paused = false; |
300 | 1 | getNext(); |
301 | | }; |
302 | 1 | meta.stream = stream; |
303 | 1 | callback(null, meta); |
304 | 1 | var index = 0; |
305 | 1 | stream.resume(); |
306 | 1 | function getNext() { |
307 | 7 | if (index === files.length) return done(); |
308 | 5 | var file = files[index++]; |
309 | 5 | var fullpath = join(path, file); |
310 | | |
311 | 5 | createStatEntry(file, fullpath, function onStatEntry(entry) { |
312 | 5 | stream.emit("data", entry); |
313 | | |
314 | 5 | if (!paused) { |
315 | 5 | getNext(); |
316 | | } |
317 | | }); |
318 | | } |
319 | 1 | function done() { |
320 | 1 | stream.emit("end"); |
321 | | } |
322 | | }); |
323 | | }); |
324 | | }); |
325 | | } |
326 | | |
327 | 2 | function mkfile(path, options, realCallback) { |
328 | 6 | var meta = {}; |
329 | 6 | var called; |
330 | 6 | var callback = function (err, meta) { |
331 | 8 | if (called) { |
332 | 2 | if (err) { |
333 | 0 | if (meta.stream) meta.stream.emit("error", err); |
334 | 0 | else console.error(err.stack); |
335 | | } |
336 | 4 | else if (meta.stream) meta.stream.emit("saved"); |
337 | 2 | return; |
338 | | } |
339 | 6 | called = true; |
340 | 6 | return realCallback.apply(this, arguments); |
341 | | }; |
342 | | |
343 | 6 | if (options.stream && !options.stream.readable) { |
344 | 0 | return callback(new TypeError("options.stream must be readable.")); |
345 | | } |
346 | | |
347 | | // Pause the input for now since we're not ready to write quite yet |
348 | 6 | var readable = options.stream; |
349 | 6 | if (readable) { |
350 | 8 | if (readable.pause) readable.pause(); |
351 | 4 | var buffer = []; |
352 | 4 | readable.on("data", onData); |
353 | 4 | readable.on("end", onEnd); |
354 | | } |
355 | | |
356 | 6 | function onData(chunk) { |
357 | 0 | buffer.push(["data", chunk]); |
358 | | } |
359 | 6 | function onEnd() { |
360 | 0 | buffer.push(["end"]); |
361 | | } |
362 | 6 | function error(err) { |
363 | 0 | if (readable) { |
364 | 0 | readable.removeListener("data", onData); |
365 | 0 | readable.removeListener("end", onEnd); |
366 | 0 | if (readable.destroy) readable.destroy(); |
367 | | } |
368 | 0 | if (err) callback(err); |
369 | | } |
370 | | |
371 | | // Make sure the user has access to the directory and get the real path. |
372 | 6 | resolvePath(path, function (err, resolvedPath) { |
373 | 6 | if (err) { |
374 | 4 | if (err.code !== "ENOENT") { |
375 | 0 | return error(err); |
376 | | } |
377 | | // If checkSymlinks is on we'll get an ENOENT when creating a new file. |
378 | | // In that case, just resolve the parent path and go from there. |
379 | 4 | resolvePath(dirname(path), function (err, dir) { |
380 | 4 | if (err) return error(err); |
381 | 4 | onPath(join(dir, basename(path))); |
382 | | }); |
383 | 4 | return; |
384 | | } |
385 | 2 | onPath(resolvedPath); |
386 | | }); |
387 | | |
388 | 6 | function onPath(path) { |
389 | 12 | if (!options.mode) options.mode = umask & 0666; |
390 | 6 | var writable = new fs.WriteStream(path, options); |
391 | 6 | if (readable) { |
392 | 4 | readable.pipe(writable); |
393 | | } |
394 | | else { |
395 | 2 | meta.stream = writable; |
396 | 2 | callback(null, meta); |
397 | | } |
398 | 6 | var hadError; |
399 | 6 | writable.once('error', function (err) { |
400 | 0 | hadError = true; |
401 | 0 | error(err); |
402 | | }); |
403 | 6 | writable.on('close', function () { |
404 | 6 | if (hadError) return; |
405 | 6 | callback(null, meta); |
406 | | }); |
407 | | |
408 | 6 | if (readable) { |
409 | | // Stop buffering events and playback anything that happened. |
410 | 4 | readable.removeListener("data", onData); |
411 | 4 | readable.removeListener("end", onEnd); |
412 | 4 | buffer.forEach(function (event) { |
413 | 0 | readable.emit.apply(readable, event); |
414 | | }); |
415 | | // Resume the input stream if possible |
416 | 8 | if (readable.resume) readable.resume(); |
417 | | } |
418 | | } |
419 | | } |
420 | | |
421 | 2 | function mkdir(path, options, callback) { |
422 | 3 | var meta = {}; |
423 | | // Make sure the user has access to the parent directory and get the real path. |
424 | 3 | resolvePath(dirname(path), function (err, dir) { |
425 | 3 | if (err) return callback(err); |
426 | 3 | path = join(dir, basename(path)); |
427 | 3 | fs.mkdir(path, function (err) { |
428 | 5 | if (err) return callback(err); |
429 | 1 | callback(null, meta); |
430 | | }); |
431 | | }); |
432 | | } |
433 | | |
434 | 2 | function rmfile(path, options, callback) { |
435 | 3 | remove(path, fs.unlink, callback); |
436 | | } |
437 | | |
438 | 2 | function rmdir(path, options, callback) { |
439 | 4 | if (options.recursive) { |
440 | 1 | remove(path, function(path, callback) { |
441 | 1 | execFile("rm", {args: ["-rf", path]}, callback); |
442 | | }, callback); |
443 | | } |
444 | | else { |
445 | 3 | remove(path, fs.rmdir, callback); |
446 | | } |
447 | | } |
448 | | |
449 | 2 | function rename(path, options, callback) { |
450 | 3 | var from, to; |
451 | 3 | if (options.from) { |
452 | 2 | from = options.from; to = path; |
453 | | } |
454 | 2 | else if (options.to) { |
455 | 4 | from = path; to = options.to; |
456 | | } |
457 | | else { |
458 | 0 | return callback(new Error("Must specify either options.from or options.to")); |
459 | | } |
460 | 3 | var meta = {}; |
461 | | // Get real path to source |
462 | 3 | resolvePath(from, function (err, from) { |
463 | 4 | if (err) return callback(err); |
464 | | // Get real path to target dir |
465 | 2 | resolvePath(dirname(to), function (err, dir) { |
466 | 2 | if (err) return callback(err); |
467 | 2 | to = join(dir, basename(to)); |
468 | | // Rename the file |
469 | 2 | fs.rename(from, to, function (err) { |
470 | 2 | if (err) return callback(err); |
471 | 2 | callback(null, meta); |
472 | | }); |
473 | | }); |
474 | | }); |
475 | | } |
476 | | |
477 | 2 | function copy(path, options, callback) { |
478 | 3 | var from, to; |
479 | 3 | if (options.from) { |
480 | 2 | from = options.from; to = path; |
481 | | } |
482 | 2 | else if (options.to) { |
483 | 4 | from = path; to = options.to; |
484 | | } |
485 | | else { |
486 | 0 | return callback(new Error("Must specify either options.from or options.to")); |
487 | | } |
488 | 3 | readfile(from, {}, function (err, meta) { |
489 | 4 | if (err) return callback(err); |
490 | 2 | mkfile(to, {stream: meta.stream}, callback); |
491 | | }); |
492 | | } |
493 | | |
494 | 2 | function symlink(path, options, callback) { |
495 | 2 | if (!options.target) return callback(new Error("options.target is required")); |
496 | 2 | var meta = {}; |
497 | | // Get real path to target dir |
498 | 2 | resolvePath(dirname(path), function (err, dir) { |
499 | 2 | if (err) return callback(err); |
500 | 2 | path = join(dir, basename(path)); |
501 | 2 | fs.symlink(options.target, path, function (err) { |
502 | 3 | if (err) return callback(err); |
503 | 1 | callback(null, meta); |
504 | | }); |
505 | | }); |
506 | | } |
507 | | |
508 | 2 | function watch(path, options, callback) { |
509 | 2 | var meta = {}; |
510 | 2 | resolvePath(path, function (err, path) { |
511 | 2 | if (err) return callback(err); |
512 | 2 | if (options.file) { |
513 | 0 | meta.watcher = fs.watchFile(path, options, function () {}); |
514 | 0 | meta.watcher.close = function () { |
515 | 0 | fs.unwatchFile(path); |
516 | | }; |
517 | | } |
518 | | else { |
519 | 2 | meta.watcher = fs.watch(path, options, function () {}); |
520 | | } |
521 | 2 | callback(null, meta); |
522 | | }); |
523 | | } |
524 | | |
525 | 2 | function connect(port, options, callback) { |
526 | 1 | var retries = options.hasOwnProperty('retries') ? options.retries : 5; |
527 | 1 | var retryDelay = options.hasOwnProperty('retryDelay') ? options.retryDelay : 50; |
528 | 1 | tryConnect(); |
529 | 1 | function tryConnect() { |
530 | 1 | var socket = net.connect(port, function () { |
531 | 1 | if (options.hasOwnProperty('encoding')) { |
532 | 1 | socket.setEncoding(options.encoding); |
533 | | } |
534 | 1 | callback(null, {stream:socket}); |
535 | | }); |
536 | 1 | socket.once("error", function (err) { |
537 | 0 | if (err.code === "ECONNREFUSED" && retries) { |
538 | 0 | setTimeout(tryConnect, retryDelay); |
539 | 0 | retries--; |
540 | 0 | retryDelay *= 2; |
541 | 0 | return; |
542 | | } |
543 | 0 | return callback(err); |
544 | | }); |
545 | | } |
546 | | } |
547 | | |
548 | 2 | function spawn(executablePath, options, callback) { |
549 | 2 | var args = options.args || []; |
550 | | |
551 | 2 | if (options.hasOwnProperty('env')) { |
552 | 1 | options.env.__proto__ = fsOptions.defaultEnv; |
553 | | } else { |
554 | 1 | options.env = fsOptions.defaultEnv; |
555 | | } |
556 | | |
557 | 2 | var child; |
558 | 2 | try { |
559 | 2 | child = childProcess.spawn(executablePath, args, options); |
560 | | } catch (err) { |
561 | 0 | return callback(err); |
562 | | } |
563 | 2 | if (options.resumeStdin) child.stdin.resume(); |
564 | 2 | if (options.hasOwnProperty('stdoutEncoding')) { |
565 | 2 | child.stdout.setEncoding(options.stdoutEncoding); |
566 | | } |
567 | 2 | if (options.hasOwnProperty('stderrEncoding')) { |
568 | 1 | child.stderr.setEncoding(options.stderrEncoding); |
569 | | } |
570 | | |
571 | 2 | callback(null, { |
572 | | process: child |
573 | | }); |
574 | | } |
575 | | |
576 | 2 | function execFile(executablePath, options, callback) { |
577 | | |
578 | 3 | if (options.hasOwnProperty('env')) { |
579 | 1 | options.env.__proto__ = fsOptions.defaultEnv; |
580 | | } else { |
581 | 2 | options.env = fsOptions.defaultEnv; |
582 | | } |
583 | | |
584 | 3 | childProcess.execFile(executablePath, options.args || [], options, function (err, stdout, stderr) { |
585 | 3 | if (err) return callback(err); |
586 | 3 | callback(null, { |
587 | | stdout: stdout, |
588 | | stderr: stderr |
589 | | }); |
590 | | }); |
591 | | } |
592 | | |
593 | 2 | function on(name, handler, callback) { |
594 | 6 | if (!handlers[name]) handlers[name] = []; |
595 | 5 | handlers[name].push(handler); |
596 | 5 | callback && callback(); |
597 | | } |
598 | | |
599 | 2 | function off(name, handler, callback) { |
600 | 5 | var list = handlers[name]; |
601 | 5 | if (list) { |
602 | 5 | var index = list.indexOf(handler); |
603 | 5 | if (index >= 0) { |
604 | 5 | list.splice(index, 1); |
605 | | } |
606 | | } |
607 | 5 | callback && callback(); |
608 | | } |
609 | | |
610 | 2 | function emit(name, value, callback) { |
611 | 6 | var list = handlers[name]; |
612 | 6 | if (list) { |
613 | 6 | for (var i = 0, l = list.length; i < l; i++) { |
614 | 6 | list[i](value); |
615 | | } |
616 | | } |
617 | 6 | callback && callback(); |
618 | | } |
619 | | |
620 | 2 | function extend(name, options, callback) { |
621 | | |
622 | 9 | var meta = {}; |
623 | | // Pull from cache if it's already loaded. |
624 | 9 | if (!options.redefine && apis.hasOwnProperty(name)) { |
625 | 1 | var err = new Error("EEXIST: Extension API already defined for " + name); |
626 | 1 | err.code = "EEXIST"; |
627 | 1 | return callback(err); |
628 | | } |
629 | | |
630 | 8 | var fn; |
631 | | |
632 | | // The user can pass in a path to a file to require |
633 | 8 | if (options.file) { |
634 | 12 | try { fn = require(options.file); } |
635 | 0 | catch (err) { return callback(err); } |
636 | 6 | fn(vfs, onEvaluate); |
637 | | } |
638 | | |
639 | | // User can pass in code as a pre-buffered string |
640 | 2 | else if (options.code) { |
641 | 2 | try { fn = evaluate(options.code); } |
642 | 0 | catch (err) { return callback(err); } |
643 | 1 | fn(vfs, onEvaluate); |
644 | | } |
645 | | |
646 | | // Or they can provide a readable stream |
647 | 1 | else if (options.stream) { |
648 | 1 | consumeStream(options.stream, function (err, code) { |
649 | 1 | if (err) return callback(err); |
650 | 1 | var fn; |
651 | 1 | try { |
652 | 1 | fn = evaluate(code); |
653 | | } catch(err) { |
654 | 0 | return callback(err); |
655 | | } |
656 | 1 | fn(vfs, onEvaluate); |
657 | | }); |
658 | | } |
659 | | |
660 | | else { |
661 | 0 | return callback(new Error("must provide `file`, `code`, or `stream` when cache is empty for " + name)); |
662 | | } |
663 | | |
664 | 8 | function onEvaluate(err, exports) { |
665 | 8 | if (err) { |
666 | 0 | return callback(err); |
667 | | } |
668 | 8 | exports.names = Object.keys(exports); |
669 | 8 | exports.name = name; |
670 | 8 | apis[name] = exports; |
671 | 8 | meta.api = exports; |
672 | 8 | callback(null, meta); |
673 | | } |
674 | | |
675 | | } |
676 | | |
677 | 2 | function unextend(name, options, callback) { |
678 | 7 | delete apis[name]; |
679 | 7 | callback(); |
680 | | } |
681 | | |
682 | 2 | function use(name, options, callback) { |
683 | 4 | var api = apis[name]; |
684 | 4 | if (!api) { |
685 | 2 | var err = new Error("ENOENT: There is no API extension named " + name); |
686 | 2 | err.code = "ENOENT"; |
687 | 2 | return callback(err); |
688 | | } |
689 | 2 | callback(null, {api:api}); |
690 | | } |
691 | | |
692 | | //////////////////////////////////////////////////////////////////////////////// |
693 | | |
694 | 2 | return vfs; |
695 | | |
696 | | }; |
697 | | |
698 | | // Consume all data in a readable stream and call callback with full buffer. |
699 | 1 | function consumeStream(stream, callback) { |
700 | 1 | var chunks = []; |
701 | 1 | stream.on("data", onData); |
702 | 1 | stream.on("end", onEnd); |
703 | 1 | stream.on("error", onError); |
704 | 1 | function onData(chunk) { |
705 | 1 | chunks.push(chunk); |
706 | | } |
707 | 1 | function onEnd() { |
708 | 1 | cleanup(); |
709 | 1 | callback(null, chunks.join("")); |
710 | | } |
711 | 1 | function onError(err) { |
712 | 0 | cleanup(); |
713 | 0 | callback(err); |
714 | | } |
715 | 1 | function cleanup() { |
716 | 1 | stream.removeListener("data", onData); |
717 | 1 | stream.removeListener("end", onEnd); |
718 | 1 | stream.removeListener("error", onError); |
719 | | } |
720 | | } |
721 | | |
722 | | // node-style eval |
723 | 1 | function evaluate(code) { |
724 | 2 | var exports = {}; |
725 | 2 | var module = { exports: exports }; |
726 | 2 | vm.runInNewContext(code, { |
727 | | require: require, |
728 | | exports: exports, |
729 | | module: module, |
730 | | console: console, |
731 | | global: global, |
732 | | process: process, |
733 | | Buffer: Buffer, |
734 | | setTimeout: setTimeout, |
735 | | clearTimeout: clearTimeout, |
736 | | setInterval: setInterval, |
737 | | clearInterval: clearInterval |
738 | | }, "dynamic-" + Date.now().toString(36), true); |
739 | 2 | return module.exports; |
740 | | } |
741 | | |
742 | | // Calculate a proper etag from a nodefs stat object |
743 | 1 | function calcEtag(stat) { |
744 | 14 | return (stat.isFile() ? '': 'W/') + '"' + (stat.ino || 0).toString(36) + "-" + stat.size.toString(36) + "-" + stat.mtime.valueOf().toString(36) + '"'; |
745 | | } |