Merge pull request #114 from humphd/issue98

[WIP] Initial work on FileSystemShell and fs.Shell(), fixes #98
This commit is contained in:
Alan K 2014-02-19 18:20:54 -05:00
commit bf0354fe78
18 changed files with 2441 additions and 34 deletions

286
README.md
View File

@ -36,7 +36,7 @@ Errors are passed to callbacks through the first parameter. As with node.js,
there is no guarantee that file system operations will be executed in the order there is no guarantee that file system operations will be executed in the order
they are invoked. Ensure proper ordering by chaining operations in callbacks. they are invoked. Ensure proper ordering by chaining operations in callbacks.
### Example ### Overview
To create a new file system or open an existing one, create a new `FileSystem` To create a new file system or open an existing one, create a new `FileSystem`
instance. By default, a new [IndexedDB](https://developer.mozilla.org/en/docs/IndexedDB) instance. By default, a new [IndexedDB](https://developer.mozilla.org/en/docs/IndexedDB)
@ -58,6 +58,15 @@ fs.open('/myfile', 'w+', function(err, fd) {
}); });
``` ```
For a complete list of `FileSystem` methods and examples, see the [FileSystem Instance Methods](#FileSystemMethods)
section below.
Filer also supports node's Path module. See the [Filer.Path](#FilerPath) section below.
In addition, common shell operations (e.g., rm, touch, cat, etc.) are supported via the
`FileSystemShell` object, which can be obtained from, and used with a `FileSystem`.
See the[FileSystemShell](#FileSystemShell) section below.
### API Reference ### API Reference
Like node.js, callbacks for methods that accept them are optional but suggested (i.e., if Like node.js, callbacks for methods that accept them are optional but suggested (i.e., if
@ -162,7 +171,7 @@ var fs = new FileSystem({ provider: compressionAdapter });
You can also write your own adapter if you need to add new capabilities to the providers. Adapters share the same You can also write your own adapter if you need to add new capabilities to the providers. Adapters share the same
interface as providers. See the code in `src/providers` and `src/adapters` for many examples. interface as providers. See the code in `src/providers` and `src/adapters` for many examples.
####Filer.Path ####Filer.Path<a name="FilerPath"></a>
The node.js [path module](http://nodejs.org/api/path.html) is available via the `Filer.Path` object. It is The node.js [path module](http://nodejs.org/api/path.html) is available via the `Filer.Path` object. It is
identical to the node.js version with the following differences: identical to the node.js version with the following differences:
@ -195,7 +204,7 @@ For more info see the docs in the [path module](http://nodejs.org/api/path.html)
* `path.sep` * `path.sep`
* `path.delimiter` * `path.delimiter`
###FileSystem Instance Methods ###FileSystem Instance Methods<a name="FileSystemMethods"></a>
Once a `FileSystem` is created, it has the following methods. NOTE: code examples below assume Once a `FileSystem` is created, it has the following methods. NOTE: code examples below assume
a `FileSystem` instance named `fs` has been created like so: a `FileSystem` instance named `fs` has been created like so:
@ -911,3 +920,274 @@ fs.open('/myfile', 'r', function(err, fd) {
fs.close(fd); fs.close(fd);
}); });
``` ```
### FileSystemShell<a name="FileSystemShell"></a>
Many common file system shell operations are available by using a `FileSystemShell` object.
The `FileSystemShell` is obtained from, and used in conjuction with a `FileSystem`,
and provides augmented features. Many separate `FileSystemShell` objects can exist per
`FileSystem`, but each `FileSystemShell` is bound to a single instance of a `FileSystem`
for its lifetime.
A `FileSystemShell` is created using the `FileSystem.Shell()` function:
```javascript
var fs = new Filer.FileSystem();
var sh = fs.Shell(options);
var sh2 = fs.Shell(options);
// sh and sh2 are two separate shells, each bound to fs
```
The `FileSystemShell` can take an optional `options` object. The `options` object
can include `env`, which is a set of environment variables. Currently supported variables
include `TMP` (the path to the temporary directory), and `PATH` (the list of known paths) and
others may be added in the future. You can also add your own, or update existing variables.
```javascript
var fs = new Filer.FileSystem();
var sh = fs.Shell({
env: {
TMP: '/tempdir',
PATH: '/one:/two'
}
});
var tempPath = sh.env.get('TMP');
sh.env.set('TMP', '/newtempdir');
```
NOTE: unless otherwise stated, all `FileSystemShell` methods can take relative or absolute
paths. Relative paths are resolved relative to the shell's current working directory (`sh.cwd`).
This is different from the `FileSystem`, which requires absolute paths, and has no notion
of a current working directory.
#### FileSystemShell Properties
A `FileSystemShell` has a number of properties, including:
* `fs` - (readonly) a reference to the bound `FileSystem`
* `env` - (readonly) the shell's environment. The shell's environemnt `env` object has `get(name)`
and `set(name, value)` methods.
Example:
```javascript
var fs = new Filer.FileSystem();
var sh = fs.Shell();
var p = sh.env.get('PATH');
// Store the current location
var before = sh.pwd();
var after;
sh.cd('/newdir', function(err) {
if(err) throw err;
// Get the new location
after = sh.pwd();
});
```
#### FileSystemShell Instance Methods
Once a `FileSystemShell` object is created, it has the following methods. NOTE: code
examples below assume a `FileSystemShell` instance named `sh` has been created like so:
```javascript
var fs = new Filer.FileSystem();
var sh = fs.Shell();
```
* [sh.cd(path, callback)](#cd)
* [sh.pwd()](#pwd)
* [sh.ls(dir, [options], callback)](#ls)
* [sh.exec(path, [args], callback)](#exec)
* [sh.touch(path, [options], callback)](#touch)
* [sh.cat(files, callback)](#cat)
* [sh.rm(path, [options], callback)](#rm)
* [sh.tempDir(callback)](#tempDir)
#### sh.cd(path, callback)<a name="cd"></a>
Changes the current working directory to the directory at `path`. The callback returns
an error if `path` does not exist, or is not a directory. Once the callback occurs
the shell's cwd is updated to the new path (you can access it via `sh.pwd()`).
Example:
```javascript
sh.cd('/dir1', function(err) {
if(err) throw err;
// sh.pwd() is now '/dir1'
});
```
#### sh.pwd()<a name="pwd"></a>
Returns the shell's current working directory. See [sh.cd()](#cd).
#### sh.ls(dir, [options], callback)<a name="ls"></a>
Get the listing of a directory, returning an array of directory entries
in the following form:
```
{
path: <String> the basename of the directory entry
links: <Number> the number of links to the entry
size: <Number> the size in bytes of the entry
modified: <Number> the last modified date/time
type: <String> the type of the entry
contents: <Array> an optional array of child entries, if this entry is itself a directory
}
```
By default `sh.ls()` gives a shallow listing. If you want to follow
directories as they are encountered, use the `recursive=true` option. NOTE:
you should not count on the order of the returned entries always being the same.
Example:
```javascript
/**
* Given a dir structure of:
*
* /dir
* file1
* file2
* dir2/
* file3
*/
// Shallow listing
sh.ls('/dir', function(err, entries) {
if(err) throw err;
// entries is now an array of 3 file/dir entries under /dir
});
// Deep listing
sh.ls('/dir', { recursive: true }, function(err, entries) {
if(err) throw err;
// entries is now an array of 3 file/dir entries under /dir.
// The entry object for '/dir2' also includes a `contents` property,
// which is an array of 1 entry element for `file3`.
});
```
#### sh.exec(path, [args], callback)<a name="exec"></a>
Attempts to execute the .js command located at `path`. The `sh.exec` method
enables apps to install larger programs into the file system and run them
later without having to re-download. Such commands should be written so as
to assume the existence of 3 global variables, which will be defined at runtime:
* `fs` - [FileSystem] the `FileSystem` object bound to this shell.
* `args` - [Array] a list of any arguments for the command, or the empty list
* `callback` - [Function] a callback function of the form `function callback(error, result)`
to call when done.
The .js command's contents should be the body of a function that
looks like this:
```javascript
function(fs, args, callback) {
//-------------------------commmand code here---------
// ...
//----------------------------------------------------
}
```
Example:
```javascript
// Simple command to delete a file.
var cmd = "fs.unlink(args[0], callback);"
// Write the file to the filesystem
fs.writeFile('/cmd.js', cmd, callback(err) {
if(err) throw err;
// Execute the command
sh.exec('/cmd.js', [ '/file' ], function(err, result) {
if(err) throw err;
});
});
```
#### sh.touch(path, [options], callback)<a name="touch"></a>
Create a file if it does not exist, or update the access and modified
times if it does. Valid options include:
* `updateOnly` - `true` if the file's access/modified dates are to be updated
only (but missing file not to be)
* `date` - a date to use instead of the current date and time when updating
access and modified dates.
Example:
```javascript
sh.touch('/newfile', function(err) {
if(err) throw err;
fs.exists('/newfile', function(exists) {
// exists is now true.
}
});
```
#### sh.cat(files, callback)<a name="cat"></a>
Concatenates multiple files into a single string, with each file
separated by a newline character. The `files` argument should be
a String (i.e., path to a single file) or an Array of Strings (i.e.,
multiple paths for multiple files).
Example:
```javascript
sh.cat([ './file1', '../file2' ], function(err, data) {
if(err) throw err;
// data is now the contents of file1 and file2 joined
});
```
#### sh.rm(path, [options], callback)<a name="rm"></a>
Removes (deletes) the file or directory at `path`. If `path` is a file, it will
be removed. If `path` is a directory, it will be removed if it is empty, otherwise
the callback will receive an error. In order to remove non-empty directories,
use the `recursive=true` option.
Example:
```javascript
sh.rm('./file', function(err) {
if(err) throw err;
// ./file is now removed
});
sh.rm('/dir', { recursive: true }, function(err) {
if(err) throw err;
// /dir and all its children are now removed
});
```
#### sh.tempDir(callback)<a name="tempDir"></a>
Gets the path to the shell's temporary directory, creating it if it
does not already exist. The temp directory to use is specified in the
`env.TMP` environment variable. The callback receives an error
and the `tempDir` path. NOTE: it is safe to call this many times (i.e.,
the temp dir will only be created once). No effort is made to clean-up
the temp dir, and it is up to the caller to destroy it if desired.
Example:
```javascript
// Default /tmp dir
sh.tempDir(function(err, tmp) {
if(err) throw err;
// tmp is now '/tmp' by default, and /tmp exists
});
// Specify a tmp dir path
sh.env.TMP = '/temporary'
sh.tempDir(function(err, tmp) {
if(err) throw err;
// tmp is now '/temporary', and /temporary exists
});
```

View File

@ -18,14 +18,17 @@ module.exports = function(grunt) {
jshint: { jshint: {
// Don't bother with src/path.js // Don't bother with src/path.js
all: ['gruntfile.js', all: [
'src/constants.js', 'gruntfile.js',
'src/error.js', 'src/constants.js',
'src/fs.js', 'src/error.js',
'src/shared.js', 'src/fs.js',
'src/providers/**/*.js', 'src/index.js',
'src/adapters/**/*.js' 'src/shared.js',
] 'src/shell.js',
'src/providers/**/*.js',
'src/adapters/**/*.js'
]
}, },
mocha: { mocha: {

964
lib/async.js Normal file
View File

@ -0,0 +1,964 @@
/*global setImmediate: false, setTimeout: false, console: false */
/**
* https://raw.github.com/caolan/async/master/lib/async.js Feb 18, 2014
* Used under MIT - https://github.com/caolan/async/blob/master/LICENSE
*/
(function () {
var async = {};
// global on the server, window in the browser
var root, previous_async;
root = this;
if (root != null) {
previous_async = root.async;
}
async.noConflict = function () {
root.async = previous_async;
return async;
};
function only_once(fn) {
var called = false;
return function() {
if (called) throw new Error("Callback was already called.");
called = true;
fn.apply(root, arguments);
}
}
//// cross-browser compatiblity functions ////
var _each = function (arr, iterator) {
if (arr.forEach) {
return arr.forEach(iterator);
}
for (var i = 0; i < arr.length; i += 1) {
iterator(arr[i], i, arr);
}
};
var _map = function (arr, iterator) {
if (arr.map) {
return arr.map(iterator);
}
var results = [];
_each(arr, function (x, i, a) {
results.push(iterator(x, i, a));
});
return results;
};
var _reduce = function (arr, iterator, memo) {
if (arr.reduce) {
return arr.reduce(iterator, memo);
}
_each(arr, function (x, i, a) {
memo = iterator(memo, x, i, a);
});
return memo;
};
var _keys = function (obj) {
if (Object.keys) {
return Object.keys(obj);
}
var keys = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
};
//// exported async module functions ////
//// nextTick implementation with browser-compatible fallback ////
if (typeof process === 'undefined' || !(process.nextTick)) {
if (typeof setImmediate === 'function') {
async.nextTick = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
async.setImmediate = async.nextTick;
}
else {
async.nextTick = function (fn) {
setTimeout(fn, 0);
};
async.setImmediate = async.nextTick;
}
}
else {
async.nextTick = process.nextTick;
if (typeof setImmediate !== 'undefined') {
async.setImmediate = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
}
else {
async.setImmediate = async.nextTick;
}
}
async.each = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
_each(arr, function (x) {
iterator(x, only_once(function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback(null);
}
}
}));
});
};
async.forEach = async.each;
async.eachSeries = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
var iterate = function () {
iterator(arr[completed], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback(null);
}
else {
iterate();
}
}
});
};
iterate();
};
async.forEachSeries = async.eachSeries;
async.eachLimit = function (arr, limit, iterator, callback) {
var fn = _eachLimit(limit);
fn.apply(null, [arr, iterator, callback]);
};
async.forEachLimit = async.eachLimit;
var _eachLimit = function (limit) {
return function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length || limit <= 0) {
return callback();
}
var completed = 0;
var started = 0;
var running = 0;
(function replenish () {
if (completed >= arr.length) {
return callback();
}
while (running < limit && started < arr.length) {
started += 1;
running += 1;
iterator(arr[started - 1], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
running -= 1;
if (completed >= arr.length) {
callback();
}
else {
replenish();
}
}
});
}
})();
};
};
var doParallel = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.each].concat(args));
};
};
var doParallelLimit = function(limit, fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [_eachLimit(limit)].concat(args));
};
};
var doSeries = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.eachSeries].concat(args));
};
};
var _asyncMap = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (err, v) {
results[x.index] = v;
callback(err);
});
}, function (err) {
callback(err, results);
});
};
async.map = doParallel(_asyncMap);
async.mapSeries = doSeries(_asyncMap);
async.mapLimit = function (arr, limit, iterator, callback) {
return _mapLimit(limit)(arr, iterator, callback);
};
var _mapLimit = function(limit) {
return doParallelLimit(limit, _asyncMap);
};
// reduce only has a series version, as doing reduce in parallel won't
// work in many situations.
async.reduce = function (arr, memo, iterator, callback) {
async.eachSeries(arr, function (x, callback) {
iterator(memo, x, function (err, v) {
memo = v;
callback(err);
});
}, function (err) {
callback(err, memo);
});
};
// inject alias
async.inject = async.reduce;
// foldl alias
async.foldl = async.reduce;
async.reduceRight = function (arr, memo, iterator, callback) {
var reversed = _map(arr, function (x) {
return x;
}).reverse();
async.reduce(reversed, memo, iterator, callback);
};
// foldr alias
async.foldr = async.reduceRight;
var _filter = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.filter = doParallel(_filter);
async.filterSeries = doSeries(_filter);
// select alias
async.select = async.filter;
async.selectSeries = async.filterSeries;
var _reject = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (!v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.reject = doParallel(_reject);
async.rejectSeries = doSeries(_reject);
var _detect = function (eachfn, arr, iterator, main_callback) {
eachfn(arr, function (x, callback) {
iterator(x, function (result) {
if (result) {
main_callback(x);
main_callback = function () {};
}
else {
callback();
}
});
}, function (err) {
main_callback();
});
};
async.detect = doParallel(_detect);
async.detectSeries = doSeries(_detect);
async.some = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (v) {
main_callback(true);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(false);
});
};
// any alias
async.any = async.some;
async.every = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (!v) {
main_callback(false);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(true);
});
};
// all alias
async.all = async.every;
async.sortBy = function (arr, iterator, callback) {
async.map(arr, function (x, callback) {
iterator(x, function (err, criteria) {
if (err) {
callback(err);
}
else {
callback(null, {value: x, criteria: criteria});
}
});
}, function (err, results) {
if (err) {
return callback(err);
}
else {
var fn = function (left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
};
callback(null, _map(results.sort(fn), function (x) {
return x.value;
}));
}
});
};
async.auto = function (tasks, callback) {
callback = callback || function () {};
var keys = _keys(tasks);
if (!keys.length) {
return callback(null);
}
var results = {};
var listeners = [];
var addListener = function (fn) {
listeners.unshift(fn);
};
var removeListener = function (fn) {
for (var i = 0; i < listeners.length; i += 1) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
return;
}
}
};
var taskComplete = function () {
_each(listeners.slice(0), function (fn) {
fn();
});
};
addListener(function () {
if (_keys(results).length === keys.length) {
callback(null, results);
callback = function () {};
}
});
_each(keys, function (k) {
var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
var taskCallback = function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
if (err) {
var safeResults = {};
_each(_keys(results), function(rkey) {
safeResults[rkey] = results[rkey];
});
safeResults[k] = args;
callback(err, safeResults);
// stop subsequent errors hitting callback multiple times
callback = function () {};
}
else {
results[k] = args;
async.setImmediate(taskComplete);
}
};
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
var ready = function () {
return _reduce(requires, function (a, x) {
return (a && results.hasOwnProperty(x));
}, true) && !results.hasOwnProperty(k);
};
if (ready()) {
task[task.length - 1](taskCallback, results);
}
else {
var listener = function () {
if (ready()) {
removeListener(listener);
task[task.length - 1](taskCallback, results);
}
};
addListener(listener);
}
});
};
async.waterfall = function (tasks, callback) {
callback = callback || function () {};
if (tasks.constructor !== Array) {
var err = new Error('First argument to waterfall must be an array of functions');
return callback(err);
}
if (!tasks.length) {
return callback();
}
var wrapIterator = function (iterator) {
return function (err) {
if (err) {
callback.apply(null, arguments);
callback = function () {};
}
else {
var args = Array.prototype.slice.call(arguments, 1);
var next = iterator.next();
if (next) {
args.push(wrapIterator(next));
}
else {
args.push(callback);
}
async.setImmediate(function () {
iterator.apply(null, args);
});
}
};
};
wrapIterator(async.iterator(tasks))();
};
var _parallel = function(eachfn, tasks, callback) {
callback = callback || function () {};
if (tasks.constructor === Array) {
eachfn.map(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
eachfn.each(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.parallel = function (tasks, callback) {
_parallel({ map: async.map, each: async.each }, tasks, callback);
};
async.parallelLimit = function(tasks, limit, callback) {
_parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback);
};
async.series = function (tasks, callback) {
callback = callback || function () {};
if (tasks.constructor === Array) {
async.mapSeries(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
async.eachSeries(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.iterator = function (tasks) {
var makeCallback = function (index) {
var fn = function () {
if (tasks.length) {
tasks[index].apply(null, arguments);
}
return fn.next();
};
fn.next = function () {
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
};
return fn;
};
return makeCallback(0);
};
async.apply = function (fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function () {
return fn.apply(
null, args.concat(Array.prototype.slice.call(arguments))
);
};
};
var _concat = function (eachfn, arr, fn, callback) {
var r = [];
eachfn(arr, function (x, cb) {
fn(x, function (err, y) {
r = r.concat(y || []);
cb(err);
});
}, function (err) {
callback(err, r);
});
};
async.concat = doParallel(_concat);
async.concatSeries = doSeries(_concat);
async.whilst = function (test, iterator, callback) {
if (test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.whilst(test, iterator, callback);
});
}
else {
callback();
}
};
async.doWhilst = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
if (test()) {
async.doWhilst(iterator, test, callback);
}
else {
callback();
}
});
};
async.until = function (test, iterator, callback) {
if (!test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.until(test, iterator, callback);
});
}
else {
callback();
}
};
async.doUntil = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
if (!test()) {
async.doUntil(iterator, test, callback);
}
else {
callback();
}
});
};
async.queue = function (worker, concurrency) {
if (concurrency === undefined) {
concurrency = 1;
}
function _insert(q, data, pos, callback) {
if(data.constructor !== Array) {
data = [data];
}
_each(data, function(task) {
var item = {
data: task,
callback: typeof callback === 'function' ? callback : null
};
if (pos) {
q.tasks.unshift(item);
} else {
q.tasks.push(item);
}
if (q.saturated && q.tasks.length === concurrency) {
q.saturated();
}
async.setImmediate(q.process);
});
}
var workers = 0;
var q = {
tasks: [],
concurrency: concurrency,
saturated: null,
empty: null,
drain: null,
push: function (data, callback) {
_insert(q, data, false, callback);
},
unshift: function (data, callback) {
_insert(q, data, true, callback);
},
process: function () {
if (workers < q.concurrency && q.tasks.length) {
var task = q.tasks.shift();
if (q.empty && q.tasks.length === 0) {
q.empty();
}
workers += 1;
var next = function () {
workers -= 1;
if (task.callback) {
task.callback.apply(task, arguments);
}
if (q.drain && q.tasks.length + workers === 0) {
q.drain();
}
q.process();
};
var cb = only_once(next);
worker(task.data, cb);
}
},
length: function () {
return q.tasks.length;
},
running: function () {
return workers;
}
};
return q;
};
async.cargo = function (worker, payload) {
var working = false,
tasks = [];
var cargo = {
tasks: tasks,
payload: payload,
saturated: null,
empty: null,
drain: null,
push: function (data, callback) {
if(data.constructor !== Array) {
data = [data];
}
_each(data, function(task) {
tasks.push({
data: task,
callback: typeof callback === 'function' ? callback : null
});
if (cargo.saturated && tasks.length === payload) {
cargo.saturated();
}
});
async.setImmediate(cargo.process);
},
process: function process() {
if (working) return;
if (tasks.length === 0) {
if(cargo.drain) cargo.drain();
return;
}
var ts = typeof payload === 'number'
? tasks.splice(0, payload)
: tasks.splice(0);
var ds = _map(ts, function (task) {
return task.data;
});
if(cargo.empty) cargo.empty();
working = true;
worker(ds, function () {
working = false;
var args = arguments;
_each(ts, function (data) {
if (data.callback) {
data.callback.apply(null, args);
}
});
process();
});
},
length: function () {
return tasks.length;
},
running: function () {
return working;
}
};
return cargo;
};
var _console_fn = function (name) {
return function (fn) {
var args = Array.prototype.slice.call(arguments, 1);
fn.apply(null, args.concat([function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (typeof console !== 'undefined') {
if (err) {
if (console.error) {
console.error(err);
}
}
else if (console[name]) {
_each(args, function (x) {
console[name](x);
});
}
}
}]));
};
};
async.log = _console_fn('log');
async.dir = _console_fn('dir');
/*async.info = _console_fn('info');
async.warn = _console_fn('warn');
async.error = _console_fn('error');*/
async.memoize = function (fn, hasher) {
var memo = {};
var queues = {};
hasher = hasher || function (x) {
return x;
};
var memoized = function () {
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
var key = hasher.apply(null, args);
if (key in memo) {
callback.apply(null, memo[key]);
}
else if (key in queues) {
queues[key].push(callback);
}
else {
queues[key] = [callback];
fn.apply(null, args.concat([function () {
memo[key] = arguments;
var q = queues[key];
delete queues[key];
for (var i = 0, l = q.length; i < l; i++) {
q[i].apply(null, arguments);
}
}]));
}
};
memoized.memo = memo;
memoized.unmemoized = fn;
return memoized;
};
async.unmemoize = function (fn) {
return function () {
return (fn.unmemoized || fn).apply(null, arguments);
};
};
async.times = function (count, iterator, callback) {
var counter = [];
for (var i = 0; i < count; i++) {
counter.push(i);
}
return async.map(counter, iterator, callback);
};
async.timesSeries = function (count, iterator, callback) {
var counter = [];
for (var i = 0; i < count; i++) {
counter.push(i);
}
return async.mapSeries(counter, iterator, callback);
};
async.compose = function (/* functions... */) {
var fns = Array.prototype.reverse.call(arguments);
return function () {
var that = this;
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
async.reduce(fns, args, function (newargs, fn, cb) {
fn.apply(that, newargs.concat([function () {
var err = arguments[0];
var nextargs = Array.prototype.slice.call(arguments, 1);
cb(err, nextargs);
}]))
},
function (err, results) {
callback.apply(that, [err].concat(results));
});
};
};
var _applyEach = function (eachfn, fns /*args...*/) {
var go = function () {
var that = this;
var args = Array.prototype.slice.call(arguments);
var callback = args.pop();
return eachfn(fns, function (fn, cb) {
fn.apply(that, args.concat([cb]));
},
callback);
};
if (arguments.length > 2) {
var args = Array.prototype.slice.call(arguments, 2);
return go.apply(this, args);
}
else {
return go;
}
};
async.applyEach = doParallel(_applyEach);
async.applyEachSeries = doSeries(_applyEach);
async.forever = function (fn, callback) {
function next(err) {
if (err) {
if (callback) {
return callback(err);
}
throw err;
}
fn(next);
}
next();
};
// AMD / RequireJS
if (typeof define !== 'undefined' && define.amd) {
define([], function () {
return async;
});
}
// Node.js
else if (typeof module !== 'undefined' && module.exports) {
module.exports = async;
}
// included directly via <script> tag
else {
root.async = async;
}
}());

