Merge pull request #140 from humphd/issue132

Support fs.watch(), FSWatcher. Fixes #132
This commit is contained in:
Alan K 2014-03-08 16:04:15 -05:00
commit 0cfb29eaec
12 changed files with 838 additions and 84 deletions

View File

@ -24,7 +24,6 @@ with the following differences:
* No synchronous versions of methods (e.g., `mkdir()` but not `mkdirSync()`). * No synchronous versions of methods (e.g., `mkdir()` but not `mkdirSync()`).
* No permissions (e.g., no `chown()`, `chmod()`, etc.). * 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`). * No support for stream-based operations (e.g., `fs.ReadStream`, `fs.WriteStream`).
Filer has other features lacking in node.js (e.g., swappable backend 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.fgetxattr(fd, name, callback)](#fgetxattr)
* [fs.removexattr(path, name, callback)](#removexattr) * [fs.removexattr(path, name, callback)](#removexattr)
* [fs.fremovexattr(fd, name, callback)](#fremovexattr) * [fs.fremovexattr(fd, name, callback)](#fremovexattr)
* [fs.watch(filename, [options], [listener])](#watch)
#### fs.rename(oldPath, newPath, callback)<a name="rename"></a> #### fs.rename(oldPath, newPath, callback)<a name="rename"></a>
@ -954,6 +954,52 @@ fs.open('/myfile', 'r', function(err, fd) {
}); });
``` ```
#### fs.watch(filename, [options], [listener])<a name="watch"></a>
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<a name="FileSystemShell"></a> ### FileSystemShell<a name="FileSystemShell"></a>
Many common file system shell operations are available by using a `FileSystemShell` object. Many common file system shell operations are available by using a `FileSystemShell` object.

View File

@ -2,6 +2,9 @@
"name": "filer", "name": "filer",
"version": "0.0.4", "version": "0.0.4",
"main": "dist/filer.js", "main": "dist/filer.js",
"dependencies": {
"eventemitter2": "~0.4.13"
},
"devDependencies": { "devDependencies": {
"mocha": "1.17.1", "mocha": "1.17.1",
"chai": "1.9.0" "chai": "1.9.0"

View File

@ -26,6 +26,8 @@ module.exports = function(grunt) {
'src/index.js', 'src/index.js',
'src/shared.js', 'src/shared.js',
'src/shell.js', 'src/shell.js',
'src/fswatcher.js',
'src/environment.js',
'src/providers/**/*.js', 'src/providers/**/*.js',
'src/adapters/**/*.js' 'src/adapters/**/*.js'
] ]
@ -45,7 +47,8 @@ module.exports = function(grunt) {
options: { options: {
paths: { paths: {
"src": "../src", "src": "../src",
"build": "../build" "build": "../build",
"EventEmitter": "../bower_components/eventemitter2/lib/eventemitter2"
}, },
baseUrl: "lib", baseUrl: "lib",
name: "build/almond", name: "build/almond",

314
lib/intercom.js Normal file
View File

@ -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;
});

View File

@ -15,8 +15,10 @@
"type": "git", "type": "git",
"url": "https://github.com/js-platform/filer.git" "url": "https://github.com/js-platform/filer.git"
}, },
"dependencies": {
"bower": "~1.0.0"
},
"devDependencies": { "devDependencies": {
"bower": "~1.0.0",
"grunt": "~0.4.0", "grunt": "~0.4.0",
"grunt-contrib-clean": "~0.4.0", "grunt-contrib-clean": "~0.4.0",
"grunt-contrib-requirejs": "~0.4.0", "grunt-contrib-requirejs": "~0.4.0",

333
src/fs.js
View File

@ -58,6 +58,8 @@ define(function(require) {
var providers = require('src/providers/providers'); var providers = require('src/providers/providers');
var adapters = require('src/adapters/adapters'); var adapters = require('src/adapters/adapters');
var Shell = require('src/shell'); var Shell = require('src/shell');
var Intercom = require('intercom');
var FSWatcher = require('src/fswatcher');
/* /*
* DirectoryEntry * DirectoryEntry
@ -192,10 +194,17 @@ define(function(require) {
update = true; 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) { if(update) {
context.put(node.id, node, callback); context.put(node.id, node, complete);
} else { } else {
callback(); complete();
} }
} }
@ -1649,21 +1658,66 @@ define(function(require) {
queue = null; 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 // Open file system storage provider
provider.open(function(err, needsFormatting) { provider.open(function(err, needsFormatting) {
function complete(error) { 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 = { fs.provider = {
getReadWriteContext: function() { openReadWriteContext: function() {
var context = provider.getReadWriteContext(); return wrappedContext('getReadWriteContext');
context.flags = flags;
return context;
}, },
getReadOnlyContext: function() { openReadOnlyContext: function() {
var context = provider.getReadOnlyContext(); return wrappedContext('getReadOnlyContext');
context.flags = flags;
return context;
} }
}; };
@ -2334,14 +2388,30 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_open(fs, context, path, flags, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_open(fs, context, path, flags, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
}; };
FileSystem.prototype.close = function(fd, callback) { 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) { FileSystem.prototype.mkdir = function(path, mode, callback) {
// Support passing a mode arg, but we ignore it internally for now. // Support passing a mode arg, but we ignore it internally for now.
@ -2352,8 +2422,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_mkdir(context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_mkdir(context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2363,8 +2437,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_rmdir(context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_rmdir(context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2374,8 +2452,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_stat(context, fs.name, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_stat(context, fs.name, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2385,8 +2467,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_fstat(fs, context, fd, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_fstat(fs, context, fd, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2396,8 +2482,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_link(context, oldpath, newpath, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_link(context, oldpath, newpath, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2407,8 +2497,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_unlink(context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_unlink(context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2423,8 +2517,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_read(fs, context, fd, buffer, offset, length, position, wrapper); function complete() {
context.close();
wrapper.apply(this, arguments);
}
_read(fs, context, fd, buffer, offset, length, position, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2434,8 +2532,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_readFile(fs, context, path, options, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_readFile(fs, context, path, options, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2445,11 +2547,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_write(fs, context, fd, buffer, offset, length, position, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_write(fs, context, fd, buffer, offset, length, position, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
}; };
FileSystem.prototype.writeFile = function(path, data, options, callback_) { FileSystem.prototype.writeFile = function(path, data, options, callback_) {
@ -2457,8 +2562,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_writeFile(fs, context, path, data, options, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_writeFile(fs, context, path, data, options, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2468,8 +2577,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_appendFile(fs, context, path, data, options, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_appendFile(fs, context, path, data, options, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2479,8 +2592,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_exists(context, fs.name, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_exists(context, fs.name, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2490,8 +2607,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_lseek(fs, context, fd, offset, whence, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_lseek(fs, context, fd, offset, whence, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2501,8 +2622,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_readdir(context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_readdir(context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2512,8 +2637,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_rename(context, oldpath, newpath, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_rename(context, oldpath, newpath, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2523,8 +2652,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_readlink(context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_readlink(context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2535,8 +2668,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_symlink(context, srcpath, dstpath, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_symlink(context, srcpath, dstpath, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2546,8 +2683,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_lstat(fs, context, path, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_lstat(fs, context, path, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2563,8 +2704,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_truncate(context, path, length, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_truncate(context, path, length, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2574,8 +2719,12 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function() { function() {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_ftruncate(fs, context, fd, length, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_ftruncate(fs, context, fd, length, complete);
} }
); );
if(error) callback(error); if(error) callback(error);
@ -2585,11 +2734,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_utimes(context, path, atime, mtime, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_utimes(context, path, atime, mtime, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2599,11 +2751,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_futimes(fs, context, fd, atime, mtime, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_futimes(fs, context, fd, atime, mtime, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2614,11 +2769,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_setxattr(context, path, name, value, _flag, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_setxattr(context, path, name, value, _flag, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2628,11 +2786,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_getxattr(context, path, name, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_getxattr(context, path, name, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2643,11 +2804,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_fsetxattr(fs, context, fd, name, value, _flag, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_fsetxattr(fs, context, fd, name, value, _flag, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2657,11 +2821,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_fgetxattr(fs, context, fd, name, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_fgetxattr(fs, context, fd, name, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2671,11 +2838,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_removexattr(context, path, name, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_removexattr(context, path, name, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }
@ -2685,11 +2855,14 @@ define(function(require) {
var fs = this; var fs = this;
var error = fs.queueOrRun( var error = fs.queueOrRun(
function () { function () {
var context = fs.provider.getReadWriteContext(); var context = fs.provider.openReadWriteContext();
_fremovexattr(fs, context, fd, name, callback); function complete() {
context.close();
callback.apply(fs, arguments);
}
_fremovexattr(fs, context, fd, name, complete);
} }
); );
if (error) { if (error) {
callback(error); callback(error);
} }

54
src/fswatcher.js Normal file
View File

@ -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;
});

View File

@ -58,7 +58,8 @@ var config = (function() {
"spec": "../tests/spec", "spec": "../tests/spec",
"bugs": "../tests/bugs", "bugs": "../tests/bugs",
"util": "../tests/lib/test-utils", "util": "../tests/lib/test-utils",
"Filer": "../src/index" "Filer": "../src/index",
"EventEmitter": "../bower_components/eventemitter2/lib/eventemitter2"
}, },
baseUrl: "../lib", baseUrl: "../lib",
optimize: "none", optimize: "none",

View File

@ -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;
});
});
});
});

View File

@ -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');
});
});
});
});
});

View File

@ -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);
});
});
});
});
});

View File

@ -35,6 +35,7 @@ define([
"spec/path-resolution.spec", "spec/path-resolution.spec",
"spec/times.spec", "spec/times.spec",
"spec/time-flags.spec", "spec/time-flags.spec",
"spec/fs.watch.spec",
// Filer.FileSystem.providers.* // Filer.FileSystem.providers.*
"spec/providers/providers.spec", "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) // 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-mkdir",
"spec/node-js/simple/test-fs-null-bytes", "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 // Regressions, Bugs
"bugs/issue105", "bugs/issue105",