To ensure cache coherence, current and future asynchronous state-changing operations of FileSystemEntry and its subclasses should implement the following high-level sequence of steps:
Note that because internal filesystem state is updated first, both the original caller and the change notification listeners observe filesystem state that is current w.r.t. the operation. Furthermore, because external change events are blocked before the operation begins, listeners will only receive the internal change event for the operation and not additional (or possibly inconsistent) external change events.
State-changing operations that block external filesystem change events must take care to always subsequently unblock the external change events in all control paths. It is safe to assume, however, that the underlying impl will always apply the callback with some value.
Caches should be conservative. Consequently, the entry's cached data should always be cleared if the underlying impl's operation fails. This is the case event for read-only operations because an unexpected failure implies that the system is in an unknown state. The entry should communicate this by failing where appropriate, and should not use the cache to hide failure.
Only watched entries should make use of cached data because change events are only expected for such entries, and change events are used to granularly invalidate out-of-date caches.
By convention, callbacks are optional for asynchronous, state-changing operations, but required for read-only operations. The first argument to the callback should always be a nullable error string from FileSystemError.
Model for a file system entry. This is the base class for File and Directory, and is never used directly.
See the File, Directory, and FileSystem classes for more details.
function FileSystemEntry(path, fileSystem) {
this._setPath(path);
this._fileSystem = fileSystem;
this._id = nextId++;
}
// Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters
Object.defineProperties(FileSystemEntry.prototype, {
"fullPath": {
get: function () { return this._path; },
set: function () { throw new Error("Cannot set fullPath"); }
},
"name": {
get: function () { return this._name; },
set: function () { throw new Error("Cannot set name"); }
},
"parentPath": {
get: function () { return this._parentPath; },
set: function () { throw new Error("Cannot set parentPath"); }
},
"id": {
get: function () { return this._id; },
set: function () { throw new Error("Cannot set id"); }
},
"isFile": {
get: function () { return this._isFile; },
set: function () { throw new Error("Cannot set isFile"); }
},
"isDirectory": {
get: function () { return this._isDirectory; },
set: function () { throw new Error("Cannot set isDirectory"); }
},
"_impl": {
get: function () { return this._fileSystem._impl; },
set: function () { throw new Error("Cannot set _impl"); }
}
});
Parent file system.
FileSystemEntry.prototype._fileSystem = null;
Whether or not the entry is a directory
FileSystemEntry.prototype._isDirectory = false;
Whether or not the entry is a file
FileSystemEntry.prototype._isFile = false;
The parent of this entry.
FileSystemEntry.prototype._parentPath = null;
Cached stat object for this file.
FileSystemEntry.prototype._stat = null;
Clear any cached data for this entry
FileSystemEntry.prototype._clearCachedData = function () {
this._stat = undefined;
};
Determines whether or not the entry is watched.
FileSystemEntry.prototype._isWatched = function (relaxed) {
var watchedRoot = this._watchedRoot,
filterResult = this._watchedRootFilterResult;
if (!watchedRoot) {
watchedRoot = this._fileSystem._findWatchedRootForPath(this._path);
if (watchedRoot) {
this._watchedRoot = watchedRoot;
if (watchedRoot.entry !== this) { // avoid creating entries for root's parent
var parentEntry = this._fileSystem.getDirectoryForPath(this._parentPath);
if (parentEntry._isWatched() === false) {
filterResult = false;
} else {
filterResult = watchedRoot.filter(this._name, this._parentPath);
}
} else { // root itself is watched
filterResult = true;
}
this._watchedRootFilterResult = filterResult;
}
}
if (watchedRoot) {
if (watchedRoot.status === WatchedRoot.ACTIVE ||
(relaxed && watchedRoot.status === WatchedRoot.STARTING)) {
return filterResult;
} else {
// We had a watched root, but it's no longer active, so it must now be invalid.
this._watchedRoot = undefined;
this._watchedRootFilterResult = false;
this._clearCachedData();
}
}
return false;
};
Update the path for this entry
FileSystemEntry.prototype._setPath = function (newPath) {
var parts = newPath.split("/");
if (this.isDirectory) {
parts.pop(); // Remove the empty string after last trailing "/"
}
this._name = parts[parts.length - 1];
parts.pop(); // Remove name
if (parts.length > 0) {
this._parentPath = parts.join("/") + "/";
} else {
// root directories have no parent path
this._parentPath = null;
}
this._path = newPath;
var watchedRoot = this._watchedRoot;
if (watchedRoot) {
if (newPath.indexOf(watchedRoot.entry.fullPath) === 0) {
// Update watchedRootFilterResult
this._watchedRootFilterResult = watchedRoot.filter(this._name, this._parentPath);
} else {
// The entry was moved outside of the watched root
this._watchedRoot = null;
this._watchedRootFilterResult = false;
}
}
};
Private helper function for FileSystemEntry.visit that requires sanitized options.
FileSystemEntry.prototype._visitHelper = function (stats, visitedPaths, visitor, options, callback) {
var maxDepth = options.maxDepth,
maxEntriesCounter = options.maxEntriesCounter,
sortList = options.sortList;
if (maxEntriesCounter.value-- <= 0 || maxDepth-- < 0) {
// The outer FileSystemEntry.visit call is responsible for applying
// the main callback to FileSystemError.TOO_MANY_FILES in this case
callback(null);
return;
}
if (this.isDirectory) {
var visitedPath = stats.realPath || this.fullPath;
if (visitedPaths.hasOwnProperty(visitedPath)) {
// Link cycle detected
callback(null);
return;
}
visitedPaths[visitedPath] = true;
}
if (!visitor(this) || this.isFile) {
callback(null);
return;
}
this.getContents(function (err, entries, entriesStats) {
if (err) {
callback(err);
return;
}
var counter = entries.length;
if (counter === 0) {
callback(null);
return;
}
function helperCallback(err) {
if (--counter === 0) {
callback(null);
}
}
var nextOptions = {
maxDepth: maxDepth,
maxEntriesCounter: maxEntriesCounter,
sortList : sortList
};
//sort entries if required
function compareFilesWithIndices(index1, index2) {
return entries[index1]._name.toLocaleLowerCase().localeCompare(entries[index2]._name.toLocaleLowerCase());
}
if (sortList) {
var fileIndexes = [], i = 0;
for (i = 0; i < entries.length; i++) {
fileIndexes[i] = i;
}
fileIndexes.sort(compareFilesWithIndices);
fileIndexes.forEach(function (fileIndex) {
var stats = entriesStats[fileIndexes[fileIndex]];
entries[fileIndexes[fileIndex]]._visitHelper(stats, visitedPaths, visitor, nextOptions, helperCallback);
});
} else {
entries.forEach(function (entry, index) {
var stats = entriesStats[index];
entry._visitHelper(stats, visitedPaths, visitor, nextOptions, helperCallback);
});
}
}.bind(this));
};
Check to see if the entry exists on disk. Note that there will NOT be an error returned if the file does not exist on the disk; in that case the error parameter will be null and the boolean will be false. The error parameter will only be truthy when an unexpected error was encountered during the test, in which case the state of the entry should be considered unknown.
FileSystemEntry.prototype.exists = function (callback) {
if (this._stat) {
callback(null, true);
return;
}
this._impl.exists(this._path, function (err, exists) {
if (err) {
this._clearCachedData();
callback(err);
return;
}
if (!exists) {
this._clearCachedData();
}
callback(null, exists);
}.bind(this));
};
Move this entry to the trash. If the underlying file system doesn't support move to trash, the item is permanently deleted.
FileSystemEntry.prototype.moveToTrash = function (callback) {
if (!this._impl.moveToTrash) {
this.unlink(callback);
return;
}
callback = callback || function () {};
// Block external change events until after the write has finished
this._fileSystem._beginChange();
this._clearCachedData();
this._impl.moveToTrash(this._path, function (err) {
var parent = this._fileSystem.getDirectoryForPath(this.parentPath);
// Update internal filesystem state
this._fileSystem._handleDirectoryChange(parent, function (added, removed) {
try {
// Notify the caller
callback(err);
} finally {
if (parent._isWatched()) {
// Notify change listeners
this._fileSystem._fireChangeEvent(parent, added, removed);
}
// Unblock external change events
this._fileSystem._endChange();
}
}.bind(this));
}.bind(this));
};
Rename this entry.
FileSystemEntry.prototype.rename = function (newFullPath, callback) {
callback = callback || function () {};
// Block external change events until after the write has finished
this._fileSystem._beginChange();
this._impl.rename(this._path, newFullPath, function (err) {
var oldFullPath = this._path;
try {
if (err) {
this._clearCachedData();
callback(err);
return;
}
// Update internal filesystem state
this._fileSystem._handleRename(this._path, newFullPath, this.isDirectory);
try {
// Notify the caller
callback(null);
} finally {
// Notify rename listeners
this._fileSystem._fireRenameEvent(oldFullPath, newFullPath);
}
} finally {
// Unblock external change events
this._fileSystem._endChange();
}
}.bind(this));
};
Returns the stats for the entry.
FileSystemEntry.prototype.stat = function (callback) {
if (this._stat) {
callback(null, this._stat);
return;
}
this._impl.stat(this._path, function (err, stat) {
if (err) {
this._clearCachedData();
callback(err);
return;
}
if (this._isWatched()) {
this._stat = stat;
}
callback(null, stat);
}.bind(this));
};
Helpful toString for debugging purposes
FileSystemEntry.prototype.toString = function () {
return "[" + (this.isDirectory ? "Directory " : "File ") + this._path + "]";
};
Permanently delete this entry. For Directories, this will delete the directory and all of its contents. For reversible delete, see moveToTrash().
FileSystemEntry.prototype.unlink = function (callback) {
callback = callback || function () {};
// Block external change events until after the write has finished
this._fileSystem._beginChange();
this._clearCachedData();
this._impl.unlink(this._path, function (err) {
var parent = this._fileSystem.getDirectoryForPath(this.parentPath);
// Update internal filesystem state
this._fileSystem._handleDirectoryChange(parent, function (added, removed) {
try {
// Notify the caller
callback(err);
} finally {
if (parent._isWatched()) {
// Notify change listeners
this._fileSystem._fireChangeEvent(parent, added, removed);
}
// Unblock external change events
this._fileSystem._endChange();
}
}.bind(this));
}.bind(this));
};
Visit this entry and its descendents with the supplied visitor function. Correctly handles symbolic link cycles and options can be provided to limit search depth and total number of entries visited. No particular traversal order is guaranteed; instead of relying on such an order, it is preferable to use the visit function to build a list of visited entries, sort those entries as desired, and then process them. Whenever possible, deep filesystem traversals should use this method.
FileSystemEntry.prototype.visit = function (visitor, options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
} else {
if (options === undefined) {
options = {};
}
callback = callback || function () {};
}
if (options.maxDepth === undefined) {
options.maxDepth = VISIT_DEFAULT_MAX_DEPTH;
}
if (options.maxEntries === undefined) {
options.maxEntries = VISIT_DEFAULT_MAX_ENTRIES;
}
options.maxEntriesCounter = { value: options.maxEntries };
this.stat(function (err, stats) {
if (err) {
callback(err);
return;
}
this._visitHelper(stats, {}, visitor, options, function (err) {
if (callback) {
if (err) {
callback(err);
return;
}
if (options.maxEntriesCounter.value < 0) {
callback(FileSystemError.TOO_MANY_ENTRIES);
return;
}
callback(null);
}
}.bind(this));
}.bind(this));
};
// Export this class
module.exports = FileSystemEntry;
});