View File

@ -62,7 +62,12 @@ define(function(require) {
FS_PENDING: 'PENDING', FS_PENDING: 'PENDING',
FS_ERROR: 'ERROR', FS_ERROR: 'ERROR',
SUPER_NODE_ID: '00000000-0000-0000-0000-000000000000' SUPER_NODE_ID: '00000000-0000-0000-0000-000000000000',
ENVIRONMENT: {
TMP: '/tmp',
PATH: ''
}
}; };
}); });

20
src/environment.js Normal file
View File

@ -0,0 +1,20 @@
define(function(require) {
var defaults = require('src/constants').ENVIRONMENT;
function Environment(env) {
env = env || {};
env.TMP = env.TMP || defaults.TMP;
env.PATH = env.PATH || defaults.PATH;
this.get = function(name) {
return env[name];
};
this.set = function(name, value) {
env[name] = value;
};
}
return Environment;
});

View File

@ -53,6 +53,7 @@ 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');
/* /*
* DirectoryEntry * DirectoryEntry
@ -2473,6 +2474,10 @@ define(function(require) {
callback(error); callback(error);
} }
}; };
FileSystem.prototype.Shell = function(options) {
return new Shell(this, options);
};
return FileSystem; return FileSystem;
}); });

View File

@ -5,6 +5,6 @@ define(function(require) {
return { return {
FileSystem: require('src/fs'), FileSystem: require('src/fs'),
Path: require('src/path') Path: require('src/path')
} };
}); });

View File

@ -1,26 +1,7 @@
define(function(require) { define(function(require) {
var FILE_SYSTEM_NAME = require('src/constants').FILE_SYSTEM_NAME; var FILE_SYSTEM_NAME = require('src/constants').FILE_SYSTEM_NAME;
// Based on https://github.com/caolan/async/blob/master/lib/async.js var asyncCallback = require('async').nextTick;
var nextTick = (function() {
if (typeof process === 'undefined' || !(process.nextTick)) {
if (typeof setImmediate === 'function') {
return function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
} else {
return function (fn) {
setTimeout(fn, 0);
};
}
}
return process.nextTick;
}());
function asyncCallback(callback) {
nextTick(callback);
}
function MemoryContext(db, readOnly) { function MemoryContext(db, readOnly) {
this.readOnly = readOnly; this.readOnly = readOnly;

358
src/shell.js Normal file
View File

@ -0,0 +1,358 @@
/* jshint evil:true */
define(function(require) {
var Path = require('src/path');
var FilerError = require('src/error');
var Environment = require('src/environment');
var async = require('async');
function Shell(fs, options) {
options = options || {};
var env = new Environment(options.env);
var cwd = '/';
/**
* The bound FileSystem (cannot be changed)
*/
Object.defineProperty(this, 'fs', {
get: function() { return fs; },
enumerable: true
});
/**
* The shell's environment (e.g., for things like
* path, tmp, and other env vars). Use env.get()
* and env.set() to work with variables.
*/
Object.defineProperty(this, 'env', {
get: function() { return env; },
enumerable: true
});
/**
* Change the current working directory. We
* include `cd` on the `this` vs. proto so that
* we can access cwd without exposing it externally.
*/
this.cd = function(path, callback) {
path = Path.resolve(this.cwd, path);
// Make sure the path actually exists, and is a dir
fs.stat(path, function(err, stats) {
if(err) {
callback(new FilerError.ENotDirectory());
return;
}
if(stats.type === 'DIRECTORY') {
cwd = path;
callback();
} else {
callback(new FilerError.ENotDirectory());
}
});
};
/**
* Get the current working directory (changed with `cd()`)
*/
this.pwd = function() {
return cwd;
};
}
/**
* Execute the .js command located at `path`. Such commands
* should assume the existence of 3 arguments, which will be
* defined at runtime:
*
* * fs - the current shell's bound filesystem object
* * args - a list of arguments for the command, or an empty list if none
* * callback - a callback function(error, result) to call when done.
*
* The .js command's contents should be the body of a function
* that looks like this:
*
* function(fs, args, callback) {
* // .js code here
* }
*/
Shell.prototype.exec = function(path, args, callback) {
var fs = this.fs;
if(typeof args === 'function') {
callback = args;
args = [];
}
args = args || [];
callback = callback || function(){};
path = Path.resolve(this.cwd, path);
fs.readFile(path, "utf8", function(error, data) {
if(error) {
callback(error);
return;
}
try {
var cmd = new Function('fs', 'args', 'callback', data);
cmd(fs, args, callback);
} catch(e) {
callback(e);
}
});
};
/**
* Create a file if it does not exist, or update access and
* modified times if it does. Valid options include:
*
* * updateOnly - whether to create the file if missing (defaults to false)
* * date - use the provided Date value instead of current date/time
*/
Shell.prototype.touch = function(path, options, callback) {
var fs = this.fs;
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){};
path = Path.resolve(this.cwd, path);
function createFile(path) {
fs.writeFile(path, '', callback);
}
function updateTimes(path) {
var now = Date.now();
var atime = options.date || now;
var mtime = options.date || now;
fs.utimes(path, atime, mtime, callback);
}
fs.stat(path, function(error, stats) {
if(error) {
if(options.updateOnly === true) {
callback();
} else {
createFile(path);
}
} else {
updateTimes(path);
}
});
};
/**
* Concatenate multiple files into a single String, with each
* file separated by a newline. The `files` argument should
* be a String (path to single file) or an Array of Strings
* (multiple file paths).
*/
Shell.prototype.cat = function(files, callback) {
var fs = this.fs;
var all = '';
callback = callback || function(){};
if(!files) {
callback(new Error("Missing files argument"));
return;
}
files = typeof files === 'string' ? [ files ] : files;
function append(item, callback) {
var filename = Path.resolve(this.cwd, item);
fs.readFile(filename, 'utf8', function(error, data) {
if(error) {
callback(error);
return;
}
all += data + '\n';
callback();
});
}
async.eachSeries(files, append, function(error) {
if(error) {
callback(error);
} else {
callback(null, all.replace(/\n$/, ''));
}
});
};
/**
* Get the listing of a directory, returning an array of
* file entries in the following form:
*
* {
* path: <String> the basename of the directory entry
* links: <Number> the number of links to the entry
* size: <Number> the size in bytes of the entry
* modified: <Number> the last modified date/time
* type: <String> the type of the entry
* contents: <Array> an optional array of child entries
* }
*
* By default ls() gives a shallow listing. If you want
* to follow directories as they are encountered, use
* the `recursive=true` option.
*/
Shell.prototype.ls = function(dir, options, callback) {
var fs = this.fs;
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){};
if(!dir) {
callback(new Error("Missing dir argument"));
return;
}
function list(path, callback) {
var pathname = Path.resolve(this.cwd, path);
var result = [];
fs.readdir(pathname, function(error, entries) {
if(error) {
callback(error);
return;
}
function getDirEntry(name, callback) {
name = Path.join(pathname, name);
fs.stat(name, function(error, stats) {
if(error) {
callback(error);
return;
}
var entry = {
path: Path.basename(name),
links: stats.nlinks,
size: stats.size,
modified: stats.mtime,
type: stats.type
};
if(options.recursive && stats.type === 'DIRECTORY') {
list(Path.join(pathname, entry.path), function(error, items) {
if(error) {
callback(error);
return;
}
entry.contents = items;
result.push(entry);
callback();
});
} else {
result.push(entry);
callback();
}
});
}
async.each(entries, getDirEntry, function(error) {
callback(error, result);
});
});
}
list(dir, callback);
};
/**
* Removes the file or directory at `path`. If `path` is a file
* it will be removed. If `path` is a directory, it will be
* removed if it is empty, otherwise the callback will receive
* an error. In order to remove non-empty directories, use the
* `recursive=true` option.
*/
Shell.prototype.rm = function(path, options, callback) {
var fs = this.fs;
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){};
if(!path) {
callback(new Error("Missing path argument"));
return;
}
function remove(pathname, callback) {
pathname = Path.resolve(this.cwd, pathname);
fs.stat(pathname, function(error, stats) {
if(error) {
callback(error);
return;
}
// If this is a file, delete it and we're done
if(stats.type === 'FILE') {
fs.unlink(pathname, callback);
return;
}
// If it's a dir, check if it's empty
fs.readdir(pathname, function(error, entries) {
if(error) {
callback(error);
return;
}
// If dir is empty, delete it and we're done
if(entries.length === 0) {
fs.rmdir(pathname, callback);
return;
}
// If not, see if we're allowed to delete recursively
if(!options.recursive) {
callback(new FilerError.ENotEmpty());
return;
}
// Remove each dir entry recursively, then delete the dir.
entries = entries.map(function(filename) {
// Root dir entries absolutely
return Path.join(pathname, filename);
});
async.each(entries, remove, function(error) {
if(error) {
callback(error);
return;
}
fs.rmdir(pathname, callback);
});
});
});
}
remove(path, callback);
};
/**
* Gets the path to the temporary directory, creating it if not
* present. The directory used is the one specified in
* env.TMP. The callback receives (error, tempDirName).
*/
Shell.prototype.tempDir = function(callback) {
var fs = this.fs;
var tmp = this.env.get('TMP');
callback = callback || function(){};
// Try and create it, and it will either work or fail
// but either way it's now there.
fs.mkdir(tmp, function(err) {
callback(null, tmp);
});
};
return Shell;
});

