Coverage

90%
421
380
41

localfs.js

90%
421
380
41
LineHitsSource
11var fs = require('fs');
21var net = require('net');
31var childProcess = require('child_process');
41var join = require('path').join;
51var pathResolve = require('path').resolve;
61var dirname = require('path').dirname;
71var basename = require('path').basename;
81var Stream = require('stream').Stream;
91var getMime = require('simple-mime')("application/octet-stream");
101var vm = require('vm');
11
121module.exports = function setup(fsOptions) {
13
14 // Check and configure options
152 var root = fsOptions.root;
162 if (!root) throw new Error("root is a required option");
172 if (root[0] !== "/") throw new Error("root path must start in /");
182 if (root[root.length - 1] !== "/") root += "/";
192 var base = root.substr(0, root.length - 1);
202 var umask = fsOptions.umask || 0750;
212 if (fsOptions.hasOwnProperty('defaultEnv')) {
221 fsOptions.defaultEnv.__proto__ = process.env;
23 } else {
241 fsOptions.defaultEnv = process.env;
25 }
26
27 // Storage for extension APIs
282 var apis = {};
29 // Storage for event handlers
302 var handlers = {};
31
32 // Export the API
332 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)
722 function resolvePath(path, callback, alreadyRooted) {
73111 if (!alreadyRooted) path = join(root, path);
74113 if (fsOptions.checkSymlinks) fs.realpath(path, check);
751 else check(null, path);
76
7757 function check(err, path) {
7868 if (err) return callback(err);
7946 if (!(path === base || path.substr(0, root.length) === root)) {
801 err = new Error("EACCESS: '" + path + "' not in '" + root + "'");
811 err.code = "EACCESS";
821 return callback(err);
83 }
8445 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)
902 function open(path, flags, mode, callback) {
9113 resolvePath(path, function (err, path) {
9215 if (err) return callback(err);
9311 fs.open(path, flags, mode, function (err, fd) {
9411 if (err) return callback(err);
9511 fs.fstat(fd, function (err, stat) {
9611 if (err) return callback(err);
9711 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.
1052 function createStatEntry(file, fullpath, callback) {
1069 fs.lstat(fullpath, function (err, stat) {
1079 var entry = {
108 name: file
109 };
110
1119 if (err) {
1121 entry.err = err;
1131 return callback(entry);
114 } else {
1158 entry.size = stat.size;
1168 entry.mtime = stat.mtime.valueOf();
117
1188 if (stat.isDirectory()) {
1192 entry.mime = "inode/directory";
1206 } else if (stat.isBlockDevice()) entry.mime = "inode/blockdevice";
1216 else if (stat.isCharacterDevice()) entry.mime = "inode/chardevice";
1228 else if (stat.isSymbolicLink()) entry.mime = "inode/symlink";
1234 else if (stat.isFIFO()) entry.mime = "inode/fifo";
1244 else if (stat.isSocket()) entry.mime = "inode/socket";
125 else {
1264 entry.mime = getMime(fullpath);
127 }
128
1298 if (!stat.isSymbolicLink()) {
1306 return callback(entry);
131 }
1322 fs.readlink(fullpath, function (err, link) {
1332 if (err) {
1340 entry.linkErr = err.stack;
1350 return callback(entry);
136 }
1372 entry.link = link;
1382 resolvePath(pathResolve(dirname(fullpath), link), function (err, newpath) {
1392 if (err) {
1400 entry.linkStatErr = err;
1410 return callback(entry);
142 }
1432 createStatEntry(basename(newpath), newpath, function (linkStat) {
1442 entry.linkStat = linkStat;
1452 linkStat.fullPath = newpath.substr(base.length) || "/";
1462 return callback(entry);
147 });
148 }, true/*alreadyRooted*/);
149 });
150 }
151 });
152 }
153
154 // Common logic used by rmdir and rmfile
1552 function remove(path, fn, callback) {
1567 var meta = {};
1577 resolvePath(path, function (err, path) {
1589 if (err) return callback(err);
1595 fn(path, function (err) {
1607 if (err) return callback(err);
1613 return callback(null, meta);
162 });
163 });
164 }
165
166////////////////////////////////////////////////////////////////////////////////
167
1682 function resolve(path, options, callback) {
1695 resolvePath(path, function (err, path) {
1707 if (err) return callback(err);
1713 callback(null, { path: path });
172 }, options.alreadyRooted);
173 }
174
1752 function stat(path, options, callback) {
176
177 // Make sure the parent directory is accessable
1782 resolvePath(dirname(path), function (err, dir) {
1792 if (err) return callback(err);
1802 var file = basename(path);
1812 path = join(dir, file);
1822 createStatEntry(file, path, function (entry) {
1832 if (entry.err) {
1841 return callback(entry.err);
185 }
1861 callback(null, entry);
187 });
188 });
189 }
190
1912 function readfile(path, options, callback) {
192
19313 var meta = {};
194
19513 open(path, "r", umask & 0666, function (err, path, fd, stat) {
19615 if (err) return callback(err);
19711 if (stat.isDirectory()) {
1981 fs.close(fd);
1991 var err = new Error("EISDIR: Requested resource is a directory");
2001 err.code = "EISDIR";
2011 return callback(err);
202 }
203
204 // Basic file info
20510 meta.mime = getMime(path);
20610 meta.size = stat.size;
20710 meta.etag = calcEtag(stat);
208
209 // ETag support
21010 if (options.etag === meta.etag) {
2111 meta.notModified = true;
2121 fs.close(fd);
2131 return callback(null, meta);
214 }
215
216 // Range support
2179 if (options.hasOwnProperty('range') && !(options.range.etag && options.range.etag !== meta.etag)) {
2184 var range = options.range;
2194 var start, end;
2204 if (range.hasOwnProperty("start")) {
2212 start = range.start;
2222 end = range.hasOwnProperty("end") ? range.end : meta.size - 1;
223 }
224 else {
2252 if (range.hasOwnProperty("end")) {
2261 start = meta.size - range.end;
2271 end = meta.size - 1;
228 }
229 else {
2301 meta.rangeNotSatisfiable = "Invalid Range";
2311 fs.close(fd);
2321 return callback(null, meta);
233 }
234 }
2353 if (end < start || start < 0 || end >= stat.size) {
2361 meta.rangeNotSatisfiable = "Range out of bounds";
2371 fs.close(fd);
2381 return callback(null, meta);
239 }
2402 options.start = start;
2412 options.end = end;
2422 meta.size = end - start + 1;
2432 meta.partialContent = { start: start, end: end, size: stat.size };
244 }
245
246 // HEAD request support
2477 if (options.hasOwnProperty("head")) {
2483 fs.close(fd);
2493 return callback(null, meta);
250 }
251
252 // Read the file as a stream
2534 try {
2544 options.fd = fd;
2554 meta.stream = new fs.ReadStream(path, options);
256 } catch (err) {
2570 fs.close(fd);
2580 return callback(err);
259 }
2604 callback(null, meta);
261 });
262 }
263
2642 function readdir(path, options, callback) {
265
2666 var meta = {};
267
2686 resolvePath(path, function (err, path) {
2697 if (err) return callback(err);
2705 fs.stat(path, function (err, stat) {
2715 if (err) return callback(err);
2725 if (!stat.isDirectory()) {
2731 err = new Error("ENOTDIR: Requested resource is not a directory");
2741 err.code = "ENOTDIR";
2751 return callback(err);
276 }
277
278 // ETag support
2794 meta.etag = calcEtag(stat);
2804 if (options.etag === meta.etag) {
2811 meta.notModified = true;
2821 return callback(null, meta);
283 }
284
2853 fs.readdir(path, function (err, files) {
2863 if (err) return callback(err);
2873 if (options.head) {
2882 return callback(null, meta);
289 }
2901 var stream = new Stream();
2911 stream.readable = true;
2921 var paused;
2931 stream.pause = function () {
2940 if (paused === true) return;
2950 paused = true;
296 };
2971 stream.resume = function () {
2981 if (paused === false) return;
2991 paused = false;
3001 getNext();
301 };
3021 meta.stream = stream;
3031 callback(null, meta);
3041 var index = 0;
3051 stream.resume();
3061 function getNext() {
3077 if (index === files.length) return done();
3085 var file = files[index++];
3095 var fullpath = join(path, file);
310
3115 createStatEntry(file, fullpath, function onStatEntry(entry) {
3125 stream.emit("data", entry);
313
3145 if (!paused) {
3155 getNext();
316 }
317 });
318 }
3191 function done() {
3201 stream.emit("end");
321 }
322 });
323 });
324 });
325 }
326
3272 function mkfile(path, options, realCallback) {
3286 var meta = {};
3296 var called;
3306 var callback = function (err, meta) {
3318 if (called) {
3322 if (err) {
3330 if (meta.stream) meta.stream.emit("error", err);
3340 else console.error(err.stack);
335 }
3364 else if (meta.stream) meta.stream.emit("saved");
3372 return;
338 }
3396 called = true;
3406 return realCallback.apply(this, arguments);
341 };
342
3436 if (options.stream && !options.stream.readable) {
3440 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
3486 var readable = options.stream;
3496 if (readable) {
3508 if (readable.pause) readable.pause();
3514 var buffer = [];
3524 readable.on("data", onData);
3534 readable.on("end", onEnd);
354 }
355
3566 function onData(chunk) {
3570 buffer.push(["data", chunk]);
358 }
3596 function onEnd() {
3600 buffer.push(["end"]);
361 }
3626 function error(err) {
3630 if (readable) {
3640 readable.removeListener("data", onData);
3650 readable.removeListener("end", onEnd);
3660 if (readable.destroy) readable.destroy();
367 }
3680 if (err) callback(err);
369 }
370
371 // Make sure the user has access to the directory and get the real path.
3726 resolvePath(path, function (err, resolvedPath) {
3736 if (err) {
3744 if (err.code !== "ENOENT") {
3750 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.
3794 resolvePath(dirname(path), function (err, dir) {
3804 if (err) return error(err);
3814 onPath(join(dir, basename(path)));
382 });
3834 return;
384 }
3852 onPath(resolvedPath);
386 });
387
3886 function onPath(path) {
38912 if (!options.mode) options.mode = umask & 0666;
3906 var writable = new fs.WriteStream(path, options);
3916 if (readable) {
3924 readable.pipe(writable);
393 }
394 else {
3952 meta.stream = writable;
3962 callback(null, meta);
397 }
3986 var hadError;
3996 writable.once('error', function (err) {
4000 hadError = true;
4010 error(err);
402 });
4036 writable.on('close', function () {
4046 if (hadError) return;
4056 callback(null, meta);
406 });
407
4086 if (readable) {
409 // Stop buffering events and playback anything that happened.
4104 readable.removeListener("data", onData);
4114 readable.removeListener("end", onEnd);
4124 buffer.forEach(function (event) {
4130 readable.emit.apply(readable, event);
414 });
415 // Resume the input stream if possible
4168 if (readable.resume) readable.resume();
417 }
418 }
419 }
420
4212 function mkdir(path, options, callback) {
4223 var meta = {};
423 // Make sure the user has access to the parent directory and get the real path.
4243 resolvePath(dirname(path), function (err, dir) {
4253 if (err) return callback(err);
4263 path = join(dir, basename(path));
4273 fs.mkdir(path, function (err) {
4285 if (err) return callback(err);
4291 callback(null, meta);
430 });
431 });
432 }
433
4342 function rmfile(path, options, callback) {
4353 remove(path, fs.unlink, callback);
436 }
437
4382 function rmdir(path, options, callback) {
4394 if (options.recursive) {
4401 remove(path, function(path, callback) {
4411 execFile("rm", {args: ["-rf", path]}, callback);
442 }, callback);
443 }
444 else {
4453 remove(path, fs.rmdir, callback);
446 }
447 }
448
4492 function rename(path, options, callback) {
4503 var from, to;
4513 if (options.from) {
4522 from = options.from; to = path;
453 }
4542 else if (options.to) {
4554 from = path; to = options.to;
456 }
457 else {
4580 return callback(new Error("Must specify either options.from or options.to"));
459 }
4603 var meta = {};
461 // Get real path to source
4623 resolvePath(from, function (err, from) {
4634 if (err) return callback(err);
464 // Get real path to target dir
4652 resolvePath(dirname(to), function (err, dir) {
4662 if (err) return callback(err);
4672 to = join(dir, basename(to));
468 // Rename the file
4692 fs.rename(from, to, function (err) {
4702 if (err) return callback(err);
4712 callback(null, meta);
472 });
473 });
474 });
475 }
476
4772 function copy(path, options, callback) {
4783 var from, to;
4793 if (options.from) {
4802 from = options.from; to = path;
481 }
4822 else if (options.to) {
4834 from = path; to = options.to;
484 }
485 else {
4860 return callback(new Error("Must specify either options.from or options.to"));
487 }
4883 readfile(from, {}, function (err, meta) {
4894 if (err) return callback(err);
4902 mkfile(to, {stream: meta.stream}, callback);
491 });
492 }
493
4942 function symlink(path, options, callback) {
4952 if (!options.target) return callback(new Error("options.target is required"));
4962 var meta = {};
497 // Get real path to target dir
4982 resolvePath(dirname(path), function (err, dir) {
4992 if (err) return callback(err);
5002 path = join(dir, basename(path));
5012 fs.symlink(options.target, path, function (err) {
5023 if (err) return callback(err);
5031 callback(null, meta);
504 });
505 });
506 }
507
5082 function watch(path, options, callback) {
5092 var meta = {};
5102 resolvePath(path, function (err, path) {
5112 if (err) return callback(err);
5122 if (options.file) {
5130 meta.watcher = fs.watchFile(path, options, function () {});
5140 meta.watcher.close = function () {
5150 fs.unwatchFile(path);
516 };
517 }
518 else {
5192 meta.watcher = fs.watch(path, options, function () {});
520 }
5212 callback(null, meta);
522 });
523 }
524
5252 function connect(port, options, callback) {
5261 var retries = options.hasOwnProperty('retries') ? options.retries : 5;
5271 var retryDelay = options.hasOwnProperty('retryDelay') ? options.retryDelay : 50;
5281 tryConnect();
5291 function tryConnect() {
5301 var socket = net.connect(port, function () {
5311 if (options.hasOwnProperty('encoding')) {
5321 socket.setEncoding(options.encoding);
533 }
5341 callback(null, {stream:socket});
535 });
5361 socket.once("error", function (err) {
5370 if (err.code === "ECONNREFUSED" && retries) {
5380 setTimeout(tryConnect, retryDelay);
5390 retries--;
5400 retryDelay *= 2;
5410 return;
542 }
5430 return callback(err);
544 });
545 }
546 }
547
5482 function spawn(executablePath, options, callback) {
5492 var args = options.args || [];
550
5512 if (options.hasOwnProperty('env')) {
5521 options.env.__proto__ = fsOptions.defaultEnv;
553 } else {
5541 options.env = fsOptions.defaultEnv;
555 }
556
5572 var child;
5582 try {
5592 child = childProcess.spawn(executablePath, args, options);
560 } catch (err) {
5610 return callback(err);
562 }
5632 if (options.resumeStdin) child.stdin.resume();
5642 if (options.hasOwnProperty('stdoutEncoding')) {
5652 child.stdout.setEncoding(options.stdoutEncoding);
566 }
5672 if (options.hasOwnProperty('stderrEncoding')) {
5681 child.stderr.setEncoding(options.stderrEncoding);
569 }
570
5712 callback(null, {
572 process: child
573 });
574 }
575
5762 function execFile(executablePath, options, callback) {
577
5783 if (options.hasOwnProperty('env')) {
5791 options.env.__proto__ = fsOptions.defaultEnv;
580 } else {
5812 options.env = fsOptions.defaultEnv;
582 }
583
5843 childProcess.execFile(executablePath, options.args || [], options, function (err, stdout, stderr) {
5853 if (err) return callback(err);
5863 callback(null, {
587 stdout: stdout,
588 stderr: stderr
589 });
590 });
591 }
592
5932 function on(name, handler, callback) {
5946 if (!handlers[name]) handlers[name] = [];
5955 handlers[name].push(handler);
5965 callback && callback();
597 }
598
5992 function off(name, handler, callback) {
6005 var list = handlers[name];
6015 if (list) {
6025 var index = list.indexOf(handler);
6035 if (index >= 0) {
6045 list.splice(index, 1);
605 }
606 }
6075 callback && callback();
608 }
609
6102 function emit(name, value, callback) {
6116 var list = handlers[name];
6126 if (list) {
6136 for (var i = 0, l = list.length; i < l; i++) {
6146 list[i](value);
615 }
616 }
6176 callback && callback();
618 }
619
6202 function extend(name, options, callback) {
621
6229 var meta = {};
623 // Pull from cache if it's already loaded.
6249 if (!options.redefine && apis.hasOwnProperty(name)) {
6251 var err = new Error("EEXIST: Extension API already defined for " + name);
6261 err.code = "EEXIST";
6271 return callback(err);
628 }
629
6308 var fn;
631
632 // The user can pass in a path to a file to require
6338 if (options.file) {
63412 try { fn = require(options.file); }
6350 catch (err) { return callback(err); }
6366 fn(vfs, onEvaluate);
637 }
638
639 // User can pass in code as a pre-buffered string
6402 else if (options.code) {
6412 try { fn = evaluate(options.code); }
6420 catch (err) { return callback(err); }
6431 fn(vfs, onEvaluate);
644 }
645
646 // Or they can provide a readable stream
6471 else if (options.stream) {
6481 consumeStream(options.stream, function (err, code) {
6491 if (err) return callback(err);
6501 var fn;
6511 try {
6521 fn = evaluate(code);
653 } catch(err) {
6540 return callback(err);
655 }
6561 fn(vfs, onEvaluate);
657 });
658 }
659
660 else {
6610 return callback(new Error("must provide `file`, `code`, or `stream` when cache is empty for " + name));
662 }
663
6648 function onEvaluate(err, exports) {
6658 if (err) {
6660 return callback(err);
667 }
6688 exports.names = Object.keys(exports);
6698 exports.name = name;
6708 apis[name] = exports;
6718 meta.api = exports;
6728 callback(null, meta);
673 }
674
675 }
676
6772 function unextend(name, options, callback) {
6787 delete apis[name];
6797 callback();
680 }
681
6822 function use(name, options, callback) {
6834 var api = apis[name];
6844 if (!api) {
6852 var err = new Error("ENOENT: There is no API extension named " + name);
6862 err.code = "ENOENT";
6872 return callback(err);
688 }
6892 callback(null, {api:api});
690 }
691
692////////////////////////////////////////////////////////////////////////////////
693
6942 return vfs;
695
696};
697
698// Consume all data in a readable stream and call callback with full buffer.
6991function consumeStream(stream, callback) {
7001 var chunks = [];
7011 stream.on("data", onData);
7021 stream.on("end", onEnd);
7031 stream.on("error", onError);
7041 function onData(chunk) {
7051 chunks.push(chunk);
706 }
7071 function onEnd() {
7081 cleanup();
7091 callback(null, chunks.join(""));
710 }
7111 function onError(err) {
7120 cleanup();
7130 callback(err);
714 }
7151 function cleanup() {
7161 stream.removeListener("data", onData);
7171 stream.removeListener("end", onEnd);
7181 stream.removeListener("error", onError);
719 }
720}
721
722// node-style eval
7231function evaluate(code) {
7242 var exports = {};
7252 var module = { exports: exports };
7262 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);
7392 return module.exports;
740}
741
742// Calculate a proper etag from a nodefs stat object
7431function calcEtag(stat) {
74414 return (stat.isFile() ? '': 'W/') + '"' + (stat.ino || 0).toString(36) + "-" + stat.size.toString(36) + "-" + stat.mtime.valueOf().toString(36) + '"';
745}