2014-05-23 18:14:06 +00:00
|
|
|
var _ = require('../../lib/nodash.js');
|
|
|
|
|
|
|
|
var isNullPath = require('../path.js').isNull;
|
|
|
|
var nop = require('../shared.js').nop;
|
|
|
|
|
|
|
|
var Constants = require('../constants.js');
|
|
|
|
var FILE_SYSTEM_NAME = Constants.FILE_SYSTEM_NAME;
|
|
|
|
var FS_FORMAT = Constants.FS_FORMAT;
|
|
|
|
var FS_READY = Constants.FS_READY;
|
|
|
|
var FS_PENDING = Constants.FS_PENDING;
|
|
|
|
var FS_ERROR = Constants.FS_ERROR;
|
2014-06-02 20:44:20 +00:00
|
|
|
var FS_NODUPEIDCHECK = Constants.FS_NODUPEIDCHECK;
|
2014-05-23 18:14:06 +00:00
|
|
|
|
2014-05-23 18:36:23 +00:00
|
|
|
var providers = require('../providers/index.js');
|
2014-05-23 18:14:06 +00:00
|
|
|
|
|
|
|
var Shell = require('../shell/shell.js');
|
|
|
|
var Intercom = require('../../lib/intercom.js');
|
|
|
|
var FSWatcher = require('../fs-watcher.js');
|
|
|
|
var Errors = require('../errors.js');
|
2014-06-02 20:44:20 +00:00
|
|
|
var defaultGuidFn = require('../shared.js').guid;
|
2014-05-23 18:14:06 +00:00
|
|
|
|
|
|
|
var STDIN = Constants.STDIN;
|
|
|
|
var STDOUT = Constants.STDOUT;
|
|
|
|
var STDERR = Constants.STDERR;
|
|
|
|
var FIRST_DESCRIPTOR = Constants.FIRST_DESCRIPTOR;
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-06-02 20:44:20 +00:00
|
|
|
// The core fs operations live on impl
|
2014-05-23 18:14:06 +00:00
|
|
|
var impl = require('./implementation.js');
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// node.js supports a calling pattern that leaves off a callback.
|
|
|
|
function maybeCallback(callback) {
|
|
|
|
if(typeof callback === "function") {
|
|
|
|
return callback;
|
|
|
|
}
|
|
|
|
return function(err) {
|
|
|
|
if(err) {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2014-11-16 05:23:43 +00:00
|
|
|
// Default callback that logs an error if passed in
|
|
|
|
function defaultCallback(err) {
|
|
|
|
if(err) {
|
|
|
|
console.error('Filer error: ', err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
/**
|
|
|
|
* FileSystem
|
|
|
|
*
|
|
|
|
* A FileSystem takes an `options` object, which can specify a number of,
|
|
|
|
* options. All options are optional, and include:
|
|
|
|
*
|
|
|
|
* name: the name of the file system, defaults to "local"
|
|
|
|
*
|
|
|
|
* flags: one or more flags to use when creating/opening the file system.
|
|
|
|
* For example: "FORMAT" will cause the file system to be formatted.
|
|
|
|
* No explicit flags are set by default.
|
|
|
|
*
|
|
|
|
* provider: an explicit storage provider to use for the file
|
|
|
|
* system's database context provider. A number of context
|
|
|
|
* providers are included (see /src/providers), and users
|
|
|
|
* can write one of their own and pass it in to be used.
|
|
|
|
* By default an IndexedDB provider is used.
|
|
|
|
*
|
2014-06-02 20:44:20 +00:00
|
|
|
* guid: a function for generating unique IDs for nodes in the filesystem.
|
|
|
|
* Use this to override the built-in UUID generation. (Used mainly for tests).
|
|
|
|
*
|
2014-05-23 18:14:06 +00:00
|
|
|
* callback: a callback function to be executed when the file system becomes
|
|
|
|
* ready for use. Depending on the context provider used, this might
|
|
|
|
* be right away, or could take some time. The callback should expect
|
|
|
|
* an `error` argument, which will be null if everything worked. Also
|
|
|
|
* users should check the file system's `readyState` and `error`
|
|
|
|
* properties to make sure it is usable.
|
|
|
|
*/
|
|
|
|
function FileSystem(options, callback) {
|
|
|
|
options = options || {};
|
2014-11-16 05:23:43 +00:00
|
|
|
callback = callback || defaultCallback;
|
2014-04-17 23:22:20 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
var flags = options.flags;
|
2014-06-02 20:44:20 +00:00
|
|
|
var guid = options.guid ? options.guid : defaultGuidFn;
|
2014-05-23 18:14:06 +00:00
|
|
|
var provider = options.provider || new providers.Default(options.name || FILE_SYSTEM_NAME);
|
|
|
|
// If we're given a provider, match its name unless we get an explicit name
|
|
|
|
var name = options.name || provider.name;
|
|
|
|
var forceFormatting = _(flags).contains(FS_FORMAT);
|
|
|
|
|
|
|
|
var fs = this;
|
|
|
|
fs.readyState = FS_PENDING;
|
|
|
|
fs.name = name;
|
|
|
|
fs.error = null;
|
|
|
|
|
|
|
|
fs.stdin = STDIN;
|
|
|
|
fs.stdout = STDOUT;
|
|
|
|
fs.stderr = STDERR;
|
|
|
|
|
2014-10-24 18:19:17 +00:00
|
|
|
// Expose Shell constructor
|
|
|
|
this.Shell = Shell.bind(undefined, this);
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Safely expose the list of open files and file
|
|
|
|
// descriptor management functions
|
|
|
|
var openFiles = {};
|
|
|
|
var nextDescriptor = FIRST_DESCRIPTOR;
|
|
|
|
Object.defineProperty(this, "openFiles", {
|
|
|
|
get: function() { return openFiles; }
|
|
|
|
});
|
|
|
|
this.allocDescriptor = function(openFileDescription) {
|
|
|
|
var fd = nextDescriptor ++;
|
|
|
|
openFiles[fd] = openFileDescription;
|
|
|
|
return fd;
|
|
|
|
};
|
|
|
|
this.releaseDescriptor = function(fd) {
|
|
|
|
delete openFiles[fd];
|
|
|
|
};
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Safely expose the operation queue
|
|
|
|
var queue = [];
|
|
|
|
this.queueOrRun = function(operation) {
|
|
|
|
var error;
|
|
|
|
|
|
|
|
if(FS_READY == fs.readyState) {
|
|
|
|
operation.call(fs);
|
|
|
|
} else if(FS_ERROR == fs.readyState) {
|
|
|
|
error = new Errors.EFILESYSTEMERROR('unknown error');
|
|
|
|
} else {
|
|
|
|
queue.push(operation);
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
2014-05-23 18:14:06 +00:00
|
|
|
|
|
|
|
return error;
|
|
|
|
};
|
|
|
|
function runQueued() {
|
|
|
|
queue.forEach(function(operation) {
|
|
|
|
operation.call(this);
|
|
|
|
}.bind(fs));
|
|
|
|
queue = null;
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// 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 = {};
|
|
|
|
}
|
2014-03-18 18:16:12 +00:00
|
|
|
options = options || {};
|
2014-05-23 18:14:06 +00:00
|
|
|
listener = listener || nop;
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
var watcher = new FSWatcher();
|
|
|
|
watcher.start(filename, false, options.recursive);
|
|
|
|
watcher.on('change', listener);
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
return watcher;
|
|
|
|
};
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-06-02 20:44:20 +00:00
|
|
|
// Deal with various approaches to node ID creation
|
|
|
|
function wrappedGuidFn(context) {
|
|
|
|
return function(callback) {
|
|
|
|
// Skip the duplicate ID check if asked to
|
|
|
|
if(_(flags).contains(FS_NODUPEIDCHECK)) {
|
|
|
|
callback(null, guid());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise (default) make sure this id is unused first
|
|
|
|
function guidWithCheck(callback) {
|
|
|
|
var id = guid();
|
2014-08-16 20:22:41 +00:00
|
|
|
context.getObject(id, function(err, value) {
|
2014-06-02 20:44:20 +00:00
|
|
|
if(err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this id is unused, use it, otherwise find another
|
|
|
|
if(!value) {
|
|
|
|
callback(null, id);
|
|
|
|
} else {
|
|
|
|
guidWithCheck(callback);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
guidWithCheck(callback);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Let other instances (in this or other windows) know about
|
|
|
|
// any changes to this fs instance.
|
|
|
|
function broadcastChanges(changes) {
|
|
|
|
if(!changes.length) {
|
|
|
|
return;
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
2014-05-23 18:14:06 +00:00
|
|
|
var intercom = Intercom.getInstance();
|
|
|
|
changes.forEach(function(change) {
|
|
|
|
intercom.emit(change.event, change.path);
|
|
|
|
});
|
|
|
|
}
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Open file system storage provider
|
2014-08-21 00:53:01 +00:00
|
|
|
provider.open(function(err) {
|
2014-05-23 18:14:06 +00:00
|
|
|
function complete(error) {
|
|
|
|
function wrappedContext(methodName) {
|
|
|
|
var context = provider[methodName]();
|
|
|
|
context.flags = flags;
|
|
|
|
context.changes = [];
|
2014-06-02 20:44:20 +00:00
|
|
|
context.guid = wrappedGuidFn(context);
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// 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;
|
|
|
|
};
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
return context;
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// 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 = {
|
|
|
|
openReadWriteContext: function() {
|
|
|
|
return wrappedContext('getReadWriteContext');
|
|
|
|
},
|
|
|
|
openReadOnlyContext: function() {
|
|
|
|
return wrappedContext('getReadOnlyContext');
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
2014-05-23 18:14:06 +00:00
|
|
|
};
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
if(error) {
|
|
|
|
fs.readyState = FS_ERROR;
|
|
|
|
} else {
|
|
|
|
fs.readyState = FS_READY;
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
2014-08-18 18:08:54 +00:00
|
|
|
runQueued();
|
2014-05-23 18:14:06 +00:00
|
|
|
callback(error, fs);
|
|
|
|
}
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
if(err) {
|
|
|
|
return complete(err);
|
|
|
|
}
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
var context = provider.getReadWriteContext();
|
2014-06-02 20:44:20 +00:00
|
|
|
context.guid = wrappedGuidFn(context);
|
2014-08-21 00:53:01 +00:00
|
|
|
|
|
|
|
// Mount the filesystem, formatting if necessary
|
|
|
|
if(forceFormatting) {
|
|
|
|
// Wipe the storage provider, then write root block
|
|
|
|
context.clear(function(err) {
|
|
|
|
if(err) {
|
|
|
|
return complete(err);
|
|
|
|
}
|
|
|
|
impl.ensureRootDirectory(context, complete);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Use existing (or create new) root and mount
|
2014-07-07 21:02:42 +00:00
|
|
|
impl.ensureRootDirectory(context, complete);
|
2014-08-21 00:53:01 +00:00
|
|
|
}
|
2014-05-23 18:14:06 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expose storage providers on FileSystem constructor
|
|
|
|
FileSystem.providers = providers;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Public API for FileSystem
|
|
|
|
*/
|
|
|
|
[
|
|
|
|
'open',
|
|
|
|
'close',
|
|
|
|
'mknod',
|
|
|
|
'mkdir',
|
|
|
|
'rmdir',
|
|
|
|
'stat',
|
|
|
|
'fstat',
|
|
|
|
'link',
|
|
|
|
'unlink',
|
|
|
|
'read',
|
|
|
|
'readFile',
|
|
|
|
'write',
|
|
|
|
'writeFile',
|
|
|
|
'appendFile',
|
|
|
|
'exists',
|
|
|
|
'lseek',
|
|
|
|
'readdir',
|
|
|
|
'rename',
|
|
|
|
'readlink',
|
|
|
|
'symlink',
|
|
|
|
'lstat',
|
|
|
|
'truncate',
|
|
|
|
'ftruncate',
|
|
|
|
'utimes',
|
|
|
|
'futimes',
|
|
|
|
'setxattr',
|
|
|
|
'getxattr',
|
|
|
|
'fsetxattr',
|
|
|
|
'fgetxattr',
|
|
|
|
'removexattr',
|
|
|
|
'fremovexattr'
|
|
|
|
].forEach(function(methodName) {
|
|
|
|
FileSystem.prototype[methodName] = function() {
|
|
|
|
var fs = this;
|
|
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
|
|
var lastArgIndex = args.length - 1;
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// We may or may not get a callback, and since node.js supports
|
|
|
|
// fire-and-forget style fs operations, we have to dance a bit here.
|
|
|
|
var missingCallback = typeof args[lastArgIndex] !== 'function';
|
|
|
|
var callback = maybeCallback(args[lastArgIndex]);
|
2014-03-18 20:34:47 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
var error = fs.queueOrRun(function() {
|
|
|
|
var context = fs.provider.openReadWriteContext();
|
2014-03-18 20:34:47 +00:00
|
|
|
|
2014-08-18 18:08:54 +00:00
|
|
|
// Fail early if the filesystem is in an error state (e.g.,
|
|
|
|
// provider failed to open.
|
|
|
|
if(FS_ERROR === fs.readyState) {
|
|
|
|
var err = new Errors.EFILESYSTEMERROR('filesystem unavailable, operation canceled');
|
|
|
|
return callback.call(fs, err);
|
|
|
|
}
|
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Wrap the callback so we can explicitly close the context
|
|
|
|
function complete() {
|
|
|
|
context.close();
|
|
|
|
callback.apply(fs, arguments);
|
2014-03-18 18:16:12 +00:00
|
|
|
}
|
2014-03-18 20:34:47 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
// Either add or replace the callback with our wrapper complete()
|
|
|
|
if(missingCallback) {
|
|
|
|
args.push(complete);
|
|
|
|
} else {
|
|
|
|
args[lastArgIndex] = complete;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Forward this call to the impl's version, using the following
|
|
|
|
// call signature, with complete() as the callback/last-arg now:
|
|
|
|
// fn(fs, context, arg0, arg1, ... , complete);
|
|
|
|
var fnArgs = [fs, context].concat(args);
|
|
|
|
impl[methodName].apply(null, fnArgs);
|
|
|
|
});
|
|
|
|
if(error) {
|
|
|
|
callback(error);
|
|
|
|
}
|
2014-03-18 18:16:12 +00:00
|
|
|
};
|
2014-05-23 18:14:06 +00:00
|
|
|
});
|
2014-03-18 18:16:12 +00:00
|
|
|
|
2014-05-23 18:14:06 +00:00
|
|
|
module.exports = FileSystem;
|