View File

@ -68,6 +68,10 @@ function(Filer, IndexedDBTestProvider, WebSQLTestProvider, MemoryTestProvider) {
return _provider; return _provider;
} }
function shell(options) {
return fs().Shell(options);
}
function cleanup(callback) { function cleanup(callback) {
if(!_provider) { if(!_provider) {
return; return;
@ -100,6 +104,7 @@ function(Filer, IndexedDBTestProvider, WebSQLTestProvider, MemoryTestProvider) {
uniqueName: uniqueName, uniqueName: uniqueName,
setup: setup, setup: setup,
fs: fs, fs: fs,
shell: shell,
provider: provider, provider: provider,
providers: { providers: {
IndexedDB: IndexedDBTestProvider, IndexedDB: IndexedDBTestProvider,

View File

@ -0,0 +1,82 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.cat', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.cat).to.be.a('function');
});
it('should fail when files argument is absent', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.cat(null, function(error, data) {
expect(error).to.exist;
expect(data).not.to.exist;
done();
});
});
it('should return the contents of a single file', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "file contents";
fs.writeFile('/file', contents, function(err) {
if(err) throw err;
shell.cat('/file', function(err, data) {
expect(err).not.to.exist;
expect(data).to.equal(contents);
done();
});
});
});
it('should return the contents of multiple files', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "file contents";
var contents2 = contents + '\n' + contents;
fs.writeFile('/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/file2', contents2, function(err) {
if(err) throw err;
shell.cat(['/file', '/file2'], function(err, data) {
expect(err).not.to.exist;
expect(data).to.equal(contents + '\n' + contents2);
done();
});
});
});
});
it('should fail if any of multiple file paths is invalid', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "file contents";
var contents2 = contents + '\n' + contents;
fs.writeFile('/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/file2', contents2, function(err) {
if(err) throw err;
shell.cat(['/file', '/nofile'], function(err, data) {
expect(err).to.exist;
expect(data).not.to.exist;
done();
});
});
});
});
});
});

