diff --git a/README.md b/README.md index e0f0b35..5ea94a1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ with the following differences: * No synchronous versions of methods (e.g., `mkdir()` but not `mkdirSync()`). * No permissions (e.g., no `chown()`, `chmod()`, etc.). -* No support (yet) for `fs.watchFile()`, `fs.unwatchFile()`, `fs.watch()`. * No support for stream-based operations (e.g., `fs.ReadStream`, `fs.WriteStream`). Filer has other features lacking in node.js (e.g., swappable backend @@ -246,6 +245,7 @@ var fs = new Filer.FileSystem(); * [fs.fgetxattr(fd, name, callback)](#fgetxattr) * [fs.removexattr(path, name, callback)](#removexattr) * [fs.fremovexattr(fd, name, callback)](#fremovexattr) +* [fs.watch(filename, [options], [listener])](#watch) #### fs.rename(oldPath, newPath, callback) @@ -954,6 +954,52 @@ fs.open('/myfile', 'r', function(err, fd) { }); ``` +#### fs.watch(filename, [options], [listener]) + +Watch for changes to a file or directory at `filename`. The object returned is an `FSWatcher`, +which is an [`EventEmitter`](http://nodejs.org/api/events.html) with the following additional method: + +* `close()` - stops listening for changes, and removes all listeners from this instance. Use this +to stop watching a file or directory after calling `fs.watch()`. + +The only supported option is `recursive`, which if `true` will cause a watch to be placed +on a directory, and all sub-directories and files beneath it. + +The `listener` callback gets two arguments `(event, filename)`. `event` is either `'rename'` or `'change'`, +(currenty only `'rename'` is supported) and `filename` is the name of the file/dir which triggered the event. + +Unlike node.js, all watch events return a path. Also, all returned paths are absolute from the root +vs. just a relative filename. + +Examples: + +```javascript +// Example 1: create a watcher to see when a file is created +var watcher = fs.watch('/myfile', function(event, filename) { + // event could be 'change' or 'rename' and filename will be '/myfile' + // Stop watching for changes + watcher.close(); +}); +fs.writeFile('/myfile', 'data'); + +// Example 2: add the listener via watcher.on() +var watcher = fs.watch('/myfile2'); +watcher.on('change', function(event, filename) { + // event will be 'change' and filename will be '/myfile2' + // Stop watching for changes + watcher.close(); +}); +fs.writeFile('/myfile2', 'data2'); + +// Example 3: recursive watch on /data dir +var watcher = fs.watch('/data', { recursive: true }, function(event, filename) { + // event could be 'change' or 'rename' and filename will be '/data/subdir/file' + // Stop watching for changes + watcher.close(); +}); +fs.writeFile('/data/subdir/file', 'data'); +``` + ### FileSystemShell Many common file system shell operations are available by using a `FileSystemShell` object. diff --git a/bower.json b/bower.json index 24d9c70..cefaa22 100644 --- a/bower.json +++ b/bower.json @@ -2,6 +2,9 @@ "name": "filer", "version": "0.0.4", "main": "dist/filer.js", + "dependencies": { + "eventemitter2": "~0.4.13" + }, "devDependencies": { "mocha": "1.17.1", "chai": "1.9.0" diff --git a/gruntfile.js b/gruntfile.js index 13fa88e..6763153 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -26,6 +26,8 @@ module.exports = function(grunt) { 'src/index.js', 'src/shared.js', 'src/shell.js', + 'src/fswatcher.js', + 'src/environment.js', 'src/providers/**/*.js', 'src/adapters/**/*.js' ] @@ -45,7 +47,8 @@ module.exports = function(grunt) { options: { paths: { "src": "../src", - "build": "../build" + "build": "../build", + "EventEmitter": "../bower_components/eventemitter2/lib/eventemitter2" }, baseUrl: "lib", name: "build/almond", diff --git a/lib/intercom.js b/lib/intercom.js new file mode 100644 index 0000000..62a10d0 --- /dev/null +++ b/lib/intercom.js @@ -0,0 +1,314 @@ +define(function(require) { + + // Based on https://github.com/diy/intercom.js/blob/master/lib/intercom.js + // Copyright 2012 DIY Co Apache License, Version 2.0 + // http://www.apache.org/licenses/LICENSE-2.0 + + var EventEmitter = require('EventEmitter'); + var guid = require('src/shared').guid; + + function throttle(delay, fn) { + var last = 0; + return function() { + var now = Date.now(); + if (now - last > delay) { + last = now; + fn.apply(this, arguments); + } + }; + } + + function extend(a, b) { + if (typeof a === 'undefined' || !a) { a = {}; } + if (typeof b === 'object') { + for (var key in b) { + if (b.hasOwnProperty(key)) { + a[key] = b[key]; + } + } + } + return a; + } + + var localStorage = (function(window) { + if (typeof window.localStorage === 'undefined') { + return { + getItem : function() {}, + setItem : function() {}, + removeItem : function() {} + }; + } + return window.localStorage; + }(this)); + + function Intercom() { + var self = this; + var now = Date.now(); + + this.origin = guid(); + this.lastMessage = now; + this.receivedIDs = {}; + this.previousValues = {}; + + var storageHandler = function() { + self._onStorageEvent.apply(self, arguments); + }; + if (document.attachEvent) { + document.attachEvent('onstorage', storageHandler); + } else { + window.addEventListener('storage', storageHandler, false); + } + } + + Intercom.prototype._transaction = function(fn) { + var TIMEOUT = 1000; + var WAIT = 20; + var self = this; + var executed = false; + var listening = false; + var waitTimer = null; + + function lock() { + if (executed) { + return; + } + + var now = Date.now(); + var activeLock = localStorage.getItem(INDEX_LOCK)|0; + if (activeLock && now - activeLock < TIMEOUT) { + if (!listening) { + self._on('storage', lock); + listening = true; + } + waitTimer = window.setTimeout(lock, WAIT); + return; + } + executed = true; + localStorage.setItem(INDEX_LOCK, now); + + fn(); + unlock(); + } + + function unlock() { + if (listening) { + self._off('storage', lock); + } + if (waitTimer) { + window.clearTimeout(waitTimer); + } + localStorage.removeItem(INDEX_LOCK); + } + + lock(); + }; + + Intercom.prototype._cleanup_emit = throttle(100, function() { + var self = this; + + self._transaction(function() { + var now = Date.now(); + var threshold = now - THRESHOLD_TTL_EMIT; + var changed = 0; + var messages; + + try { + messages = JSON.parse(localStorage.getItem(INDEX_EMIT) || '[]'); + } catch(e) { + messages = []; + } + for (var i = messages.length - 1; i >= 0; i--) { + if (messages[i].timestamp < threshold) { + messages.splice(i, 1); + changed++; + } + } + if (changed > 0) { + localStorage.setItem(INDEX_EMIT, JSON.stringify(messages)); + } + }); + }); + + Intercom.prototype._cleanup_once = throttle(100, function() { + var self = this; + + self._transaction(function() { + var timestamp, ttl, key; + var table; + var now = Date.now(); + var changed = 0; + + try { + table = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}'); + } catch(e) { + table = {}; + } + for (key in table) { + if (self._once_expired(key, table)) { + delete table[key]; + changed++; + } + } + + if (changed > 0) { + localStorage.setItem(INDEX_ONCE, JSON.stringify(table)); + } + }); + }); + + Intercom.prototype._once_expired = function(key, table) { + if (!table) { + return true; + } + if (!table.hasOwnProperty(key)) { + return true; + } + if (typeof table[key] !== 'object') { + return true; + } + + var ttl = table[key].ttl || THRESHOLD_TTL_ONCE; + var now = Date.now(); + var timestamp = table[key].timestamp; + return timestamp < now - ttl; + }; + + Intercom.prototype._localStorageChanged = function(event, field) { + if (event && event.key) { + return event.key === field; + } + + var currentValue = localStorage.getItem(field); + if (currentValue === this.previousValues[field]) { + return false; + } + this.previousValues[field] = currentValue; + return true; + }; + + Intercom.prototype._onStorageEvent = function(event) { + event = event || window.event; + var self = this; + + if (this._localStorageChanged(event, INDEX_EMIT)) { + this._transaction(function() { + var now = Date.now(); + var data = localStorage.getItem(INDEX_EMIT); + var messages; + + try { + messages = JSON.parse(data || '[]'); + } catch(e) { + messages = []; + } + for (var i = 0; i < messages.length; i++) { + if (messages[i].origin === self.origin) continue; + if (messages[i].timestamp < self.lastMessage) continue; + if (messages[i].id) { + if (self.receivedIDs.hasOwnProperty(messages[i].id)) continue; + self.receivedIDs[messages[i].id] = true; + } + self.trigger(messages[i].name, messages[i].payload); + } + self.lastMessage = now; + }); + } + + this._trigger('storage', event); + }; + + Intercom.prototype._emit = function(name, message, id) { + id = (typeof id === 'string' || typeof id === 'number') ? String(id) : null; + if (id && id.length) { + if (this.receivedIDs.hasOwnProperty(id)) return; + this.receivedIDs[id] = true; + } + + var packet = { + id : id, + name : name, + origin : this.origin, + timestamp : Date.now(), + payload : message + }; + + var self = this; + this._transaction(function() { + var data = localStorage.getItem(INDEX_EMIT) || '[]'; + var delimiter = (data === '[]') ? '' : ','; + data = [data.substring(0, data.length - 1), delimiter, JSON.stringify(packet), ']'].join(''); + localStorage.setItem(INDEX_EMIT, data); + self.trigger(name, message); + + window.setTimeout(function() { + self._cleanup_emit(); + }, 50); + }); + }; + + Intercom.prototype.emit = function(name, message) { + this._emit.apply(this, arguments); + this._trigger('emit', name, message); + }; + + Intercom.prototype.once = function(key, fn, ttl) { + if (!Intercom.supported) { + return; + } + + var self = this; + this._transaction(function() { + var data; + try { + data = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}'); + } catch(e) { + data = {}; + } + if (!self._once_expired(key, data)) { + return; + } + + data[key] = {}; + data[key].timestamp = Date.now(); + if (typeof ttl === 'number') { + data[key].ttl = ttl * 1000; + } + + localStorage.setItem(INDEX_ONCE, JSON.stringify(data)); + fn(); + + window.setTimeout(function() { + self._cleanup_once(); + }, 50); + }); + }; + + extend(Intercom.prototype, EventEmitter.prototype); + + Intercom.supported = (typeof localStorage !== 'undefined'); + + var INDEX_EMIT = 'intercom'; + var INDEX_ONCE = 'intercom_once'; + var INDEX_LOCK = 'intercom_lock'; + + var THRESHOLD_TTL_EMIT = 50000; + var THRESHOLD_TTL_ONCE = 1000 * 3600; + + Intercom.destroy = function() { + localStorage.removeItem(INDEX_LOCK); + localStorage.removeItem(INDEX_EMIT); + localStorage.removeItem(INDEX_ONCE); + }; + + Intercom.getInstance = (function() { + var intercom; + return function() { + if (!intercom) { + intercom = new Intercom(); + } + return intercom; + }; + })(); + + return Intercom; +}); diff --git a/package.json b/package.json index b9c5a73..82b5bab 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "type": "git", "url": "https://github.com/js-platform/filer.git" }, + "dependencies": { + "bower": "~1.0.0" + }, "devDependencies": { - "bower": "~1.0.0", "grunt": "~0.4.0", "grunt-contrib-clean": "~0.4.0", "grunt-contrib-requirejs": "~0.4.0", diff --git a/src/fs.js b/src/fs.js index 1c19237..e5035eb 100644 --- a/src/fs.js +++ b/src/fs.js @@ -58,6 +58,8 @@ define(function(require) { var providers = require('src/providers/providers'); var adapters = require('src/adapters/adapters'); var Shell = require('src/shell'); + var Intercom = require('intercom'); + var FSWatcher = require('src/fswatcher'); /* * DirectoryEntry @@ -192,10 +194,17 @@ define(function(require) { update = true; } + function complete(error) { + // Queue this change so we can send watch events. + // Unlike node.js, we send the full path vs. basename/dirname only. + context.changes.push({ event: 'change', path: path }); + callback(error); + } + if(update) { - context.put(node.id, node, callback); + context.put(node.id, node, complete); } else { - callback(); + complete(); } } @@ -1649,21 +1658,66 @@ define(function(require) { queue = null; } + // We support the optional `options` arg from node, but ignore it + this.watch = function(filename, options, listener) { + if(isNullPath(filename)) { + throw new Error('Path must be a string without null bytes.'); + } + if(typeof options === 'function') { + listener = options; + options = {}; + } + options = options || {}; + listener = listener || nop; + + var watcher = new FSWatcher(); + watcher.start(filename, false, options.recursive); + watcher.on('change', listener); + + return watcher; + }; + + // Let other instances (in this or other windows) know about + // any changes to this fs instance. + function broadcastChanges(changes) { + if(!changes.length) { + return; + } + var intercom = Intercom.getInstance(); + changes.forEach(function(change) { + intercom.emit(change.event, change.event, change.path); + }); + } + // Open file system storage provider provider.open(function(err, needsFormatting) { function complete(error) { - // Wrap the provider so we can extend the context with fs flags. - // From this point forward we won't call open again, so drop it. + + function wrappedContext(methodName) { + var context = provider[methodName](); + context.flags = flags; + context.changes = []; + + // When the context is finished, let the fs deal with any change events + context.close = function() { + var changes = context.changes; + broadcastChanges(changes); + changes.length = 0; + }; + + return context; + } + + // Wrap the provider so we can extend the context with fs flags and + // an array of changes (e.g., watch event 'change' and 'rename' events + // for paths updated during the lifetime of the context). From this + // point forward we won't call open again, so it's safe to drop it. fs.provider = { - getReadWriteContext: function() { - var context = provider.getReadWriteContext(); - context.flags = flags; - return context; + openReadWriteContext: function() { + return wrappedContext('getReadWriteContext'); }, - getReadOnlyContext: function() { - var context = provider.getReadOnlyContext(); - context.flags = flags; - return context; + openReadOnlyContext: function() { + return wrappedContext('getReadOnlyContext'); } }; @@ -2334,14 +2388,30 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _open(fs, context, path, flags, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _open(fs, context, path, flags, complete); } ); if(error) callback(error); }; FileSystem.prototype.close = function(fd, callback) { - _close(this, fd, maybeCallback(callback)); + callback = maybeCallback(callback); + var fs = this; + var error = fs.queueOrRun( + function() { + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _close(fs, fd, complete); + } + ); + if(error) callback(error); }; FileSystem.prototype.mkdir = function(path, mode, callback) { // Support passing a mode arg, but we ignore it internally for now. @@ -2352,8 +2422,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _mkdir(context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _mkdir(context, path, complete); } ); if(error) callback(error); @@ -2363,8 +2437,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _rmdir(context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _rmdir(context, path, complete); } ); if(error) callback(error); @@ -2374,8 +2452,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _stat(context, fs.name, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _stat(context, fs.name, path, complete); } ); if(error) callback(error); @@ -2385,8 +2467,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _fstat(fs, context, fd, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _fstat(fs, context, fd, complete); } ); if(error) callback(error); @@ -2396,8 +2482,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _link(context, oldpath, newpath, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _link(context, oldpath, newpath, complete); } ); if(error) callback(error); @@ -2407,8 +2497,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _unlink(context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _unlink(context, path, complete); } ); if(error) callback(error); @@ -2423,8 +2517,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _read(fs, context, fd, buffer, offset, length, position, wrapper); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + wrapper.apply(this, arguments); + } + _read(fs, context, fd, buffer, offset, length, position, complete); } ); if(error) callback(error); @@ -2434,8 +2532,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _readFile(fs, context, path, options, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _readFile(fs, context, path, options, complete); } ); if(error) callback(error); @@ -2445,11 +2547,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _write(fs, context, fd, buffer, offset, length, position, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _write(fs, context, fd, buffer, offset, length, position, complete); } ); - if(error) callback(error); }; FileSystem.prototype.writeFile = function(path, data, options, callback_) { @@ -2457,8 +2562,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _writeFile(fs, context, path, data, options, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _writeFile(fs, context, path, data, options, complete); } ); if(error) callback(error); @@ -2468,8 +2577,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _appendFile(fs, context, path, data, options, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _appendFile(fs, context, path, data, options, complete); } ); if(error) callback(error); @@ -2479,8 +2592,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _exists(context, fs.name, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _exists(context, fs.name, path, complete); } ); if(error) callback(error); @@ -2490,8 +2607,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _lseek(fs, context, fd, offset, whence, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _lseek(fs, context, fd, offset, whence, complete); } ); if(error) callback(error); @@ -2501,8 +2622,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _readdir(context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _readdir(context, path, complete); } ); if(error) callback(error); @@ -2512,8 +2637,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _rename(context, oldpath, newpath, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _rename(context, oldpath, newpath, complete); } ); if(error) callback(error); @@ -2523,8 +2652,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _readlink(context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _readlink(context, path, complete); } ); if(error) callback(error); @@ -2535,8 +2668,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _symlink(context, srcpath, dstpath, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _symlink(context, srcpath, dstpath, complete); } ); if(error) callback(error); @@ -2546,8 +2683,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _lstat(fs, context, path, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _lstat(fs, context, path, complete); } ); if(error) callback(error); @@ -2563,8 +2704,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _truncate(context, path, length, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _truncate(context, path, length, complete); } ); if(error) callback(error); @@ -2574,8 +2719,12 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function() { - var context = fs.provider.getReadWriteContext(); - _ftruncate(fs, context, fd, length, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _ftruncate(fs, context, fd, length, complete); } ); if(error) callback(error); @@ -2585,11 +2734,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _utimes(context, path, atime, mtime, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _utimes(context, path, atime, mtime, complete); } ); - if (error) { callback(error); } @@ -2599,11 +2751,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _futimes(fs, context, fd, atime, mtime, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _futimes(fs, context, fd, atime, mtime, complete); } ); - if (error) { callback(error); } @@ -2614,11 +2769,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _setxattr(context, path, name, value, _flag, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _setxattr(context, path, name, value, _flag, complete); } ); - if (error) { callback(error); } @@ -2628,11 +2786,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _getxattr(context, path, name, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _getxattr(context, path, name, complete); } ); - if (error) { callback(error); } @@ -2643,11 +2804,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _fsetxattr(fs, context, fd, name, value, _flag, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _fsetxattr(fs, context, fd, name, value, _flag, complete); } ); - if (error) { callback(error); } @@ -2657,11 +2821,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _fgetxattr(fs, context, fd, name, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _fgetxattr(fs, context, fd, name, complete); } ); - if (error) { callback(error); } @@ -2671,11 +2838,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _removexattr(context, path, name, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _removexattr(context, path, name, complete); } ); - if (error) { callback(error); } @@ -2685,11 +2855,14 @@ define(function(require) { var fs = this; var error = fs.queueOrRun( function () { - var context = fs.provider.getReadWriteContext(); - _fremovexattr(fs, context, fd, name, callback); + var context = fs.provider.openReadWriteContext(); + function complete() { + context.close(); + callback.apply(fs, arguments); + } + _fremovexattr(fs, context, fd, name, complete); } ); - if (error) { callback(error); } diff --git a/src/fswatcher.js b/src/fswatcher.js new file mode 100644 index 0000000..8bbf879 --- /dev/null +++ b/src/fswatcher.js @@ -0,0 +1,54 @@ +define(function(require) { + + var EventEmitter = require('EventEmitter'); + var isNullPath = require('src/path').isNull; + var Intercom = require('intercom'); + + /** + * FSWatcher based on node.js' FSWatcher + * see https://github.com/joyent/node/blob/master/lib/fs.js + */ + function FSWatcher() { + EventEmitter.call(this); + var self = this; + var recursive = false; + var filename; + + function onchange(event, path) { + // Watch for exact filename, or parent path when recursive is true + if(filename === path || (recursive && path.indexOf(filename + '/') === 0)) { + self.emit('change', 'change', path); + } + } + + // We support, but ignore the second arg, which node.js uses. + self.start = function(filename_, persistent_, recursive_) { + // Bail if we've already started (and therefore have a filename); + if(filename) { + return; + } + + if(isNullPath(filename_)) { + throw new Error('Path must be a string without null bytes.'); + } + // TODO: get realpath for symlinks on filename... + filename = filename_; + + // Whether to watch beneath this path or not + recursive = recursive_ === true; + + var intercom = Intercom.getInstance(); + intercom.on('change', onchange); + }; + + self.close = function() { + var intercom = Intercom.getInstance(); + intercom.off('change', onchange); + self.removeAllListeners('change'); + }; + } + FSWatcher.prototype = new EventEmitter(); + FSWatcher.prototype.constructor = FSWatcher; + + return FSWatcher; +}); diff --git a/tests/require-config.js b/tests/require-config.js index 2415177..662f9ad 100644 --- a/tests/require-config.js +++ b/tests/require-config.js @@ -58,7 +58,8 @@ var config = (function() { "spec": "../tests/spec", "bugs": "../tests/bugs", "util": "../tests/lib/test-utils", - "Filer": "../src/index" + "Filer": "../src/index", + "EventEmitter": "../bower_components/eventemitter2/lib/eventemitter2" }, baseUrl: "../lib", optimize: "none", diff --git a/tests/spec/fs.watch.spec.js b/tests/spec/fs.watch.spec.js new file mode 100644 index 0000000..3ee5d7e --- /dev/null +++ b/tests/spec/fs.watch.spec.js @@ -0,0 +1,43 @@ +define(["Filer", "util"], function(Filer, util) { + + describe('fs.watch', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var fs = util.fs(); + expect(typeof fs.watch).to.equal('function'); + }); + + it('should get a change event when writing a file', function(done) { + var fs = util.fs(); + + var watcher = fs.watch('/myfile', function(event, filename) { + expect(event).to.equal('change'); + expect(filename).to.equal('/myfile'); + watcher.close(); + done(); + }); + + fs.writeFile('/myfile', 'data', function(error) { + if(error) throw error; + }); + }); + + it('should get a change event when writing a file in a dir with recursive=true', function(done) { + var fs = util.fs(); + + var watcher = fs.watch('/', { recursive: true }, function(event, filename) { + expect(event).to.equal('change'); + expect(filename).to.equal('/'); + watcher.close(); + done(); + }); + + fs.writeFile('/myfile', 'data', function(error) { + if(error) throw error; + }); + }); + }); + +}); diff --git a/tests/spec/node-js/simple/test-fs-watch-recursive.js b/tests/spec/node-js/simple/test-fs-watch-recursive.js new file mode 100644 index 0000000..b3072fa --- /dev/null +++ b/tests/spec/node-js/simple/test-fs-watch-recursive.js @@ -0,0 +1,38 @@ +define(["Filer", "util"], function(Filer, util) { + + /** + * NOTE: unlike node.js, which either doesn't give filenames (e.g., in case of + * fd vs. path) for events, or gives only a portion thereof (e.g., basname), + * we give full, abs paths always. + */ + + describe("node.js tests: https://github.com/joyent/node/blob/master/test/simple/test-fs-watch-recursive.js", function() { + + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should get change event for writeFile() under a recursive watched dir', function(done) { + var fs = util.fs(); + + fs.mkdir('/test', function(error) { + if(error) throw error; + + fs.mkdir('/test/subdir', function(error) { + if(error) throw error; + + var watcher = fs.watch('/test', {recursive: true}); + watcher.on('change', function(event, filename) { + expect(event).to.equal('change'); + // Expect to see that a new file was created in /test/subdir + expect(filename).to.equal('/test/subdir'); + watcher.close(); + done(); + }); + + fs.writeFile('/test/subdir/watch.txt', 'world'); + }); + }); + }); + + }); +}); diff --git a/tests/spec/node-js/simple/test-fs-watch.js b/tests/spec/node-js/simple/test-fs-watch.js new file mode 100644 index 0000000..9e1e76d --- /dev/null +++ b/tests/spec/node-js/simple/test-fs-watch.js @@ -0,0 +1,74 @@ +define(["Filer", "util"], function(Filer, util) { + + /** + * NOTE: unlike node.js, which either doesn't give filenames (e.g., in case of + * fd vs. path) for events, or gives only a portion thereof (e.g., basname), + * we give full, abs paths always. + */ + + var filenameOne = '/watch.txt'; + var filenameTwo = '/hasOwnProperty'; + + describe("node.js tests: https://github.com/joyent/node/blob/master/test/simple/test-fs-watch.js", function() { + + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should get change event for writeFile() using FSWatcher object', function(done) { + var fs = util.fs(); + var changes = 0; + + var watcher = fs.watch(filenameOne); + watcher.on('change', function(event, filename) { + expect(event).to.equal('change'); + expect(filename).to.equal(filenameOne); + + // Make sure only one change event comes in (i.e., close() works) + changes++; + watcher.close(); + + fs.writeFile(filenameOne, 'hello again', function(error) { + expect(changes).to.equal(1); + done(); + }); + }); + + fs.writeFile(filenameOne, 'hello'); + }); + + it('should get change event for writeFile() using fs.watch() only', function(done) { + var fs = util.fs(); + var changes = 0; + + var watcher = fs.watch(filenameTwo, function(event, filename) { + expect(event).to.equal('change'); + expect(filename).to.equal(filenameTwo); + + watcher.close(); + done(); + }); + + fs.writeFile(filenameTwo, 'pardner'); + }); + + it('should allow watches on dirs', function(done) { + var fs = util.fs(); + fs.mkdir('/tmp', function(error) { + if(error) throw error; + + var watcher = fs.watch('/tmp', function(event, filename) { +// TODO: node thinks this should be 'rename', need to add rename along with change. + expect(event).to.equal('change'); + expect(filename).to.equal('/tmp'); + watcher.close(); + done(); + }); + + fs.open('/tmp/newfile.txt', 'w', function(error, fd) { + if(error) throw error; + fs.close(fd); + }); + }); + }); + }); +}); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 058e937..5e99bf1 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -35,6 +35,7 @@ define([ "spec/path-resolution.spec", "spec/times.spec", "spec/time-flags.spec", + "spec/fs.watch.spec", // Filer.FileSystem.providers.* "spec/providers/providers.spec", @@ -58,6 +59,8 @@ define([ // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir", "spec/node-js/simple/test-fs-null-bytes", + "spec/node-js/simple/test-fs-watch", + "spec/node-js/simple/test-fs-watch-recursive", // Regressions, Bugs "bugs/issue105",