var FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes
Callback to notify FileSystem of watcher changes
var _changeCallback;
Timeout used to batch up file watcher changes (setTimeout() return value)
var _changeTimeout;
Callback to notify FileSystem if watchers stop working entirely
var _offlineCallback;
Pending file watcher changes - map from fullPath to flag indicating whether we need to pass stats to _changeCallback() for this path.
var _pendingChanges = {};
var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(),
_modulePath = FileUtils.getNativeModuleDirectoryPath(module),
_nodePath = "node/FileWatcherDomain",
_domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"),
_nodeDomain = new NodeDomain("fileWatcher", _domainPath);
var _isRunningOnWindowsXP = window.navigator.userAgent.indexOf("Windows NT 5.") >= 0;
// If the connection closes, notify the FileSystem that watchers have gone offline.
_nodeDomain.connection.on("close", function (event, promise) {
if (_offlineCallback) {
_offlineCallback();
}
});
Enqueue a file change event for eventual reporting back to the FileSystem.
function _enqueueChange(changedPath, stats) {
_pendingChanges[changedPath] = stats;
if (!_changeTimeout) {
_changeTimeout = window.setTimeout(function () {
if (_changeCallback) {
Object.keys(_pendingChanges).forEach(function (path) {
_changeCallback(path, _pendingChanges[path]);
});
}
_changeTimeout = null;
_pendingChanges = {};
}, FILE_WATCHER_BATCH_TIMEOUT);
}
}
Event handler for the Node fileWatcher domain's change event.
function _fileWatcherChange(evt, event, parentDirPath, entryName, statsObj) {
var change;
switch (event) {
case "changed":
// an existing file/directory was modified; stats are passed if available
var fsStats;
if (statsObj) {
fsStats = new FileSystemStats(statsObj);
} else {
console.warn("FileWatcherDomain was expected to deliver stats for changed event!");
}
_enqueueChange(parentDirPath + entryName, fsStats);
break;
case "created":
case "deleted":
// file/directory was created/deleted; fire change on parent to reload contents
_enqueueChange(parentDirPath, null);
break;
default:
console.error("Unexpected 'change' event:", event);
}
}
// Setup the change handler. This only needs to happen once.
_nodeDomain.on("change", _fileWatcherChange);
Convert appshell error codes to FileSystemError values.
function _mapError(err) {
if (!err) {
return null;
}
switch (err) {
case appshell.fs.ERR_INVALID_PARAMS:
return FileSystemError.INVALID_PARAMS;
case appshell.fs.ERR_NOT_FOUND:
return FileSystemError.NOT_FOUND;
case appshell.fs.ERR_CANT_READ:
return FileSystemError.NOT_READABLE;
case appshell.fs.ERR_CANT_WRITE:
return FileSystemError.NOT_WRITABLE;
case appshell.fs.ERR_UNSUPPORTED_ENCODING:
return FileSystemError.UNSUPPORTED_ENCODING;
case appshell.fs.ERR_OUT_OF_SPACE:
return FileSystemError.OUT_OF_SPACE;
case appshell.fs.ERR_FILE_EXISTS:
return FileSystemError.ALREADY_EXISTS;
case appshell.fs.ERR_ENCODE_FILE_FAILED:
return FileSystemError.ENCODE_FILE_FAILED;
case appshell.fs.ERR_DECODE_FILE_FAILED:
return FileSystemError.DECODE_FILE_FAILED;
case appshell.fs.ERR_UNSUPPORTED_UTF16_ENCODING:
return FileSystemError.UNSUPPORTED_UTF16_ENCODING;
}
return FileSystemError.UNKNOWN;
}
Convert a callback to one that transforms its first parameter from an appshell error code to a FileSystemError string.
function _wrap(cb) {
return function (err) {
var args = Array.prototype.slice.call(arguments);
args[0] = _mapError(args[0]);
cb.apply(null, args);
};
}
Determine whether a file or directory exists at the given path by calling back asynchronously with either a FileSystemError string or a boolean, which is true if the file exists and false otherwise. The error will never be FileSystemError.NOT_FOUND; in that case, there will be no error and the boolean parameter will be false.
function exists(path, callback) {
stat(path, function (err) {
if (err) {
if (err === FileSystemError.NOT_FOUND) {
callback(null, false);
} else {
callback(err);
}
return;
}
callback(null, true);
});
}
Initialize file watching for this filesystem, using the supplied changeCallback to provide change notifications. The first parameter of changeCallback specifies the changed path (either a file or a directory); if this parameter is null, it indicates that the implementation cannot specify a particular changed path, and so the callers should consider all paths to have changed and to update their state accordingly. The second parameter to changeCallback is an optional FileSystemStats object that may be provided in case the changed path already exists and stats are readily available. The offlineCallback will be called in case watchers are no longer expected to function properly. All watched paths are cleared when the offlineCallback is called.
function initWatchers(changeCallback, offlineCallback) {
_changeCallback = changeCallback;
_offlineCallback = offlineCallback;
if (_isRunningOnWindowsXP && _offlineCallback) {
_offlineCallback();
}
}
Create a directory at the given path, and call back asynchronously with either a FileSystemError string or a stats object for the newly created directory. The octal mode parameter is optional; if unspecified, the mode of the created directory is implementation dependent.
function mkdir(path, mode, callback) {
if (typeof mode === "function") {
callback = mode;
mode = parseInt("0755", 8);
}
appshell.fs.makedir(path, mode, function (err) {
if (err) {
callback(_mapError(err));
} else {
stat(path, function (err, stat) {
callback(err, stat);
});
}
});
}
Move the file or directory at the given path to a system dependent trash location, calling back asynchronously with a possibly null FileSystemError string. Directories will be moved even when non-empty.
function moveToTrash(path, callback) {
appshell.fs.moveToTrash(path, function (err) {
callback(_mapError(err));
});
}
Read the contents of the file at the given path, calling back asynchronously with either a FileSystemError string, or with the data and the FileSystemStats object associated with the read file. The options parameter can be used to specify an encoding (default "utf8"), and also a cached stats object that the implementation is free to use in order to avoid an additional stat call.
Note: if either the read or the stat call fails then neither the read data nor stat will be passed back, and the call should be considered to have failed. If both calls fail, the error from the read call is passed back.
function readFile(path, options, callback) {
var encoding = options.encoding || "utf8";
// callback to be executed when the call to stat completes
// or immediately if a stat object was passed as an argument
function doReadFile(stat) {
if (stat.size > (FileUtils.MAX_FILE_SIZE)) {
callback(FileSystemError.EXCEEDS_MAX_FILE_SIZE);
} else {
appshell.fs.readFile(path, encoding, function (_err, _data, encoding, preserveBOM) {
if (_err) {
callback(_mapError(_err));
} else {
callback(null, _data, encoding, preserveBOM, stat);
}
});
}
}
if (options.stat) {
doReadFile(options.stat);
} else {
exports.stat(path, function (_err, _stat) {
if (_err) {
callback(_err);
} else {
doReadFile(_stat);
}
});
}
}
Read the contents of the directory at the given path, calling back asynchronously either with a FileSystemError string or an array of FileSystemEntry objects along with another consistent array, each index of which either contains a FileSystemStats object for the corresponding FileSystemEntry object in the second parameter or a FileSystemError string describing a stat error.
function readdir(path, callback) {
appshell.fs.readdir(path, function (err, contents) {
if (err) {
callback(_mapError(err));
return;
}
var count = contents.length;
if (!count) {
callback(null, [], []);
return;
}
var stats = [];
contents.forEach(function (val, idx) {
stat(path + "/" + val, function (err, stat) {
stats[idx] = err || stat;
count--;
if (count <= 0) {
callback(null, contents, stats);
}
});
});
});
}
Rename the file or directory at oldPath to newPath, and call back asynchronously with a possibly null FileSystemError string.
function rename(oldPath, newPath, callback) {
appshell.fs.rename(oldPath, newPath, _wrap(callback));
}
Display an open-files dialog to the user and call back asynchronously with either a FileSystmError string or an array of path strings, which indicate the entry or entries selected.
function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) {
appshell.fs.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, _wrap(callback));
}
Display a save-file dialog and call back asynchronously with either a FileSystemError string or the path to which the user has chosen to save the file. If the dialog is cancelled, the path string will be empty.
function showSaveDialog(title, initialPath, proposedNewFilename, callback) {
appshell.fs.showSaveDialog(title, initialPath, proposedNewFilename, _wrap(callback));
}
Stat the file or directory at the given path, calling back asynchronously with either a FileSystemError string or the entry's associated FileSystemStats object.
function stat(path, callback) {
appshell.fs.stat(path, function (err, stats) {
if (err) {
callback(_mapError(err));
} else {
var options = {
isFile: stats.isFile(),
mtime: stats.mtime,
size: stats.size,
realPath: stats.realPath,
hash: stats.mtime.getTime()
};
var fsStats = new FileSystemStats(options);
callback(null, fsStats);
}
});
}
Unlink (i.e., permanently delete) the file or directory at the given path, calling back asynchronously with a possibly null FileSystemError string. Directories will be unlinked even when non-empty.
function unlink(path, callback) {
appshell.fs.unlink(path, function (err) {
callback(_mapError(err));
});
}
Stop providing change notifications for all previously watched files and directories, optionally calling back asynchronously with a possibly null FileSystemError string when the operation is complete.
function unwatchAll(callback) {
_nodeDomain.exec("unwatchAll")
.then(callback, callback);
}
// Export public API
exports.showOpenDialog = showOpenDialog;
exports.showSaveDialog = showSaveDialog;
exports.exists = exists;
exports.readdir = readdir;
exports.mkdir = mkdir;
exports.rename = rename;
exports.stat = stat;
exports.readFile = readFile;
exports.writeFile = writeFile;
exports.unlink = unlink;
exports.moveToTrash = moveToTrash;
exports.initWatchers = initWatchers;
exports.watchPath = watchPath;
exports.unwatchPath = unwatchPath;
exports.unwatchAll = unwatchAll;
Stop providing change notifications for the file or directory at the given path, calling back asynchronously with a possibly null FileSystemError string when the operation is complete. This function needs to mirror the signature of watchPath because of FileSystem.prototype._watchOrUnwatchEntry implementation.
function unwatchPath(path, ignored, callback) {
_nodeDomain.exec("unwatchPath", path)
.then(callback, callback);
}
Start providing change notifications for the file or directory at the given path, calling back asynchronously with a possibly null FileSystemError string when the initialization is complete. Notifications are provided using the changeCallback function provided by the initWatchers method. Note that change notifications are only provided recursively for directories when the recursiveWatch property of this module is true.
function watchPath(path, ignored, callback) {
if (_isRunningOnWindowsXP) {
callback(FileSystemError.NOT_SUPPORTED);
return;
}
appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) {
if (err || isNetworkDrive) {
if (isNetworkDrive) {
callback(FileSystemError.NETWORK_DRIVE_NOT_SUPPORTED);
} else {
callback(FileSystemError.UNKNOWN);
}
return;
}
_nodeDomain.exec("watchPath", path, ignored)
.then(callback, callback);
});
}
Write data to the file at the given path, calling back asynchronously with either a FileSystemError string or the FileSystemStats object associated with the written file and a boolean that indicates whether the file was created by the write (true) or not (false). If no file exists at the given path, a new file will be created. The options parameter can be used to specify an encoding (default "utf8"), an octal mode (default unspecified and implementation dependent), and a consistency hash, which is used to the current state of the file before overwriting it. If a consistency hash is provided but does not match the hash of the file on disk, a FileSystemError.CONTENTS_MODIFIED error is passed to the callback.
function writeFile(path, data, options, callback) {
var encoding = options.encoding || "utf8",
preserveBOM = options.preserveBOM;
function _finishWrite(created) {
appshell.fs.writeFile(path, data, encoding, preserveBOM, function (err) {
if (err) {
callback(_mapError(err));
} else {
stat(path, function (err, stat) {
callback(err, stat, created);
});
}
});
}
stat(path, function (err, stats) {
if (err) {
switch (err) {
case FileSystemError.NOT_FOUND:
_finishWrite(true);
break;
default:
callback(err);
}
return;
}
if (options.hasOwnProperty("expectedHash") && options.expectedHash !== stats._hash) {
console.error("Blind write attempted: ", path, stats._hash, options.expectedHash);
if (options.hasOwnProperty("expectedContents")) {
appshell.fs.readFile(path, encoding, function (_err, _data) {
if (_err || _data !== options.expectedContents) {
callback(FileSystemError.CONTENTS_MODIFIED);
return;
}
_finishWrite(false);
});
return;
} else {
callback(FileSystemError.CONTENTS_MODIFIED);
return;
}
}
_finishWrite(false);
});
}