123
tests/spec/shell/cd.spec.js Normal file
View File

@ -0,0 +1,123 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.cd', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.cd).to.be.a('function');
});
it('should default to a cwd of /', function() {
var shell = util.shell();
expect(shell.pwd()).to.equal('/');
});
it('should allow changing the path to a valid dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('/dir', function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/dir');
done();
});
});
});
it('should fail when changing the path to an invalid dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('/nodir', function(err) {
expect(err).to.exist;
expect(err.name).to.equal('ENotDirectory');
expect(shell.pwd()).to.equal('/');
done();
});
});
});
it('should fail when changing the path to a file', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.writeFile('/file', 'file', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('/file', function(err) {
expect(err).to.exist;
expect(err.name).to.equal('ENotDirectory');
expect(shell.pwd()).to.equal('/');
done();
});
});
});
it('should allow relative paths for a valid dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('./dir', function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/dir');
done();
});
});
});
it('should allow .. in paths for a valid dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('./dir', function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/dir');
shell.cd('..', function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/');
done();
});
});
});
});
it('should follow symlinks to dirs', function(done) {
var fs = util.fs();
fs.mkdir('/dir', function(error) {
if(error) throw error;
fs.symlink('/dir', '/link', function(error) {
if(error) throw error;
var shell = fs.Shell();
shell.cd('link', function(error) {
expect(error).not.to.exist;
expect(shell.pwd()).to.equal('/link');
done();
});
});
});
});
});
});

