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",