├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── localfs.js ├── package.json └── test ├── math.js ├── mock ├── dir │ ├── smile.png │ └── stuff.json ├── dirLink ├── file.txt ├── fileLink.txt └── listing.json ├── node_modules └── vfs-local └── test-local.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.8 5 | - 0.6 6 | script: npm test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Ajax.org B.V 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VFS Local 2 | 3 | [![Build Status](https://secure.travis-ci.org/c9/vfs-local.png?branch=master)](http://travis-ci.org/c9/vfs-local) 4 | 5 | VFS is an abstract interface for working with systems. This module is the reference implementation and provides a vfs interface to the local system using node apis. This is also often used in conjuction with `vfs-socket` to provide the vfs interface to a remote system over any kind of network socket. 6 | 7 | ## setup(fsOptions) -> vfs 8 | 9 | This is the main exports of the module. It's a function that returns a vfs-local instance. 10 | 11 | The `fsOptions` argument is an object that can contain the following properties: 12 | 13 | - `root`: Root path to mount the vfs. All fs operations are done relative to this root. Access outside this root is not allowed. 14 | - `checkSymlinks`: Normally paths are resolved using pure string manipulation. This options will aditionally realpath any symlinks to get the absolute path on the filesystem. Pratically it prevents you from using symlinks that point outside the `root`. 15 | - `umask`: Default umask for creating files (defaults to 0750) 16 | - `defaultEnv`: A shallow hash of env values to inject into child processes. 17 | 18 | ## vfs.resolve(path, options, callback(err, meta)) 19 | 20 | This takes a virtual path as `path` and returns the resolved path in the real filesystem as `meta.path` in the callback. 21 | 22 | This function has one option `options.alreadyRooted` that tells resolve to not prefix the path with the vfs root. 23 | 24 | ## vfs.stat(path, options, callback(err, stat)) 25 | 26 | Loads the stat information for a single path entity as `stat` in the callback. This is a javascript object with the following fields: 27 | 28 | - `name`: The basename of the file path (eg: file.txt). 29 | - `size`: The size of the entity in bytes. 30 | - `mtime`: The mtime of the file in ms since epoch. 31 | - `mime`: The mime type of the entity. Folders will have a mime that matches `/(directory|folder)$/`. This vfs implementation will give `inode/directory` for directories. 32 | - `link`: If the file is a symlink, this property will contain the link data as a string. 33 | - `linkStat`: The stat information for what the link points to. 34 | - `fullPath`: The link stat object will have an additional property that's the resolved path relative to the vfs root. 35 | 36 | ## vfs.readfile(path, options, callback(err, meta)) 37 | 38 | Open a file as a readable stream. 39 | 40 | Options can include: 41 | 42 | - `head`: Set to any truthy value to skip creating the file stream. 43 | - `etag`: Make a conditional request. If the provided etag value matches the entity, the `meta` response will skip the stream and instead have a truthy `notModified` property. 44 | - `range`: Read only a part of a file. The range value is an object with `start`, `end`, and/or `etag`. Start and end are byte offsets and inclusive. Etag means to only get the partial if the etag matches. The `meta` object in the response will have a `partialContent` property containing an object with `start`, `end`, and `size` (total size). The `meta.size` property will be only the size of the bytes in the partial response. If the `options.range` object is invalid `meta` will contain `rangeNotSatisfiable`. 45 | - Any other options that node's `fs.createReadStream` accepts (like `encoding`) 46 | 47 | Meta may include: 48 | 49 | - `stream`: A readable node stream containing the contents. 50 | - `size`: The length of the stream in bytes. 51 | - `mime`: The mime type of the file. 52 | - `etag`: The etag for the file. 53 | - `rangeNotSatisfiable`: The range was bad. 54 | - `partialContent`: The range was successful, this has the range offsets. 55 | - `notModified`: The conditional etag matched. 56 | 57 | ## vfs.readdir(path, options, callback(err, meta)) 58 | 59 | Read the contents of a directory as a stream of stat objects. The stream in `meta.stream` emits vfs stat objects as documented above for the `data` events. This is not a byte stream. 60 | 61 | Options can include: 62 | 63 | - `head`: Set to any truthy value to skip creating the event stream. 64 | - `etag`: Provide an etag and the function will set `meta.partialContent` to true and skip the stream if it matches. 65 | 66 | Meta may include: 67 | 68 | - `stream`: The node stream that emits stat objects. 69 | - `etag`: The weak etag for the directory listing. 70 | - `notModified`: The conditional etag matched. 71 | 72 | ## vfs.mkfile(path, options, callback) 73 | 74 | Create or overwrite a file. This has two modes. In one mode, the caller provides a readable stream which is then piped to the filesystem. In the other mode, the callback meta contains a writable stream to the filesystem. 75 | 76 | Options can include: 77 | 78 | - `stream`: A readable streaem to be written to the filesystem. 79 | - `parents`: make parent directories as needed 80 | - Any other options are passed through to node's `fs.createWriteStream`. 81 | 82 | Meta may include: 83 | 84 | - `stream`: If a stream wasn't provided in the options, a writable stream is returned in the meta. This stream will emit "done" when it's done being written to. Any errors will also be emitted on this stream object since the callback will have already fired. 85 | 86 | ## vfs.mkdir(path, options, callback) 87 | 88 | Create a directory at `path`. Will error with `EEXIST` if something is already at the path. 89 | 90 | Options can include: 91 | 92 | - `parents`: make parent directories as needed 93 | 94 | 95 | ## vfs.rmfile(path, options, callback) 96 | 97 | Delete a file at `path`. 98 | 99 | If `options.recursive` is truthy, it will instead shell out to `rm -rf` after resolving the path. 100 | 101 | ## vfs.rmdir(path, options, callback) 102 | 103 | Delete a directory at `path` 104 | 105 | ## vfs.rename(path, options, callback) 106 | 107 | Rename/move a file or directory. There are two modes depending on the option passed in. 108 | 109 | Options can include: 110 | 111 | - `to`: This property is the path to the target, the `path` option will be read as the source. 112 | - `from`: This property is the path of the source, the `path` option will be read as the target. 113 | 114 | ## vfs.copy(path, options, callback) 115 | 116 | Copy a file. There are two modes depending on the option passed in. 117 | 118 | Options can include: 119 | 120 | - `to`: This property is the path to the new file, the `path` option will be read as the source. 121 | - `from`: This property is the path of the source, the `path` option will be read as the new file. 122 | 123 | ## vfs.symlink(path, options, callback) 124 | 125 | Create a special symlink file at `path`. The symlink data will be the value of `options.target`. No translation of the link data is done. It's taken literally. 126 | 127 | ## vfs.watch(path, options, callback) 128 | 129 | Wrapper around node's `fs.watch` and `fs.watchFile`. 130 | 131 | If `options.file` is truthy, then `fs.watchFile` is used. Otherwise `fs.watch` is used. The watcher will be at `meta.watcher`. For consistency, `watchFile` objects will have a `close()` method added that internally calls `fs.unwatchFile` for the original path. 132 | 133 | ## vfs.connect(port, options, callback) 134 | 135 | Make a TCP connection and return the duplex stream as `meta.stream`. 136 | 137 | Options can include: 138 | 139 | - `retries`: Number of times to retry connecting. Defaults to 5. 140 | - `retryDelay`: The delay between retrieson ms. Defaults to 50. 141 | 142 | ## vfs.spawn(executablePath, options, callback) 143 | 144 | Spawns a child process and returns a process object complete with three stdio streams. Wraps node's `child_process.spawn`. 145 | 146 | The `executablePath` is a pre-resolved path. It will not get prefixed. So if you want a relative path within the vfs root, use `vfs.resolve()`. 147 | 148 | Options can include: 149 | 150 | - `args`: An array of args to pass to the executable. 151 | - `stdoutEncoding`: The encoding to use on the stdout stream. 152 | - `stderrEncoding`: The encoding to use on the stderr stream. 153 | - Any other options you want to pass through to node's `child_process.spawn` (`env`, ...) 154 | 155 | Meta will contain: 156 | 157 | - `process`: The child process. This will have `stdin`, `stdout`, and `stderr` stream properties s as well as emit some events itself. 158 | 159 | ## vfs.execFile(executablePath, options, callback) 160 | 161 | Execute a process and buffers the output till the process exist. Don't use this on anything that will output substantial data or run for a long time. Wraps node's `child_process.execFile`. 162 | 163 | The options are the same as `vfs.spawn()`. 164 | 165 | Meta will contain: 166 | 167 | - `stdout`: The buffered stdout data. 168 | - `stderr`: The buffered stderr data. 169 | 170 | ## vfs.on(event, handler, callback) 171 | 172 | Listen for custom vfs events. `event` is a string identifier for the event type. `handler` is a function that will get called every time the event is emitted. `callback` will be called once the registration is complete. This is required since vfs is usually used over some async socket transport. 173 | 174 | ## vfs.off(event, handler, callback) 175 | 176 | Remove an event handler. The arguments are the same as `vfs.on`, but remove the listener instead of registering it. 177 | 178 | ## vfs.emit(event, value, callback) 179 | 180 | Emit an event to all listening handlers. The callback means that the event was sent to all handlers, but they may not have received it yet if their transport is slower than yours. 181 | 182 | ## vfs.extend(name, options, callback) 183 | 184 | This API is provided to extend the capabilities of vfs. This is useful for when the vfs is on a remote filesystem and you want to run some custom node code there. 185 | 186 | See the unit tests for docs. 187 | 188 | TODO: document this better 189 | 190 | ## vfs.unextend(name, options, callback) 191 | 192 | Remove an extension by name. 193 | 194 | ## vfs.use(name, options, callback) 195 | 196 | Get a reference to an existing vfs extension api. 197 | -------------------------------------------------------------------------------- /localfs.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var net = require("net"); 3 | var childProcess = require("child_process"); 4 | var constants = require("constants"); 5 | var join = require("path").join; 6 | var pathResolve = require("path").resolve; 7 | var pathNormalize = require("path").normalize; 8 | var dirname = require("path").dirname; 9 | var basename = require("path").basename; 10 | var Stream = require("stream").Stream; 11 | var getMime = require("simple-mime")("application/octet-stream"); 12 | var vm = require("vm"); 13 | var exists = fs.exists || require("path").exists; 14 | var crypto = require("crypto"); 15 | 16 | module.exports = function setup(fsOptions) { 17 | var pty; 18 | if (!fsOptions.nopty) { 19 | try { 20 | if (fsOptions.local) throw new Error(); 21 | pty = fsOptions.local ? require('pty.nw.js') : require('pty.js'); 22 | } catch(e) { 23 | console.warn("unable to initialize " 24 | + (fsOptions.local ? "pty.nw.js" : "pty.js") + ":"); 25 | console.warn(e); 26 | pty = function(){}; 27 | } 28 | } 29 | else { 30 | pty = function(){ 31 | console.log("PTY is not supported."); 32 | }; 33 | } 34 | 35 | // Get the separator char. In Node 0.8, we can use path.sep instead 36 | var pathSep = pathNormalize("/"); 37 | 38 | var METAPATH = fsOptions.metapath; 39 | var WSMETAPATH = fsOptions.wsmetapath; 40 | var TESTING = fsOptions.testing; 41 | 42 | // Check and configure options 43 | var root = fsOptions.root; 44 | if (!root) throw new Error("root is a required option"); 45 | root = pathNormalize(root); 46 | 47 | if (pathSep == "/" && root[0] !== "/") throw new Error("root path must start in /"); 48 | if (root[root.length - 1] !== pathSep) root += pathSep; 49 | 50 | var base = root.substr(0, root.length - 1); 51 | // root = "/" doesn't work on windows 52 | if (pathSep == "\\" && root == pathSep) root = ""; 53 | 54 | var umask = fsOptions.umask || 0750; 55 | if (fsOptions.hasOwnProperty('defaultEnv')) { 56 | fsOptions.defaultEnv.__proto__ = process.env; 57 | } else { 58 | fsOptions.defaultEnv = process.env; 59 | } 60 | 61 | // Storage for extension APIs 62 | var apis = {}; 63 | // Storage for event handlers 64 | var handlers = {}; 65 | 66 | // Export the API 67 | var vfs = { 68 | // File management 69 | resolve: resolve, 70 | stat: stat, 71 | readfile: readfile, 72 | readdir: readdir, 73 | mkfile: mkfile, 74 | mkdir: mkdir, 75 | mkdirP: mkdirP, 76 | rmfile: rmfile, 77 | rmdir: rmdir, 78 | rename: rename, 79 | copy: copy, 80 | symlink: symlink, 81 | 82 | // Retrieve Metadata 83 | metadata: metadata, 84 | 85 | // Wrapper around fs.watch or fs.watchFile 86 | watch: watch, 87 | 88 | // Network connection 89 | connect: connect, 90 | 91 | // Process Management 92 | spawn: spawn, 93 | execFile: execFile, 94 | 95 | // Basic async event emitter style API 96 | on: on, 97 | off: off, 98 | emit: emit, 99 | 100 | // Extending the API 101 | extend: extend, 102 | unextend: unextend, 103 | use: use 104 | }; 105 | 106 | //////////////////////////////////////////////////////////////////////////////// 107 | 108 | // Realpath a file and check for access 109 | // callback(err, path) 110 | function resolvePath(path, options, callback) { 111 | if (!callback) { 112 | callback = options; 113 | options = {}; 114 | } 115 | 116 | var alreadyRooted = options.alreadyRooted; 117 | var checkSymlinks = options.checkSymlinks === undefined 118 | ? true : options.checkSymlinks; 119 | var isHome = false; 120 | 121 | if (checkSymlinks === undefined) 122 | checkSymlinks = true; 123 | if (path.substr(0, 2) == "~/") { 124 | isHome = true; 125 | path = process.env.HOME + path.substr(1); 126 | } 127 | else if (!alreadyRooted) 128 | path = join(root, path); 129 | 130 | if (checkSymlinks && fsOptions.checkSymlinks && !alreadyRooted) 131 | fs.realpath(path, check); 132 | else check(null, path); 133 | 134 | function check(err, path) { 135 | if (err) return callback(err); 136 | 137 | if (!options.nocheck) { 138 | if (!(path === base || path.substr(0, root.length) === root) 139 | && !isHome) { 140 | err = new Error("EACCESS: '" + path + "' not in '" + root + "'"); 141 | err.code = "EACCESS"; 142 | return callback(err); 143 | } 144 | } 145 | callback(null, path); 146 | } 147 | } 148 | 149 | // A wrapper around fs.open that enforces permissions and gives extra data in 150 | // the callback. (err, path, fd, stat) 151 | function open(path, flags, mode, callback) { 152 | resolvePath(path, function (err, path) { 153 | if (err) return callback(err); 154 | fs.open(path, flags, mode, function (err, fd) { 155 | if (err) return callback(err); 156 | fs.fstat(fd, function (err, stat) { 157 | if (err) return callback(err); 158 | callback(null, path, fd, stat); 159 | }); 160 | }); 161 | }); 162 | } 163 | 164 | // This helper function doesn't follow node conventions in the callback, 165 | // there is no err, only entry. 166 | function createStatEntry(file, fullpath, callback) { 167 | fs.lstat(fullpath, function (err, stat) { 168 | var entry = { 169 | name: file 170 | }; 171 | 172 | if (err) { 173 | entry.err = err; 174 | return callback(entry); 175 | } else { 176 | entry.size = stat.size; 177 | entry.mtime = stat.mtime.valueOf(); 178 | 179 | if (stat.isDirectory()) { 180 | entry.mime = "inode/directory"; 181 | } else if (stat.isBlockDevice()) entry.mime = "inode/blockdevice"; 182 | else if (stat.isCharacterDevice()) entry.mime = "inode/chardevice"; 183 | else if (stat.isSymbolicLink()) entry.mime = "inode/symlink"; 184 | else if (stat.isFIFO()) entry.mime = "inode/fifo"; 185 | else if (stat.isSocket()) entry.mime = "inode/socket"; 186 | else { 187 | entry.mime = getMime(fullpath); 188 | } 189 | 190 | if (!stat.isSymbolicLink()) { 191 | return callback(entry); 192 | } 193 | fs.readlink(fullpath, function (err, link) { 194 | if (entry.name == link) { 195 | entry.linkStatErr = "ELOOP: recursive symlink"; 196 | return callback(entry); 197 | } 198 | 199 | if (err) { 200 | entry.linkErr = err.stack; 201 | return callback(entry); 202 | } 203 | entry.link = link; 204 | resolvePath(pathResolve(dirname(fullpath), link), {alreadyRooted: true}, function (err, newpath) { 205 | if (err) { 206 | entry.linkStatErr = err; 207 | return callback(entry); 208 | } 209 | createStatEntry(basename(newpath), newpath, function (linkStat) { 210 | entry.linkStat = linkStat; 211 | linkStat.fullPath = newpath.substr(base.length) || "/"; 212 | return callback(entry); 213 | }); 214 | }); 215 | }); 216 | } 217 | }); 218 | } 219 | 220 | // Common logic used by rmdir and rmfile 221 | function remove(path, fn, callback) { 222 | var meta = {}; 223 | resolvePath(path, function (err, realpath) { 224 | if (err) return callback(err); 225 | fn(realpath, function (err) { 226 | if (err) return callback(err); 227 | 228 | // Remove metadata 229 | resolvePath(WSMETAPATH + path, function (err, realpath) { 230 | if (err) return callback(null, meta); 231 | 232 | fn(realpath, function(){ 233 | return callback(null, meta); 234 | }); 235 | }); 236 | }); 237 | }); 238 | } 239 | 240 | //////////////////////////////////////////////////////////////////////////////// 241 | 242 | function resolve(path, options, callback) { 243 | resolvePath(path, options, function (err, path) { 244 | if (err) return callback(err); 245 | callback(null, { path: path }); 246 | }); 247 | } 248 | 249 | function stat(path, options, callback) { 250 | 251 | // Make sure the parent directory is accessable 252 | resolvePath(dirname(path), function (err, dir) { 253 | if (err) return callback(err); 254 | var file = basename(path); 255 | path = join(dir, file); 256 | createStatEntry(file, path, function (entry) { 257 | if (entry.err) { 258 | return callback(entry.err); 259 | } 260 | callback(null, entry); 261 | }); 262 | }); 263 | } 264 | 265 | function metadata(path, data, callback) { 266 | var dirpath = (path.substr(0,5) == "/_/_/" 267 | ? METAPATH + dirname(path.substr(4)) 268 | : WSMETAPATH + "/" + dirname(path)); 269 | resolvePath(dirpath, function (err, dir) { 270 | if (err) return callback(err); 271 | 272 | var file = basename(path); 273 | path = join(dir, file); 274 | 275 | execFile("mkdir", { args: ["-p", dir] }, function(err){ 276 | if (err) return callback(err); 277 | 278 | fs.writeFile(path, JSON.stringify(data), {}, function(err){ 279 | if (err) return callback(err); 280 | callback(null, {}); 281 | }); 282 | }); 283 | }); 284 | } 285 | 286 | function readfile(path, options, callback) { 287 | 288 | var meta = {}; 289 | 290 | open(path, "r", umask & 0666, function (err, path, fd, stat) { 291 | if (err) return callback(err); 292 | if (stat.isDirectory()) { 293 | fs.close(fd); 294 | err = new Error("EISDIR: Requested resource is a directory"); 295 | err.code = "EISDIR"; 296 | return callback(err); 297 | } 298 | 299 | // Basic file info 300 | meta.mime = getMime(path); 301 | meta.size = stat.size; 302 | meta.etag = calcEtag(stat); 303 | 304 | // ETag support 305 | if ((TESTING || stat.mtime % 1000) && options.etag === meta.etag) { 306 | meta.notModified = true; 307 | fs.close(fd); 308 | return callback(null, meta); 309 | } 310 | 311 | // Range support 312 | if (options.hasOwnProperty('range') && !(options.range.etag && options.range.etag !== meta.etag)) { 313 | var range = options.range; 314 | var start, end; 315 | if (range.hasOwnProperty("start")) { 316 | start = range.start; 317 | end = range.hasOwnProperty("end") ? range.end : meta.size - 1; 318 | } 319 | else { 320 | if (range.hasOwnProperty("end")) { 321 | start = meta.size - range.end; 322 | end = meta.size - 1; 323 | } 324 | else { 325 | meta.rangeNotSatisfiable = "Invalid Range"; 326 | fs.close(fd); 327 | return callback(null, meta); 328 | } 329 | } 330 | if (end < start || start < 0 || end >= stat.size) { 331 | meta.rangeNotSatisfiable = "Range out of bounds"; 332 | fs.close(fd); 333 | return callback(null, meta); 334 | } 335 | options.start = start; 336 | options.end = end; 337 | meta.size = end - start + 1; 338 | meta.partialContent = { start: start, end: end, size: stat.size }; 339 | } 340 | 341 | // HEAD request support 342 | if (options.hasOwnProperty("head")) { 343 | fs.close(fd); 344 | return callback(null, meta); 345 | } 346 | 347 | // Read the file as a stream 348 | try { 349 | options.fd = fd; 350 | meta.stream = new fs.ReadStream(path, options); 351 | } catch (err) { 352 | fs.close(fd); 353 | return callback(err); 354 | } 355 | callback(null, meta); 356 | }); 357 | } 358 | 359 | function readdir(path, options, callback) { 360 | var meta = {}; 361 | 362 | resolvePath(path, function (err, path) { 363 | if (err) return callback(err); 364 | fs.stat(path, function (err, stat) { 365 | if (err) return callback(err); 366 | if (!stat.isDirectory()) { 367 | err = new Error("ENOTDIR: Requested resource is not a directory"); 368 | err.code = "ENOTDIR"; 369 | return callback(err); 370 | } 371 | 372 | // ETag support 373 | meta.etag = calcEtag(stat); 374 | if ((TESTING || stat.mtime % 1000) && options.etag === meta.etag) { 375 | meta.notModified = true; 376 | return callback(null, meta); 377 | } 378 | 379 | fs.readdir(path, function (err, files) { 380 | if (err) return callback(err); 381 | if (options.head) { 382 | return callback(null, meta); 383 | } 384 | var stream = new Stream(); 385 | stream.readable = true; 386 | var paused; 387 | stream.pause = function () { 388 | if (paused === true) return; 389 | paused = true; 390 | }; 391 | stream.resume = function () { 392 | if (paused === false) return; 393 | paused = false; 394 | getNext(); 395 | }; 396 | meta.stream = stream; 397 | callback(null, meta); 398 | var index = 0; 399 | stream.resume(); 400 | function getNext() { 401 | if (index === files.length) return done(); 402 | var file = files[index++]; 403 | var fullpath = join(path, file); 404 | 405 | createStatEntry(file, fullpath, function onStatEntry(entry) { 406 | stream.emit("data", entry); 407 | 408 | if (!paused) { 409 | getNext(); 410 | } 411 | }); 412 | } 413 | function done() { 414 | stream.emit("end"); 415 | } 416 | }); 417 | }); 418 | }); 419 | } 420 | 421 | // This is used for creating / overwriting files. It always creates a new tmp 422 | // file and then renames to the final destination. 423 | // It will copy the properties of the existing file is there is one. 424 | function mkfile(path, options, realCallback) { 425 | var meta = {}; 426 | var called; 427 | var callback = function (err) { 428 | if (called) { 429 | if (err) { 430 | if (meta.stream) meta.stream.emit("error", err); 431 | else console.error(err.stack); 432 | } 433 | else if (meta.stream) meta.stream.emit("saved"); 434 | return; 435 | } 436 | called = true; 437 | return realCallback(err, meta); 438 | }; 439 | 440 | if (options.stream && !options.stream.readable) { 441 | return callback(new TypeError("options.stream must be readable.")); 442 | } 443 | 444 | // Pause the input for now since we're not ready to write quite yet 445 | var readable = options.stream; 446 | if (readable) { 447 | if (readable.pause) readable.pause(); 448 | var buffer = []; 449 | readable.on("data", onData); 450 | readable.on("end", onEnd); 451 | } 452 | 453 | function onData(chunk) { 454 | buffer.push(["data", chunk]); 455 | } 456 | function onEnd() { 457 | buffer.push(["end"]); 458 | } 459 | function error(err) { 460 | resume(); 461 | if (tempPath) { 462 | fs.unlink(tempPath, callback.bind(null, err)); 463 | } 464 | else 465 | return callback(err); 466 | } 467 | 468 | function resume() { 469 | if (readable) { 470 | // Stop buffering events and playback anything that happened. 471 | readable.removeListener("data", onData); 472 | readable.removeListener("end", onEnd); 473 | 474 | buffer.forEach(function (event) { 475 | readable.emit.apply(readable, event); 476 | }); 477 | // Resume the input stream if possible 478 | if (readable.resume) readable.resume(); 479 | } 480 | } 481 | 482 | var tempPath; 483 | var resolvedPath = ""; 484 | 485 | mkdir(); 486 | 487 | function mkdir() { 488 | if (options.parents) { 489 | mkdirP(dirname(path), {}, function(err) { 490 | if (err) return error(err); 491 | resolve(); 492 | }); 493 | } 494 | else { 495 | resolve(); 496 | } 497 | } 498 | 499 | // Make sure the user has access to the directory and get the real path. 500 | function resolve() { 501 | resolvePath(path, function (err, _resolvedPath) { 502 | if (err) { 503 | if (err.code !== "ENOENT") { 504 | return error(err); 505 | } 506 | // If checkSymlinks is on we'll get an ENOENT when creating a new file. 507 | // In that case, just resolve the parent path and go from there. 508 | resolvePath(dirname(path), function (err, dir) { 509 | if (err) return error(err); 510 | resolvedPath = join(dir, basename(path)); 511 | createTempFile(); 512 | }); 513 | return; 514 | } 515 | 516 | resolvedPath = _resolvedPath; 517 | createTempFile(); 518 | }); 519 | } 520 | 521 | 522 | function createTempFile() { 523 | tempPath = tmpFile(dirname(resolvedPath), "." + basename(resolvedPath) + "-", "~"); 524 | 525 | var mode = options.mode || umask & 0666; 526 | fs.stat(resolvedPath, function(err, stat) { 527 | if (err && err.code !== "ENOENT") return error(err); 528 | 529 | var uid = process.getuid ? process.getuid() : 0; 530 | var gid = process.getgid ? process.getgid() : 0; 531 | 532 | if (stat) { 533 | mode = stat.mode & 0777; 534 | uid = stat.uid; 535 | gid = stat.gid; 536 | } 537 | 538 | // node 0.8.x adds a "wx" shortcut, but since it's not in 0.6.x we use the 539 | // longhand here. 540 | var flags = constants.O_CREAT | constants.O_WRONLY | constants.O_EXCL; 541 | fs.open(tempPath, flags, mode, function (err, fd) { 542 | if (err) return error(err); 543 | 544 | fs.fchown(fd, uid, gid, function(err) { 545 | fs.close(fd); 546 | if (err) return error(err); 547 | 548 | pipe(fs.WriteStream(tempPath, { 549 | encoding: options.encoding || null, 550 | mode: mode 551 | })); 552 | }); 553 | }); 554 | }); 555 | } 556 | 557 | function pipe(writable) { 558 | var hadError; 559 | 560 | if (readable) { 561 | readable.pipe(writable); 562 | } 563 | else { 564 | writable.on('open', function () { 565 | if (hadError) return; 566 | meta.stream = writable; 567 | callback(); 568 | }); 569 | } 570 | writable.on('error', function (err) { 571 | hadError = true; 572 | error(err); 573 | }); 574 | writable.on('close', function () { 575 | if (hadError) return; 576 | swap(); 577 | }); 578 | 579 | resume(); 580 | } 581 | 582 | function swap() { 583 | fs.rename(tempPath, resolvedPath, function (err) { 584 | if (err) return error(err); 585 | callback(); 586 | }); 587 | } 588 | } 589 | 590 | function mkdirP(path, options, callback) { 591 | resolvePath(path, { checkSymlinks: false}, function(err, dir) { 592 | if (err) return callback(err); 593 | 594 | exists(dir, function(exists) { 595 | if (exists) return callback(null, {}); 596 | execFile("mkdir", { args: ["-p", dir] }, function(err) { 597 | if (err && err.message.indexOf("exists") > -1) 598 | callback({"code": "EEXIST", "message": err.message}); 599 | else 600 | callback(null, {}); 601 | }); 602 | }); 603 | }); 604 | } 605 | 606 | function mkdir(path, options, callback) { 607 | var meta = {}; 608 | 609 | if (options.parents) 610 | return mkdirP(path, options, callback); 611 | 612 | // Make sure the user has access to the parent directory and get the real path. 613 | resolvePath(dirname(path), function (err, dir) { 614 | if (err) return callback(err); 615 | path = join(dir, basename(path)); 616 | fs.mkdir(path, function (err) { 617 | if (err) return callback(err); 618 | callback(null, meta); 619 | }); 620 | }); 621 | } 622 | 623 | function rmfile(path, options, callback) { 624 | remove(path, fs.unlink, callback); 625 | } 626 | 627 | function rmdir(path, options, callback) { 628 | if (options.recursive) { 629 | remove(path, function(path, callback) { 630 | execFile("rm", {args: ["-rf", path]}, callback); 631 | }, callback); 632 | } 633 | else { 634 | remove(path, fs.rmdir, callback); 635 | } 636 | } 637 | 638 | function rename(path, options, callback) { 639 | var from, to; 640 | if (options.from) { 641 | from = options.from; to = path; 642 | } 643 | else if (options.to) { 644 | from = path; to = options.to; 645 | } 646 | else { 647 | return callback(new Error("Must specify either options.from or options.to")); 648 | } 649 | var meta = {}; 650 | // Get real path to source 651 | resolvePath(from, function (err, frompath) { 652 | if (err) return callback(err); 653 | // Get real path to target dir 654 | resolvePath(dirname(to), function (err, dir) { 655 | if (err) return callback(err); 656 | var topath = join(dir, basename(to)); 657 | 658 | exists(topath, function(exists){ 659 | if (options.overwrite || !exists) { 660 | // Rename the file 661 | fs.rename(frompath, topath, function (err) { 662 | if (err) return callback(err); 663 | 664 | // Rename metadata 665 | if (options.metadata !== false) { 666 | rename(WSMETAPATH + from, { 667 | to: WSMETAPATH + to, 668 | metadata: false 669 | }, function(err){ 670 | callback(null, meta); 671 | }); 672 | } 673 | }); 674 | } 675 | else { 676 | var err = new Error("File already exists."); 677 | err.code = "EEXIST"; 678 | callback(err); 679 | } 680 | }); 681 | }); 682 | }); 683 | } 684 | 685 | function copy(path, options, callback) { 686 | var from, to; 687 | if (options.from) { 688 | from = options.from; to = path; 689 | } 690 | else if (options.to) { 691 | from = path; to = options.to; 692 | } 693 | else { 694 | return callback(new Error("Must specify either options.from or options.to")); 695 | } 696 | 697 | if (!options.overwrite) { 698 | resolvePath(to, function(err, path){ 699 | if (err) { 700 | if (err.code == "ENOENT") 701 | return innerCopy(from, to); 702 | 703 | return callback(err); 704 | } 705 | 706 | fs.stat(path, function(err, stat){ 707 | if (!err && stat && !stat.err) { 708 | // TODO: this logic should be pushed into the application code 709 | var path = to.replace(/(?:\.([\d+]))?(\.[^\.]*)?$/, function(m, d, e){ 710 | return "." + (parseInt(d, 10)+1 || 1) + (e ? e : ""); 711 | }); 712 | 713 | copy(from, { 714 | to : path, 715 | overwrite : false, 716 | recursive : options.recursive 717 | }, callback); 718 | } 719 | else { 720 | innerCopy(from, to); 721 | } 722 | }); 723 | }); 724 | } 725 | else { 726 | innerCopy(from, to); 727 | } 728 | 729 | function innerCopy(from, to) { 730 | if (options.recursive) { 731 | resolvePath(from, function(err, rFrom){ 732 | resolvePath(to, function(err, rTo){ 733 | spawn("cp", { 734 | args: [ "-a", rFrom, rTo ], 735 | stdoutEncoding : "utf8", 736 | stderrEncoding : "utf8", 737 | stdinEncoding : "utf8" 738 | }, function(err, child){ 739 | if (err) return callback(err); 740 | 741 | var proc = child.process; 742 | var hasError; 743 | 744 | proc.stderr.on("data", function(d){ 745 | if (d) { 746 | hasError = true; 747 | callback(new Error(d)); 748 | } 749 | }); 750 | proc.stdout.on("end", function() { 751 | if (!hasError) 752 | callback(null, { to: to, meta: null }); 753 | }); 754 | }); 755 | }); 756 | }); 757 | } 758 | else { 759 | readfile(from, {}, function (err, meta) { 760 | if (err) return callback(err); 761 | mkfile(to, {stream: meta.stream}, function (err, meta) { 762 | callback(err, { 763 | to: to, 764 | meta: meta 765 | }); 766 | }); 767 | }); 768 | } 769 | } 770 | } 771 | 772 | function symlink(path, options, callback) { 773 | if (!options.target) return callback(new Error("options.target is required")); 774 | var meta = {}; 775 | // Get real path to target dir 776 | resolvePath(dirname(path), function (err, dir) { 777 | if (err) return callback(err); 778 | path = join(dir, basename(path)); 779 | 780 | resolvePath(options.target, function (err, target) { 781 | if (err) return callback(err); 782 | fs.symlink(target, path, function (err) { 783 | if (err) return callback(err); 784 | callback(null, meta); 785 | }); 786 | }); 787 | }); 788 | } 789 | 790 | function WatcherWrapper(path, options){ 791 | var listeners = []; 792 | var persistent = options.persistent; 793 | var watcher; 794 | 795 | function watch(){ 796 | if (options.file) { 797 | watcher = fs.watchFile(path, { persistent: false }, function () {}); 798 | watcher.close = function () { fs.unwatchFile(path); }; 799 | } 800 | else { 801 | watcher = fs.watch(path, { persistent: false }, function () {}); 802 | } 803 | 804 | watcher.on("change", listen); 805 | } 806 | 807 | function listen(event, filename){ 808 | listeners.forEach(function(fn){ 809 | fn(event, filename); 810 | }); 811 | 812 | if (persistent !== false) { 813 | // This timeout fixes an eternal loop that can occur with watchers 814 | setTimeout(function(){ 815 | try{ 816 | watcher.close(); 817 | watch(); 818 | } catch(e) { } 819 | }); 820 | } 821 | } 822 | 823 | this.close = function(){ 824 | listeners = []; 825 | watcher.removeListener("change", listen); 826 | watcher.close(); 827 | }; 828 | 829 | this.on = function(name, fn){ 830 | if (name != "change") 831 | watcher.on.apply(watcher, arguments); 832 | else { 833 | listeners.push(fn); 834 | } 835 | }; 836 | 837 | this.removeListener = function(name, fn){ 838 | if (name != "change") 839 | watcher.removeListener.apply(watcher, arguments); 840 | else { 841 | listeners.splice(listeners.indexOf(fn), 1); 842 | } 843 | }; 844 | 845 | this.removeAllListeners = function() { 846 | listeners = []; 847 | watcher.removeAllListeners(); 848 | }; 849 | 850 | watch(); 851 | } 852 | 853 | function watch(path, options, callback) { 854 | var meta = {}; 855 | resolvePath(path, function (err, path) { 856 | if (err) return callback(err); 857 | 858 | try { 859 | meta.watcher = new WatcherWrapper(path, options); 860 | } catch (e) { 861 | return callback(e); 862 | } 863 | 864 | callback(null, meta); 865 | }); 866 | } 867 | 868 | function connect(port, options, callback) { 869 | var retries = options.hasOwnProperty('retries') ? options.retries : 5; 870 | var retryDelay = options.hasOwnProperty('retryDelay') ? options.retryDelay : 50; 871 | tryConnect(); 872 | function tryConnect() { 873 | var socket = net.connect(port, process.env.OPENSHIFT_DIY_IP || "localhost", function () { 874 | if (options.hasOwnProperty('encoding')) { 875 | socket.setEncoding(options.encoding); 876 | } 877 | callback(null, {stream:socket}); 878 | }); 879 | socket.once("error", function (err) { 880 | if (err.code === "ECONNREFUSED" && retries) { 881 | setTimeout(tryConnect, retryDelay); 882 | retries--; 883 | retryDelay *= 2; 884 | return; 885 | } 886 | return callback(err); 887 | }); 888 | } 889 | } 890 | 891 | function spawn(executablePath, options, callback) { 892 | var args = options.args || []; 893 | 894 | if (options.hasOwnProperty('env')) { 895 | options.env.__proto__ = fsOptions.defaultEnv; 896 | } else { 897 | options.env = fsOptions.defaultEnv; 898 | } 899 | if (options.cwd && options.cwd.charAt(0) == "~") 900 | options.cwd = options.env.HOME + options.cwd.substr(1); 901 | 902 | resolvePath(executablePath, { 903 | nocheck : 1, 904 | alreadyRooted : true 905 | }, function(err, path){ 906 | if (err) return callback(err); 907 | 908 | var child; 909 | try { 910 | child = childProcess.spawn(path, args, options); 911 | } catch (err) { 912 | return callback(err); 913 | } 914 | if (options.resumeStdin) child.stdin.resume(); 915 | if (options.hasOwnProperty('stdoutEncoding')) { 916 | child.stdout.setEncoding(options.stdoutEncoding); 917 | } 918 | if (options.hasOwnProperty('stderrEncoding')) { 919 | child.stderr.setEncoding(options.stderrEncoding); 920 | } 921 | 922 | // node 0.10.x emits error events if the file does not exist 923 | child.on("error", function(err) { 924 | child.emit("exit", 127); 925 | }); 926 | 927 | callback(null, { 928 | process: child 929 | }); 930 | }); 931 | } 932 | 933 | function ptyspawn(executablePath, options, callback) { 934 | var args = options.args || []; 935 | delete options.args; 936 | 937 | if (options.hasOwnProperty('env')) { 938 | options.env.__proto__ = fsOptions.defaultEnv; 939 | } else { 940 | options.env = fsOptions.defaultEnv; 941 | } 942 | 943 | // Pty is only reading from the object itself; 944 | var env = {}; 945 | for (var prop in options.env) { 946 | if (prop == "TMUX") continue; 947 | env[prop] = options.env[prop]; 948 | } 949 | options.env = env; 950 | if (options.cwd && options.cwd.charAt(0) == "~") 951 | options.cwd = env.HOME + options.cwd.substr(1); 952 | 953 | resolvePath(executablePath, { 954 | nocheck : 1, 955 | alreadyRooted : true 956 | }, function(err, path){ 957 | if (err) return callback(err); 958 | 959 | var proc; 960 | try { 961 | proc = pty.spawn(path, args, options); 962 | proc.on("error", function(){ 963 | // Prevent PTY from throwing an error; 964 | // I don't know how to test and the src is funky because 965 | // it tests for .length < 2. Who is setting the other event? 966 | }); 967 | } catch (err) { 968 | return callback(err); 969 | } 970 | 971 | callback(null, { 972 | pty: proc 973 | }); 974 | }); 975 | } 976 | 977 | function execFile(executablePath, options, callback) { 978 | if (options.hasOwnProperty('env')) { 979 | options.env.__proto__ = fsOptions.defaultEnv; 980 | } else { 981 | options.env = fsOptions.defaultEnv; 982 | } 983 | if (options.cwd && options.cwd.charAt(0) == "~") 984 | options.cwd = options.env.HOME + options.cwd.substr(1); 985 | 986 | resolvePath(executablePath, { 987 | nocheck : 1, 988 | alreadyRooted : true 989 | }, function(err, path){ 990 | if (err) return callback(err); 991 | 992 | childProcess.execFile(path, options.args || [], 993 | options, function (err, stdout, stderr) { 994 | if (err) { 995 | err.stderr = stderr; 996 | err.stdout = stdout; 997 | return callback(err); 998 | } 999 | 1000 | callback(null, { 1001 | stdout: stdout, 1002 | stderr: stderr 1003 | }); 1004 | }); 1005 | }); 1006 | } 1007 | 1008 | function on(name, handler, callback) { 1009 | if (!handlers[name]) handlers[name] = []; 1010 | handlers[name].push(handler); 1011 | callback && callback(); 1012 | } 1013 | 1014 | function off(name, handler, callback) { 1015 | var list = handlers[name]; 1016 | if (list) { 1017 | var index = list.indexOf(handler); 1018 | if (index >= 0) { 1019 | list.splice(index, 1); 1020 | } 1021 | } 1022 | callback && callback(); 1023 | } 1024 | 1025 | function emit(name, value, callback) { 1026 | var list = handlers[name]; 1027 | if (list) { 1028 | for (var i = 0, l = list.length; i < l; i++) { 1029 | list[i](value); 1030 | } 1031 | } 1032 | callback && callback(); 1033 | } 1034 | 1035 | function extend(name, options, callback) { 1036 | 1037 | var meta = {}; 1038 | // Pull from cache if it's already loaded. 1039 | if (!options.redefine && apis.hasOwnProperty(name)) { 1040 | var err = new Error("EEXIST: Extension API already defined for " + name); 1041 | err.code = "EEXIST"; 1042 | return callback(err); 1043 | } 1044 | 1045 | var fn; 1046 | 1047 | // The user can pass in a path to a file to require 1048 | if (options.file) { 1049 | try { fn = require(options.file); } 1050 | catch (err) { return callback(err); } 1051 | fn(vfs, onEvaluate); 1052 | } 1053 | 1054 | // User can pass in code as a pre-buffered string 1055 | else if (options.code) { 1056 | try { fn = evaluate(options.code); } 1057 | catch (err) { return callback(err); } 1058 | fn(vfs, onEvaluate); 1059 | } 1060 | 1061 | // Or they can provide a readable stream 1062 | else if (options.stream) { 1063 | consumeStream(options.stream, function (err, code) { 1064 | if (err) return callback(err); 1065 | var fn; 1066 | try { 1067 | fn = evaluate(code); 1068 | } catch(err) { 1069 | return callback(err); 1070 | } 1071 | fn(vfs, onEvaluate); 1072 | }); 1073 | } 1074 | 1075 | else { 1076 | return callback(new Error("must provide `file`, `code`, or `stream` when cache is empty for " + name)); 1077 | } 1078 | 1079 | function onEvaluate(err, exports) { 1080 | if (err) { 1081 | return callback(err); 1082 | } 1083 | exports.names = Object.keys(exports); 1084 | exports.name = name; 1085 | apis[name] = exports; 1086 | meta.api = exports; 1087 | callback(null, meta); 1088 | } 1089 | 1090 | } 1091 | 1092 | function unextend(name, options, callback) { 1093 | delete apis[name]; 1094 | callback(null, {}); 1095 | } 1096 | 1097 | function use(name, options, callback) { 1098 | var api = apis[name]; 1099 | if (!api) { 1100 | var err = new Error("ENOENT: There is no API extension named " + name); 1101 | err.code = "ENOENT"; 1102 | return callback(err); 1103 | } 1104 | callback(null, {api:api}); 1105 | } 1106 | 1107 | //////////////////////////////////////////////////////////////////////////////// 1108 | 1109 | return vfs; 1110 | 1111 | }; 1112 | 1113 | // Consume all data in a readable stream and call callback with full buffer. 1114 | function consumeStream(stream, callback) { 1115 | var chunks = []; 1116 | stream.on("data", onData); 1117 | stream.on("end", onEnd); 1118 | stream.on("error", onError); 1119 | function onData(chunk) { 1120 | chunks.push(chunk); 1121 | } 1122 | function onEnd() { 1123 | cleanup(); 1124 | callback(null, chunks.join("")); 1125 | } 1126 | function onError(err) { 1127 | cleanup(); 1128 | callback(err); 1129 | } 1130 | function cleanup() { 1131 | stream.removeListener("data", onData); 1132 | stream.removeListener("end", onEnd); 1133 | stream.removeListener("error", onError); 1134 | } 1135 | } 1136 | 1137 | // node-style eval 1138 | function evaluate(code) { 1139 | var exports = {}; 1140 | var module = { exports: exports }; 1141 | vm.runInNewContext(code, { 1142 | require: require, 1143 | exports: exports, 1144 | module: module, 1145 | console: console, 1146 | global: global, 1147 | process: process, 1148 | Buffer: Buffer, 1149 | setTimeout: setTimeout, 1150 | clearTimeout: clearTimeout, 1151 | setInterval: setInterval, 1152 | clearInterval: clearInterval 1153 | }, "dynamic-" + Date.now().toString(36), true); 1154 | return module.exports; 1155 | } 1156 | 1157 | // Calculate a proper etag from a nodefs stat object 1158 | function calcEtag(stat) { 1159 | return (stat.isFile() ? '': 'W/') + '"' + (stat.ino || 0).toString(36) + "-" + stat.size.toString(36) + "-" + stat.mtime.valueOf().toString(36) + '"'; 1160 | } 1161 | 1162 | function uid(length) { 1163 | return (crypto 1164 | .randomBytes(length) 1165 | .toString("base64") 1166 | .slice(0, length) 1167 | .replace(/[+\/]+/g, "") 1168 | ); 1169 | } 1170 | 1171 | function tmpFile(baseDir, prefix, suffix) { 1172 | return join(baseDir, [prefix || "", uid(20), suffix || ""].join("")); 1173 | } 1174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ajax.org B.V. ", 3 | "contributors": [ 4 | { "name": "Tim Caswell", "email": "tim@c9.io>" }, 5 | { "name": "Fabian Jakobs", "email": "fabian@c9.io" } 6 | ], 7 | "name": "vfs-local", 8 | "description": "A vfs implementation that works on the local filesystem.", 9 | "version": "0.3.14", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/c9/vfs-local.git" 13 | }, 14 | "main": "localfs.js", 15 | "dependencies": { 16 | "simple-mime": "~0.0.7", 17 | "pty.js": "0.3.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "~1.2.0", 21 | "mocha": "~1.12.0", 22 | "vfs-lint": "~0.0.0" 23 | }, 24 | "scripts": { 25 | "test": "git clean -df test/mock; mocha -R spec" 26 | }, 27 | "licenses" : [{ 28 | "type" : "MIT" 29 | }], 30 | "engines": { 31 | "node": ">=0.6.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/math.js: -------------------------------------------------------------------------------- 1 | module.exports = function (vfs, register) { 2 | register(null, { 3 | add: function (a, b, callback) { 4 | callback(null, a + b); 5 | }, 6 | multiply: function (a, b, callback) { 7 | callback(null, a * b); 8 | } 9 | }); 10 | }; -------------------------------------------------------------------------------- /test/mock/dir/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9/vfs-local/9569c00e2fca4a4e2c2c9c0227e440322cd74882/test/mock/dir/smile.png -------------------------------------------------------------------------------- /test/mock/dir/stuff.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c9/vfs-local/9569c00e2fca4a4e2c2c9c0227e440322cd74882/test/mock/dir/stuff.json -------------------------------------------------------------------------------- /test/mock/dirLink: -------------------------------------------------------------------------------- 1 | dir -------------------------------------------------------------------------------- /test/mock/file.txt: -------------------------------------------------------------------------------- 1 | This is a simple file! 2 | -------------------------------------------------------------------------------- /test/mock/fileLink.txt: -------------------------------------------------------------------------------- 1 | file.txt -------------------------------------------------------------------------------- /test/mock/listing.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name":"listing.json","access":6,"size":921,"mtime":1342205386000,"mime":"application/json","href":"http://localhost:44988/listing.json"}, 3 | {"name":"dir","access":7,"size":4096,"mtime":1342199601000,"mime":"inode/directory","href":"http://localhost:44988/dir/"}, 4 | {"name":"file.txt","access":6,"size":0,"mtime":1342199547000,"mime":"text/plain","href":"http://localhost:44988/file.txt"}, 5 | {"name":"fileLink.txt","access":7,"size":8,"mtime":1342199557000,"mime":"inode/symlink","link":"file.txt","linkStat":{"name":"file.txt","access":6,"size":0,"mtime":1342199547000,"mime":"text/plain","fullPath":"/file.txt"},"href":"http://localhost:44988/fileLink.txt"}, 6 | {"name":"dirLink","access":7,"size":3,"mtime":1342199574000,"mime":"inode/symlink","link":"dir","linkStat":{"name":"dir","access":7,"size":4096,"mtime":1342199601000,"mime":"inode/directory","fullPath":"/dir"},"href":"http://localhost:44988/dirLink/"} 7 | ] -------------------------------------------------------------------------------- /test/node_modules/vfs-local: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /test/test-local.js: -------------------------------------------------------------------------------- 1 | /*global describe:false, it:false */ 2 | 3 | "use server"; 4 | "use mocha"; 5 | 6 | var expect = require('chai').expect; 7 | 8 | describe('vfs-local', function () { 9 | 10 | var root = __dirname + "/mock/"; 11 | var base = root.substr(0, root.length - 1); 12 | 13 | var vfs = require('vfs-lint')(require("vfs-local")({ 14 | root: root, 15 | testing: true, 16 | defaultEnv: { CUSTOM: 43 }, 17 | checkSymlinks: true 18 | })); 19 | 20 | var vfsLoose = require('vfs-lint')(require("vfs-local")({ 21 | root: root 22 | })); 23 | 24 | var fs = require('fs'); 25 | if (!fs.existsSync) fs.existsSync = require('path').existsSync; 26 | 27 | describe('vfs.resolve()', function () { 28 | it('should prepend root when resolving virtual paths', function (done) { 29 | var vpath = "/dir/stuff.json"; 30 | vfs.resolve(vpath, {}, function (err, meta) { 31 | if (err) throw err; 32 | expect(meta).property("path").equals(base + vpath); 33 | done(); 34 | }); 35 | }); 36 | it('should reject paths that resolve outside the root', function (done) { 37 | vfs.resolve("/../test-local.js", {}, function (err, meta) { 38 | expect(err).property("code").equals("EACCESS"); 39 | done(); 40 | }); 41 | }); 42 | it('should not prepend when already rooted', function (done) { 43 | var path = base + "/file.txt"; 44 | vfs.resolve(path, { alreadyRooted: true }, function (err, meta) { 45 | if (err) throw err; 46 | expect(meta).property("path").equal(path); 47 | done(); 48 | }); 49 | }); 50 | it('should error with ENOENT when the path is invalid', function (done) { 51 | vfs.resolve("/notexists.txt", {}, function (err, meta) { 52 | expect(err).property("code").equals("ENOENT"); 53 | done(); 54 | }); 55 | }); 56 | it('should not check fs when checkSymlinks is off', function (done) { 57 | var vpath = "/badpath.txt"; 58 | vfsLoose.resolve(vpath, {}, function (err, meta) { 59 | if (err) throw err; 60 | expect(meta).property("path").equal(base + vpath); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('vfs.stat()', function () { 67 | it('should return stat info for the text file', function (done) { 68 | vfs.stat("/file.txt", {}, function (err, stat) { 69 | if (err) throw err; 70 | expect(stat).property("name").equal("file.txt"); 71 | expect(stat).property("size").equal(23); 72 | expect(stat).property("mime").equal("text/plain"); 73 | done(); 74 | }); 75 | }); 76 | it("should error with ENOENT when the file doesn't exist", function (done) { 77 | vfs.stat("/badfile.json", {}, function (err, stat) { 78 | expect(err).property("code").equal("ENOENT"); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('vfs.readfile()', function () { 85 | it("should read the text file", function (done) { 86 | vfs.readfile("/file.txt", {}, function (err, meta) { 87 | if (err) throw err; 88 | expect(meta).property("mime").equals("text/plain"); 89 | expect(meta).property("size").equals(23); 90 | expect(meta).property("etag"); 91 | expect(meta).property("stream").property("readable"); 92 | var stream = meta.stream; 93 | var chunks = []; 94 | var length = 0; 95 | stream.on("data", function (chunk) { 96 | chunks.push(chunk); 97 | length += chunk.length; 98 | }); 99 | stream.on("end", function () { 100 | expect(length).equal(23); 101 | var body = chunks.join(""); 102 | expect(body).equal("This is a simple file!\n"); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | it("should error with ENOENT on missing files", function (done) { 108 | vfs.readfile("/badfile.json", {}, function (err, meta) { 109 | expect(err).property("code").equal("ENOENT"); 110 | done(); 111 | }); 112 | }); 113 | it("should error with EISDIR on directories", function (done) { 114 | vfs.readfile("/", {}, function (err, meta) { 115 | expect(err).property("code").equal("EISDIR"); 116 | done(); 117 | }); 118 | }); 119 | it("should support head requests", function (done) { 120 | vfs.readfile("/file.txt", {head:true}, function (err, meta) { 121 | if (err) throw err; 122 | expect(meta).property("mime").equal("text/plain"); 123 | expect(meta).property("size").equal(23); 124 | expect(meta).property("mime").ok; 125 | expect(meta.stream).not.ok; 126 | done(); 127 | }); 128 | }); 129 | it("should support 304 via etags", function (done) { 130 | vfs.readfile("/file.txt", {head:true}, function (err, meta) { 131 | if (err) throw err; 132 | expect(meta).property("etag").ok 133 | var etag = meta.etag; 134 | vfs.readfile("/file.txt", {etag:etag}, function (err, meta) { 135 | if (err) throw err; 136 | expect(meta).property("mime").equal("text/plain"); 137 | expect(meta).property("size").equal(23); 138 | expect(meta).property("notModified").ok; 139 | expect(meta.stream).not.ok; 140 | done(); 141 | }); 142 | }); 143 | }); 144 | it("should support range requests", function (done) { 145 | vfs.readfile("/file.txt", {range:{start:1,end:3}}, function (err, meta) { 146 | if (err) throw err; 147 | expect(meta).property("mime").equal("text/plain"); 148 | expect(meta).property("size").equal(3); 149 | expect(meta).property("etag").ok; 150 | expect(meta).property("partialContent").deep.equal({ start: 1, end: 3, size: 23 }); 151 | expect(meta).property("stream").ok; 152 | var stream = meta.stream; 153 | var chunks = []; 154 | stream.on("data", function (chunk) { 155 | chunks.push(chunk); 156 | }); 157 | stream.on("end", function () { 158 | var data = chunks.join(""); 159 | expect(data).equal("his"); 160 | done(); 161 | }); 162 | }); 163 | }); 164 | it("should support getting the last 10 bytes", function (done) { 165 | vfs.readfile("/file.txt", {range:{end:10},head:true}, function (err, meta) { 166 | if (err) throw err; 167 | expect(meta).property("size").equal(10); 168 | expect(meta).property("etag").ok; 169 | expect(meta).property("partialContent").deep.equal({ start: 13, end: 22, size: 23 }); 170 | done(); 171 | }); 172 | }); 173 | it("should get rangeNotSatisfiable if start and end are both omitted", function (done) { 174 | vfs.readfile("/file.txt", {range:{}}, function (err, meta) { 175 | if (err) throw err; 176 | expect(meta).property("rangeNotSatisfiable"); 177 | done(); 178 | }); 179 | }); 180 | it("should get rangeNotSatisfiable if start is after end", function (done) { 181 | vfs.readfile("/file.txt", {range:{start:5,end:4}}, function (err, meta) { 182 | if (err) throw err; 183 | expect(meta).property("rangeNotSatisfiable"); 184 | done(); 185 | }); 186 | }); 187 | }); 188 | 189 | describe('vfs.readdir()', function () { 190 | it("should read the directory", function (done) { 191 | vfs.readdir("/", {}, function (err, meta) { 192 | if (err) throw err; 193 | expect(meta).property("etag"); 194 | expect(meta).property("stream").property("readable"); 195 | var stream = meta.stream; 196 | var parts = []; 197 | stream.on("data", function (part) { 198 | parts.push(part); 199 | }); 200 | stream.on("end", function () { 201 | expect(parts).length(5); 202 | done(); 203 | }); 204 | }); 205 | }); 206 | it("should error with ENOENT when the folder doesn't exist", function (done) { 207 | vfs.readdir("/fake", {}, function (err, meta) { 208 | expect(err).property("code").equal("ENOENT"); 209 | done(); 210 | }); 211 | }); 212 | it("should error with ENOTDIR when the path is a file", function (done) { 213 | vfs.readdir("/file.txt", {}, function (err, meta) { 214 | expect(err).property("code").equal("ENOTDIR"); 215 | done(); 216 | }); 217 | }); 218 | it("should support head requests", function (done) { 219 | vfs.readdir("/", {head:true}, function (err, meta) { 220 | if (err) throw err; 221 | expect(meta).property("etag").ok; 222 | expect(meta.stream).not.ok; 223 | done(); 224 | }); 225 | }); 226 | it("should support 304 via etags", function (done) { 227 | vfs.readdir("/", {head:true}, function (err, meta) { 228 | if (err) throw err; 229 | expect(meta).property("etag").ok; 230 | var etag = meta.etag; 231 | vfs.readdir("/", {etag:etag}, function (err, meta) { 232 | if (err) throw err; 233 | expect(meta).property("notModified").ok; 234 | expect(meta.stream).not.ok; 235 | done(); 236 | }); 237 | }); 238 | }); 239 | }); 240 | 241 | describe('vfs.mkfile()', function () { 242 | it("should create a file using using readable in options", function (done) { 243 | var stream = fs.createReadStream(__filename); 244 | var vpath = "/test.js"; 245 | // Make sure the file doesn't exist. 246 | expect(fs.existsSync(base + vpath)).not.ok; 247 | vfs.mkfile(vpath, { stream: stream }, function (err, meta) { 248 | if (err) { 249 | fs.unlinkSync(base + vpath); 250 | return done(err); 251 | } 252 | var actual = fs.readFileSync(base + vpath, "utf8"); 253 | var original = fs.readFileSync(__filename, "utf8"); 254 | fs.unlinkSync(base + vpath); 255 | expect(actual).equal(original); 256 | done(); 257 | }); 258 | }); 259 | it("should create a file using writable in callback", function (done) { 260 | var vpath = "/test.js"; 261 | // Make sure the file doesn't exist. 262 | expect(fs.existsSync(base + vpath)).not.ok; 263 | vfs.mkfile(vpath, {}, function (err, meta) { 264 | if (err) { 265 | fs.unlinkSync(base + vpath); 266 | return done(err); 267 | } 268 | expect(meta).property("stream").property("writable").ok; 269 | var writable = meta.stream; 270 | var readable = fs.createReadStream(__filename); 271 | readable.pipe(writable); 272 | writable.on("close", function () { 273 | var actual = fs.readFileSync(base + vpath, "utf8"); 274 | var original = fs.readFileSync(__filename, "utf8"); 275 | fs.unlinkSync(base + vpath); 276 | expect(actual).equal(original); 277 | done(); 278 | }); 279 | }); 280 | }); 281 | it("should update an existing file using readble in options", function (done) { 282 | var vpath = "/changeme.txt"; 283 | var stream = fs.createReadStream(__filename); 284 | fs.writeFileSync(base + vpath, "Original Content\n"); 285 | vfs.mkfile(vpath, {stream: stream}, function (err, meta) { 286 | if (err) { 287 | fs.unlinkSync(base + vpath); 288 | return done(err); 289 | } 290 | var actual = fs.readFileSync(base + vpath, "utf8"); 291 | var original = fs.readFileSync(__filename, "utf8"); 292 | fs.unlinkSync(base + vpath); 293 | expect(actual).equal(original); 294 | done(); 295 | }); 296 | }), 297 | it("should update an existing file using writable in callback", function (done) { 298 | var vpath = "/changeme.txt"; 299 | fs.writeFileSync(base + vpath, "Original Content\n"); 300 | vfs.mkfile(vpath, {}, function (err, meta) { 301 | if (err) { 302 | fs.unlinkSync(base + vpath); 303 | return done(err); 304 | } 305 | expect(meta).property("stream").property("writable").ok; 306 | var writable = meta.stream; 307 | var readable = fs.createReadStream(__filename); 308 | readable.pipe(writable); 309 | writable.on("saved", function () { 310 | var actual = fs.readFileSync(base + vpath, "utf8"); 311 | var original = fs.readFileSync(__filename, "utf8"); 312 | fs.unlinkSync(base + vpath); 313 | expect(actual).equal(original); 314 | done(); 315 | }); 316 | }); 317 | }); 318 | it("should not write to a read only file", function(done) { 319 | var vpath = "/readonly.txt"; 320 | fs.writeFileSync(base + vpath, "read only"); 321 | fs.chmodSync(base + vpath, "0444"); 322 | vfs.mkfile(vpath, {}, function(err, meta) { 323 | expect(err).property("code").equal("EACCES"); 324 | fs.unlinkSync(base + vpath); 325 | done(); 326 | }); 327 | }); 328 | it("should create intermediate directories", function(done) { 329 | vfs.execFile("rm", {args: ["-rf", root + "/nested"]}, function() { 330 | vfs.mkfile("/nested/dir/file.txt", { parents: true }, function(err, meta) { 331 | meta.stream.write("juhu"); 332 | meta.stream.end(); 333 | 334 | meta.stream.on("saved", function() { 335 | var contents = fs.readFileSync(root + "nested/dir/file.txt", "utf8"); 336 | expect(contents).equal("juhu"); 337 | vfs.execFile("rm", {args: ["-rf", root + "/nested"]}, done); 338 | }); 339 | }); 340 | }); 341 | }); 342 | }); 343 | 344 | describe('vfs.mkdir()', function () { 345 | it("should create a directory", function (done) { 346 | var vpath = "/newdir"; 347 | // Make sure it doesn't exist yet 348 | expect(fs.existsSync(base + vpath)).not.ok; 349 | vfs.mkdir(vpath, {}, function (err, meta) { 350 | if (err) { 351 | fs.rmdirSync(base + vpath); 352 | return done(err); 353 | } 354 | expect(fs.existsSync(base + vpath)).ok; 355 | fs.rmdirSync(base + vpath); 356 | done(); 357 | }); 358 | }); 359 | it("should error with EEXIST when the directory already exists", function (done) { 360 | vfs.mkdir("/dir", {}, function (err, meta) { 361 | expect(err).property("code").equal("EEXIST"); 362 | done(); 363 | }); 364 | }); 365 | it("should error with EEXIST when a file already exists at the path", function (done) { 366 | vfs.mkdir("/file.txt", {}, function (err, meta) { 367 | expect(err).property("code").equal("EEXIST"); 368 | done(); 369 | }); 370 | }); 371 | }); 372 | 373 | describe('vfs.rmfile()', function () { 374 | it("should delete a file", function (done) { 375 | var vpath = "/deleteme.txt"; 376 | fs.writeFileSync(base + vpath, "DELETE ME!\n"); 377 | expect(fs.existsSync(base + vpath)).ok; 378 | vfs.rmfile(vpath, {}, function (err, meta) { 379 | if (err) throw err; 380 | expect(fs.existsSync(base + vpath)).not.ok; 381 | done(); 382 | }); 383 | }); 384 | it("should error with ENOENT if the file doesn't exist", function (done) { 385 | var vpath = "/badname.txt"; 386 | expect(fs.existsSync(base + vpath)).not.ok; 387 | vfs.rmfile(vpath, {}, function (err, meta) { 388 | expect(err).property("code").equal("ENOENT"); 389 | done(); 390 | }); 391 | }); 392 | it("should error with EISDIR if the path is a directory", function (done) { 393 | var vpath = "/dir"; 394 | expect(fs.existsSync(base + vpath)).ok; 395 | vfs.rmfile(vpath, {}, function (err, meta) { 396 | expect(err).property("code").equal("EISDIR"); 397 | done(); 398 | }); 399 | }); 400 | }); 401 | 402 | describe('vfs.rmdir()', function () { 403 | it("should delete a directory", function (done) { 404 | var vpath = "/newdir"; 405 | fs.mkdirSync(base + vpath); 406 | expect(fs.existsSync(base + vpath)).ok; 407 | vfs.rmdir(vpath, {}, function (err, meta) { 408 | if (err) throw err; 409 | expect(fs.existsSync(base + vpath)).not.ok; 410 | done(); 411 | }); 412 | }); 413 | it("should error with ENOENT if the directory doesn't exist", function (done) { 414 | var vpath = "/baddir"; 415 | expect(fs.existsSync(base + vpath)).not.ok; 416 | vfs.rmdir(vpath, {}, function (err, meta) { 417 | expect(err).property("code").equal("ENOENT"); 418 | done(); 419 | }); 420 | }); 421 | it("should error with ENOTDIR if the path is a file", function (done) { 422 | var vpath = "/file.txt"; 423 | expect(fs.existsSync(base + vpath)).ok; 424 | vfs.rmdir(vpath, {}, function (err, meta) { 425 | expect(err).property("code").equal("ENOTDIR"); 426 | done(); 427 | }); 428 | }); 429 | it("should do recursive deletes if options.recursive is set", function (done) { 430 | fs.mkdirSync(base + "/foo"); 431 | fs.writeFileSync(base + "/foo/bar.txt", "Hello"); 432 | expect(fs.existsSync(base + "/foo")).ok; 433 | expect(fs.existsSync(base + "/foo/bar.txt")).ok; 434 | vfs.rmdir("/foo", {recursive:true}, function (err, meta) { 435 | if (err) throw err; 436 | expect(fs.existsSync(base + "/foo/bar.txt")).not.ok; 437 | expect(fs.existsSync(base + "/foo")).not.ok; 438 | done(); 439 | }); 440 | }); 441 | }); 442 | 443 | describe('vfs.rename()', function () { 444 | it("should rename a file using options.to", function (done) { 445 | var before = "/start.txt"; 446 | var after = "/end.txt"; 447 | var text = "Move me please\n"; 448 | fs.writeFileSync(base + before, text); 449 | expect(fs.existsSync(base + before)).ok; 450 | expect(fs.existsSync(base + after)).not.ok; 451 | vfs.rename(before, {to: after}, function (err, meta) { 452 | if (err) throw err; 453 | expect(fs.existsSync(base + before)).not.ok; 454 | expect(fs.existsSync(base + after)).ok; 455 | expect(fs.readFileSync(base + after, "utf8")).equal(text); 456 | fs.unlinkSync(base + after); 457 | done(); 458 | }); 459 | }); 460 | it("should rename a file using options.from", function (done) { 461 | var before = "/start.txt"; 462 | var after = "/end.txt"; 463 | var text = "Move me please\n"; 464 | fs.writeFileSync(base + before, text); 465 | expect(fs.existsSync(base + before)).ok; 466 | expect(fs.existsSync(base + after)).not.ok; 467 | vfs.rename(after, {from: before}, function (err, meta) { 468 | if (err) throw err; 469 | expect(fs.existsSync(base + before)).not.ok; 470 | expect(fs.existsSync(base + after)).ok; 471 | expect(fs.readFileSync(base + after, "utf8")).equal(text); 472 | fs.unlinkSync(base + after); 473 | done(); 474 | }); 475 | }); 476 | it("should error with ENOENT if the source doesn't exist", function (done) { 477 | vfs.rename("/notexist", {to:"/newname"}, function (err, meta) { 478 | expect(err).property("code").equal("ENOENT"); 479 | done(); 480 | }); 481 | }); 482 | }); 483 | 484 | describe('vfs.copy()', function () { 485 | it("should copy a file using options.to", function (done) { 486 | var source = "/file.txt"; 487 | var target = "/copy.txt"; 488 | var text = fs.readFileSync(base + source, "utf8"); 489 | vfs.copy(source, {to: target}, function (err, meta) { 490 | if (err) throw err; 491 | expect(fs.existsSync(base + target)).ok; 492 | expect(fs.readFileSync(base + target, "utf8")).equal(text); 493 | fs.unlinkSync(base + target); 494 | done(); 495 | }); 496 | }); 497 | it("should copy a file using options.from", function (done) { 498 | var source = "/file.txt"; 499 | var target = "/copy.txt"; 500 | var text = fs.readFileSync(base + source, "utf8"); 501 | vfs.copy(target, {from: source}, function (err, meta) { 502 | if (err) throw err; 503 | expect(fs.existsSync(base + target)).ok; 504 | expect(fs.readFileSync(base + target, "utf8")).equal(text); 505 | fs.unlinkSync(base + target); 506 | done(); 507 | }); 508 | }); 509 | it("should error with ENOENT if the source doesn't exist", function (done) { 510 | vfs.copy("/badname.txt", {to:"/copy.txt"}, function (err, meta) { 511 | expect(err).property("code").equal("ENOENT"); 512 | done(); 513 | }); 514 | }); 515 | }); 516 | 517 | describe('vfs.symlink()', function () { 518 | it("should create a symlink", function (done) { 519 | var target = "file.txt"; 520 | var vpath = "/newlink.txt"; 521 | var text = fs.readFileSync(root + target, "utf8"); 522 | vfs.symlink(vpath, {target: target}, function (err, meta) { 523 | if (err) throw err; 524 | expect(fs.readFileSync(base + vpath, "utf8")).equal(text); 525 | fs.unlinkSync(base + vpath); 526 | done(); 527 | }); 528 | }); 529 | it("should error with ENOENT if the dire3ctory of the target file does not exists", function (done) { 530 | vfs.symlink("/file.txt", {target:"/this/is/crazy"}, function (err, meta) { 531 | expect(err).property("code").equal("ENOENT"); 532 | done(); 533 | }); 534 | }); 535 | it("should error with EEXIST if the file already exists", function (done) { 536 | var target = "/target.txt"; 537 | fs.writeFileSync(root + target, "Target"); 538 | vfs.symlink("/file.txt", {target: target}, function (err, meta) { 539 | fs.unlinkSync(base + target); 540 | expect(err).property("code").equal("EEXIST"); 541 | done(); 542 | }); 543 | }); 544 | }); 545 | 546 | describe('vfs.watch()', function () { 547 | it("should notice a directly watched file change (OS changing it)", function (done) { 548 | var vpath = "/newfile.txt"; 549 | expect(fs.existsSync(base + vpath)).not.ok; 550 | fs.writeFile(base + vpath, "Test", function(){ 551 | vfs.watch(vpath, {}, function (err, meta) { 552 | if (err) throw err; 553 | expect(meta).property("watcher").ok; 554 | var watcher = meta.watcher; 555 | watcher.on("change", function listen(event, filename) { 556 | // expect(event).equal("change"); 557 | expect(filename).equal(vpath.substr(1)); 558 | 559 | if (inner) { 560 | watcher.close(); 561 | done(); 562 | } 563 | }); 564 | 565 | var inner = false; 566 | fs.writeFile(base + vpath, "Change!", function(){ 567 | setTimeout(function(){ 568 | inner = true; 569 | fs.unlinkSync(base + vpath); 570 | }, 100); 571 | }) 572 | }); 573 | }); 574 | }); 575 | it("should notice a directly watched file change (change via VFS)", function (done) { 576 | var vpath = "/newfile.txt"; 577 | expect(fs.existsSync(base + vpath)).not.ok; 578 | fs.writeFile(base + vpath, "Test", function(){ 579 | vfs.watch(vpath, {}, function (err, meta) { 580 | if (err) throw err; 581 | expect(meta).property("watcher").ok; 582 | var watcher = meta.watcher; 583 | watcher.on("change", function listen(event, filename) { 584 | // expect(event).equal("change"); 585 | expect(filename).equal(vpath.substr(1)); 586 | 587 | if (inner) { 588 | watcher.close(); 589 | done(); 590 | } 591 | }); 592 | 593 | var inner = false; 594 | vfs.mkfile(vpath, {}, function(err, meta){ 595 | var stream = meta.stream; 596 | stream.write("Change!"); 597 | stream.end(); 598 | setTimeout(function(){ 599 | inner = true; 600 | fs.unlinkSync(base + vpath); 601 | }, 100); 602 | }); 603 | }); 604 | }); 605 | }); 606 | it("should notice a new file in a watched directory", function (done) { 607 | var vpath = "/newfile.txt"; 608 | expect(fs.existsSync(base + vpath)).not.ok; 609 | vfs.watch("/", {}, function (err, meta) { 610 | if (err) throw err; 611 | expect(meta).property("watcher").ok; 612 | var watcher = meta.watcher; 613 | watcher.on("change", function (event, filename) { 614 | watcher.close(); 615 | expect(event).ok; 616 | expect(filename).equal(vpath.substr(1)); 617 | fs.unlinkSync(base + vpath); 618 | done(); 619 | }); 620 | fs.writeFileSync(base + vpath, "newfile!"); 621 | }); 622 | }); 623 | it("should return error if path does not exist", function(done) { 624 | var vpath = "/newfile.txt"; 625 | expect(fs.existsSync(base + vpath)).not.ok; 626 | vfs.watch(base + vpath, {file: false}, function (err, meta) { 627 | expect(err).ok; 628 | done(); 629 | }); 630 | }); 631 | }); 632 | 633 | describe('vfs.connect()', function () { 634 | var net = require('net'); 635 | it("should connect to a tcp server and ping-pong", function (done) { 636 | var stream; 637 | var server = net.createServer(function (client) { 638 | client.setEncoding('utf8'); 639 | client.once("data", function (chunk) { 640 | expect(chunk).equal("ping"); 641 | stream.once("data", function (chunk) { 642 | expect(chunk).equal("pong"); 643 | client.end(); 644 | stream.end(); 645 | server.close(); 646 | done(); 647 | }); 648 | client.write("pong"); 649 | }); 650 | }); 651 | server.listen(function () { 652 | var port = server.address().port; 653 | vfs.connect(port, {encoding:"utf8"}, function (err, meta) { 654 | if (err) throw err; 655 | expect(meta).property("stream").ok; 656 | stream = meta.stream; 657 | stream.write("ping"); 658 | }); 659 | }); 660 | }); 661 | }); 662 | 663 | describe('vfs.spawn()', function () { 664 | it("should spawn a child process", function (done) { 665 | var args = ["-e", "process.stdin.pipe(process.stdout);try{process.stdin.resume()}catch(e){};"]; 666 | vfs.spawn(process.execPath, {args: args, stdoutEncoding: "utf8"}, function (err, meta) { 667 | if (err) throw err; 668 | expect(meta).property("process").ok; 669 | var child = meta.process; 670 | expect(child).property("stdout").ok; 671 | expect(child).property("stdin").ok; 672 | child.stdout.on("data", function (chunk) { 673 | expect(chunk).equal("echo me"); 674 | child.stdout.on("end", function () { 675 | done(); 676 | }); 677 | child.stdin.end(); 678 | }); 679 | child.stdin.write("echo me"); 680 | }); 681 | }); 682 | it("should have environment variables from process, fsOptions, and call", function (done) { 683 | process.env.PROCESS = 42; 684 | var args = ["-e", "console.log([process.env.PROCESS, process.env.CUSTOM, process.env.LOCAL].join(','))"]; 685 | vfs.spawn(process.execPath, {args:args, stdoutEncoding: "utf8", stderrEncoding: "utf8", env: {LOCAL:44}}, function (err, meta) { 686 | if (err) throw err; 687 | expect(meta).property("process").ok; 688 | var child = meta.process; 689 | var stdout = []; 690 | child.stdout.on("data", function (chunk) { 691 | stdout.push(chunk); 692 | }); 693 | child.stdout.on("end", function () { 694 | stdout = stdout.join(""); 695 | expect(stdout).equal("42,43,44\n"); 696 | done(); 697 | }); 698 | }); 699 | }); 700 | }); 701 | 702 | describe('vfs.execFile()', function () { 703 | it("should exec a child process", function (done) { 704 | var args = ["-p", "-e", "process.version"]; 705 | vfs.execFile(process.execPath, {args:args}, function (err, meta) { 706 | if (err) throw err; 707 | expect(meta).property("stdout").equal(process.version + "\n"); 708 | expect(meta).property("stderr").equal(""); 709 | done(); 710 | }); 711 | }); 712 | it("should have environment variables from process, fsOptions, and call", function (done) { 713 | process.env.PROCESS = 42; 714 | var args = ["-e", "console.log([process.env.PROCESS, process.env.CUSTOM, process.env.LOCAL].join(','))"]; 715 | vfs.execFile(process.execPath, {args:args, env: {LOCAL:44}}, function (err, meta) { 716 | if (err) throw err; 717 | expect(meta).property("stdout").equal("42,43,44\n"); 718 | done(); 719 | }); 720 | }); 721 | it("should return stdout and stderr on the error object", function(done) { 722 | var args = ["-e", "console.error('error'); console.log('out'); process.exit(1);"]; 723 | vfs.execFile(process.execPath, {args:args}, function (err, meta) { 724 | expect(err).property("stderr").equal("error\n"); 725 | expect(err).property("stdout").equal("out\n"); 726 | expect(err).property("code").equal(1); 727 | done(); 728 | }); 729 | }); 730 | }); 731 | 732 | describe('vfs.on(), vfs.off(), vfs.emit()', function () { 733 | it ("should register an event listener and catch an event", function (done) { 734 | vfs.on("myevent", onEvent, function (err) { 735 | if (err) throw err; 736 | vfs.emit("myevent", 42, function (err) { 737 | if (err) throw err; 738 | }); 739 | }); 740 | function onEvent(data) { 741 | expect(data).equal(42); 742 | vfs.off("myevent", onEvent, done); 743 | } 744 | }); 745 | it("should catch multiple events of the same type", function (done) { 746 | var times = 0; 747 | vfs.on("myevent", onEvent, function (err) { 748 | if (err) throw err; 749 | vfs.emit("myevent", 43, function (err) { 750 | if (err) throw err; 751 | }); 752 | vfs.emit("myevent", 43, function (err) { 753 | if (err) throw err; 754 | }); 755 | }); 756 | function onEvent(data) { 757 | expect(data).equal(43); 758 | if (++times === 2) { 759 | vfs.off("myevent", onEvent, done); 760 | } 761 | } 762 | }); 763 | it("should call multiple listeners for a single event", function (done) { 764 | var times = 0; 765 | vfs.on("myevent", onEvent1, function (err) { 766 | if (err) throw err; 767 | vfs.on("myevent", onEvent2, function (err) { 768 | if (err) throw err; 769 | vfs.emit("myevent", 44, function (err) { 770 | if (err) throw err; 771 | }); 772 | }); 773 | }); 774 | function onEvent1(data) { 775 | expect(data).equal(44); 776 | times++; 777 | } 778 | function onEvent2(data) { 779 | expect(data).equal(44); 780 | if (++times === 2) { 781 | vfs.off("myevent", onEvent1, function (err) { 782 | if (err) throw err; 783 | vfs.off("myevent", onEvent2, done); 784 | }); 785 | } 786 | } 787 | }); 788 | it("should stop listening after a handler is removed", function (done) { 789 | vfs.on("myevent", onEvent, function (err) { 790 | if (err) throw err; 791 | vfs.emit("myevent", 45, function (err) { 792 | if (err) throw err; 793 | vfs.off("myevent", onEvent, function (err) { 794 | if (err) throw err; 795 | vfs.emit("myevent", 46, done); 796 | }); 797 | }); 798 | }); 799 | function onEvent(data) { 800 | expect(data).equal(45); 801 | } 802 | }); 803 | }); 804 | 805 | describe('vfs.extend()', function () { 806 | it("should extend using a local file", function (done) { 807 | vfs.extend("math", {file: __dirname + "/math.js"}, function (err, meta) { 808 | if (err) throw err; 809 | expect(meta).property("api").ok; 810 | var api = meta.api; 811 | expect(api).property("add").a("function"); 812 | expect(api).property("multiply").a("function"); 813 | api.add(3, 4, function (err, result) { 814 | if (err) throw err; 815 | expect(result).equal(3 + 4); 816 | vfs.unextend("math", {}, done); 817 | }); 818 | }); 819 | }); 820 | it("should extend using a string", function (done) { 821 | var code = fs.readFileSync(__dirname + "/math.js", "utf8"); 822 | vfs.extend("math2", {code: code}, function (err, meta) { 823 | if (err) throw err; 824 | expect(meta).property("api").ok; 825 | var api = meta.api; 826 | expect(api).property("add").a("function"); 827 | expect(api).property("multiply").a("function"); 828 | api.add(3, 4, function (err, result) { 829 | if (err) throw err; 830 | expect(result).equal(3 + 4); 831 | vfs.unextend("math2", {}, done); 832 | }); 833 | }); 834 | }); 835 | it("should extend using a stream", function (done) { 836 | var stream = fs.createReadStream(__dirname + "/math.js"); 837 | vfs.extend("math3", {stream: stream}, function (err, meta) { 838 | if (err) throw err; 839 | expect(meta).property("api").ok; 840 | var api = meta.api; 841 | expect(api).property("add").a("function"); 842 | expect(api).property("multiply").a("function"); 843 | api.add(3, 4, function (err, result) { 844 | if (err) throw err; 845 | expect(result).equal(3 + 4); 846 | vfs.unextend("math3", {}, done); 847 | }); 848 | }); 849 | }); 850 | it("should error with EEXIST if the same extension is added twice", function (done) { 851 | vfs.extend("math", {file: __dirname + "/math.js"}, function (err, meta) { 852 | if (err) throw err; 853 | expect(meta).property("api").ok; 854 | vfs.extend("math", {file: __dirname + "/math.js"}, function (err, meta) { 855 | expect(err).property("code").equal("EEXIST"); 856 | vfs.unextend("math", {}, done); 857 | }); 858 | }); 859 | }); 860 | it("should allow a redefine if options.redefine is set", function (done) { 861 | vfs.extend("test", {file: __dirname + "/math.js"}, function (err, meta) { 862 | if (err) throw err; 863 | expect(meta).property("api").ok; 864 | vfs.extend("test", {redefine: true, file: __dirname + "/math.js"}, function (err, meta) { 865 | if (err) throw err; 866 | expect(meta).property("api").ok; 867 | vfs.unextend("test", {}, done); 868 | }); 869 | }); 870 | }); 871 | }); 872 | 873 | describe('vfs.unextend()', function () { 874 | it("should remove an extension", function (done) { 875 | vfs.extend("math7", {file: __dirname + "/math.js"}, function (err, meta) { 876 | if (err) throw err; 877 | expect(meta).property("api").ok; 878 | vfs.use("math7", {}, function (err, meta) { 879 | if (err) throw err; 880 | expect(meta).property("api").ok; 881 | vfs.unextend("math7", {}, function (err, meta) { 882 | if (err) throw err; 883 | vfs.use("math7", {}, function (err, meta) { 884 | expect(err).property("code").equal("ENOENT"); 885 | done(); 886 | }); 887 | }); 888 | }); 889 | }); 890 | }); 891 | }); 892 | 893 | describe('vfs.use()', function () { 894 | it("should load an existing api", function (done) { 895 | vfs.extend("math4", {file: __dirname + "/math.js"}, function (err, meta) { 896 | if (err) throw err; 897 | expect(meta).property("api").ok; 898 | vfs.use("math4", {}, function (err, meta) { 899 | if (err) throw err; 900 | expect(meta).property("api").ok; 901 | var api = meta.api; 902 | expect(api).property("add").a("function"); 903 | expect(api).property("multiply").a("function"); 904 | api.add(3, 4, function (err, result) { 905 | if (err) throw err; 906 | expect(result).equal(3 + 4); 907 | vfs.unextend("math4", {}, done); 908 | }); 909 | }); 910 | }); 911 | }); 912 | it("should error with ENOENT if the api doesn't exist", function (done) { 913 | vfs.use("notfound", {}, function (err, meta) { 914 | expect(err).property("code").equal("ENOENT"); 915 | done(); 916 | }); 917 | }); 918 | }); 919 | 920 | }); 921 | --------------------------------------------------------------------------------