View File

@ -0,0 +1,112 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.env', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should get default env options', function() {
var shell = util.shell();
expect(shell.env).to.exist;
expect(shell.env.get('TMP')).to.equal('/tmp');
expect(shell.env.get('PATH')).to.equal('');
expect(shell.pwd()).to.equal('/');
});
it('should be able to specify env options', function() {
var options = {
env: {
TMP: '/tempdir',
PATH: '/dir'
}
};
var shell = util.shell(options);
expect(shell.env).to.exist;
expect(shell.env.get('TMP')).to.equal('/tempdir');
expect(shell.env.get('PATH')).to.equal('/dir');
expect(shell.pwd()).to.equal('/');
expect(shell.env.get('FOO')).not.to.exist;
shell.env.set('FOO', 1);
expect(shell.env.get('FOO')).to.equal(1);
});
it('should fail when dirs argument is absent', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.cat(null, function(error, list) {
expect(error).to.exist;
expect(list).not.to.exist;
done();
});
});
it('should give new value for shell.pwd() when cwd changes', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
expect(shell.pwd()).to.equal('/');
shell.cd('/dir', function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/dir');
done();
});
});
});
it('should create/return the default tmp dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
expect(shell.env.get('TMP')).to.equal('/tmp');
shell.tempDir(function(err, tmp) {
expect(err).not.to.exist;
shell.cd(tmp, function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/tmp');
done();
});
});
});
it('should create/return the tmp dir specified in env.TMP', function(done) {
var fs = util.fs();
var shell = fs.Shell({
env: {
TMP: '/tempdir'
}
});
expect(shell.env.get('TMP')).to.equal('/tempdir');
shell.tempDir(function(err, tmp) {
expect(err).not.to.exist;
shell.cd(tmp, function(err) {
expect(err).not.to.exist;
expect(shell.pwd()).to.equal('/tempdir');
done();
});
});
});
it('should allow repeated calls to tempDir()', function(done) {
var fs = util.fs();
var shell = fs.Shell();
expect(shell.env.get('TMP')).to.equal('/tmp');
shell.tempDir(function(err, tmp) {
expect(err).not.to.exist;
expect(tmp).to.equal('/tmp');
shell.tempDir(function(err, tmp) {
expect(err).not.to.exist;
expect(tmp).to.equal('/tmp');
done();
});
});
});
});
});

View File

@ -0,0 +1,33 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.exec', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.exec).to.be.a('function');
});
it('should be able to execute a command .js file from the filesystem', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var cmdString = "fs.writeFile(args[0], args[1], callback);";
fs.writeFile('/cmd.js', cmdString, function(error) {
if(error) throw error;
shell.exec('/cmd.js', ['/test', 'hello world'], function(error, result) {
if(error) throw error;
fs.readFile('/test', 'utf8', function(error, data) {
if(error) throw error;
expect(data).to.equal('hello world');
done();
});
});
});
});
});
});

186
tests/spec/shell/ls.spec.js Normal file
View File

@ -0,0 +1,186 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.ls', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.ls).to.be.a('function');
});
it('should fail when dirs argument is absent', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.cat(null, function(error, list) {
expect(error).to.exist;
expect(list).not.to.exist;
done();
});
});
it('should return the contents of a simple dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "a";
var contents2 = "bb";
fs.writeFile('/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/file2', contents2, function(err) {
if(err) throw err;
shell.ls('/', function(err, list) {
expect(err).not.to.exist;
expect(list.length).to.equal(2);
var item0 = list[0];
expect(item0.path).to.equal('file');
expect(item0.links).to.equal(1);
expect(item0.size).to.equal(1);
expect(item0.modified).to.be.a('number');
expect(item0.type).to.equal('FILE');
expect(item0.contents).not.to.exist;
var item1 = list[1];
expect(item1.path).to.equal('file2');
expect(item1.links).to.equal(1);
expect(item1.size).to.equal(2);
expect(item1.modified).to.be.a('number');
expect(item1.type).to.equal('FILE');
expect(item0.contents).not.to.exist;
done();
});
});
});
});
it('should return the shallow contents of a dir tree', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "a";
fs.mkdir('/dir', function(err) {
if(err) throw err;
fs.mkdir('/dir/dir2', function(err) {
if(err) throw err;
fs.writeFile('/dir/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/dir/file2', contents, function(err) {
if(err) throw err;
shell.ls('/dir', function(err, list) {
expect(err).not.to.exist;
expect(list.length).to.equal(3);
// We shouldn't rely on the order we'll get the listing
list.forEach(function(item, i, arr) {
switch(item.path) {
case 'dir2':
expect(item.links).to.equal(1);
expect(item.size).to.be.a('number');
expect(item.modified).to.be.a('number');
expect(item.type).to.equal('DIRECTORY');
expect(item.contents).not.to.exist;
break;
case 'file':
case 'file2':
expect(item.links).to.equal(1);
expect(item.size).to.equal(1);
expect(item.modified).to.be.a('number');
expect(item.type).to.equal('FILE');
expect(item.contents).not.to.exist;
break;
default:
// shouldn't happen
expect(true).to.be.false;
break;
}
if(i === arr.length -1) {
done();
}
});
});
});
});
});
});
});
it('should return the deep contents of a dir tree', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "a";
fs.mkdir('/dir', function(err) {
if(err) throw err;
fs.mkdir('/dir/dir2', function(err) {
if(err) throw err;
fs.writeFile('/dir/dir2/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/dir/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/dir/file2', contents, function(err) {
if(err) throw err;
shell.ls('/dir', { recursive: true }, function(err, list) {
expect(err).not.to.exist;
expect(list.length).to.equal(3);
// We shouldn't rely on the order we'll get the listing
list.forEach(function(item, i, arr) {
switch(item.path) {
case 'dir2':
expect(item.links).to.equal(1);
expect(item.size).to.be.a('number');
expect(item.modified).to.be.a('number');
expect(item.type).to.equal('DIRECTORY');
expect(item.contents).to.exist;
expect(item.contents.length).to.equal(1);
var contents0 = item.contents[0];
expect(contents0.path).to.equal('file');
expect(contents0.links).to.equal(1);
expect(contents0.size).to.equal(1);
expect(contents0.modified).to.be.a('number');
expect(contents0.type).to.equal('FILE');
expect(contents0.contents).not.to.exist;
break;
case 'file':
case 'file2':
expect(item.links).to.equal(1);
expect(item.size).to.equal(1);
expect(item.modified).to.be.a('number');
expect(item.type).to.equal('FILE');
expect(item.contents).not.to.exist;
break;
default:
// shouldn't happen
expect(true).to.be.false;
break;
}
if(i === arr.length -1) {
done();
}
});
});
});
});
});
});
});
});
});
});

137
tests/spec/shell/rm.spec.js Normal file
View File

@ -0,0 +1,137 @@
define(["Filer", "util"], function(Filer, util) {
describe('FileSystemShell.rm', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.rm).to.be.a('function');
});
it('should fail when path argument is absent', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.rm(null, function(error, list) {
expect(error).to.exist;
expect(list).not.to.exist;
done();
});
});
it('should remove a single file', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "a";
fs.writeFile('/file', contents, function(err) {
if(err) throw err;
shell.rm('/file', function(err) {
expect(err).not.to.exist;
fs.stat('/file', function(err, stats) {
expect(err).to.exist;
expect(stats).not.to.exist;
done();
});
});
});
});
it('should remove an empty dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
shell.rm('/dir', function(err) {
expect(err).not.to.exist;
fs.stat('/dir', function(err, stats) {
expect(err).to.exist;
expect(stats).not.to.exist;
done();
});
});
});
});
it('should fail to remove a non-empty dir', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
shell.touch('/dir/file', function(err) {
if(err) throw err;
shell.rm('/dir', function(err) {
expect(err).to.exist;
expect(err.name).to.equal('ENotEmpty');
done();
});
});
});
});
it('should remove a non-empty dir with option.recursive set', function(done) {
var fs = util.fs();
var shell = fs.Shell();
fs.mkdir('/dir', function(err) {
if(err) throw err;
shell.touch('/dir/file', function(err) {
if(err) throw err;
shell.rm('/dir', { recursive: true }, function(err) {
expect(err).not.to.exist;
fs.stat('/dir', function(err, stats) {
expect(err).to.exist;
expect(stats).not.to.exist;
done();
});
});
});
});
});
it('should work on a complex dir structure', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var contents = "a";
fs.mkdir('/dir', function(err) {
if(err) throw err;
fs.mkdir('/dir/dir2', function(err) {
if(err) throw err;
fs.writeFile('/dir/file', contents, function(err) {
if(err) throw err;
fs.writeFile('/dir/file2', contents, function(err) {
if(err) throw err;
shell.rm('/dir', { recursive: true }, function(err) {
expect(err).not.to.exist;
fs.stat('/dir', function(err, stats) {
expect(err).to.exist;
expect(stats).not.to.exist;
done();
});
});
});
});
});
});
});
});
});

View File

@ -0,0 +1,104 @@
define(["Filer", "util"], function(Filer, util) {
function getTimes(fs, path, callback) {
fs.stat(path, function(error, stats) {
if(error) throw error;
callback({mtime: stats.mtime, atime: stats.atime});
});
}
describe('FileSystemShell.touch', function() {
beforeEach(util.setup);
afterEach(util.cleanup);
it('should be a function', function() {
var shell = util.shell();
expect(shell.touch).to.be.a('function');
});
it('should create a new file if path does not exist', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.touch('/newfile', function(error) {
if(error) throw error;
fs.stat('/newfile', function(error, stats) {
expect(error).not.to.exist;
expect(stats.type).to.equal('FILE');
done();
});
});
});
it('should skip creating a new file if options.updateOnly is true', function(done) {
var fs = util.fs();
var shell = fs.Shell();
shell.touch('/newfile', { updateOnly: true }, function(error) {
if(error) throw error;
fs.stat('/newfile', function(error, stats) {
expect(error).to.exist;
done();
});
});
});
it('should update times if path does exist', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var atime = Date.parse('1 Oct 2000 15:33:22');
var mtime = Date.parse('30 Sep 2000 06:43:54');
fs.open('/newfile', 'w', function (error, fd) {
if (error) throw error;
fs.futimes(fd, atime, mtime, function (error) {
if(error) throw error;
fs.close(fd, function(error) {
if(error) throw error;
getTimes(fs, '/newfile', function(times1) {
shell.touch('/newfile', function(error) {
expect(error).not.to.exist;
getTimes(fs, '/newfile', function(times2) {
expect(times2.mtime).to.be.above(times1.mtime);
expect(times2.atime).to.be.above(times1.atime);
done();
});
});
});
});
});
});
});
it('should update times to specified date if path does exist', function(done) {
var fs = util.fs();
var shell = fs.Shell();
var date = Date.parse('1 Oct 2001 15:33:22');
fs.open('/newfile', 'w', function (error, fd) {
if (error) throw error;
fs.close(fd, function(error) {
if(error) throw error;
shell.touch('/newfile', { date: date }, function(error) {
expect(error).not.to.exist;
getTimes(fs, '/newfile', function(times) {
expect(times.mtime).to.equal(date);
expect(times.atime).to.equal(date);
done();
});
});
});
});
});
});
});

View File

@ -42,6 +42,15 @@ define([
"spec/adapters/adapters.spec", "spec/adapters/adapters.spec",
"spec/adapters/adapters.general.spec", "spec/adapters/adapters.general.spec",
// Filer.FileSystemShell.*
"spec/shell/cd.spec",
"spec/shell/touch.spec",
"spec/shell/exec.spec",
"spec/shell/cat.spec",
"spec/shell/ls.spec",
"spec/shell/rm.spec",
"spec/shell/env.spec",
// 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",