Modules (181)

FileSystemEntry

Description

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:

  1. Block external filesystem change events;
  2. Execute the low-level state-changing operation;
  3. Update the internal filesystem state, including caches;
  4. Apply the callback;
  5. Fire an appropriate internal change notification; and
  6. Unblock external change events.

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.

Dependencies

Variables

nextId

Counter to give every entry a unique id

    var nextId = 0;

Classes

Constructor

FileSystemEntry

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.

path string
The path for this entry.
fileSystem FileSystem
The file system associated with this entry.
    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"); }
        }
    });

Properties

Private

_fileSystem

Parent file system.

Type
!FileSystem
    FileSystemEntry.prototype._fileSystem = null;
Private

_isDirectory

Whether or not the entry is a directory

Type
boolean
    FileSystemEntry.prototype._isDirectory = false;
Private

_isFile

Whether or not the entry is a file

Type
boolean
    FileSystemEntry.prototype._isFile = false;
Private

_name

The name of this entry.

Type
string
    FileSystemEntry.prototype._name = null;
Private

_parentPath

The parent of this entry.

Type
string
    FileSystemEntry.prototype._parentPath = null;
Private

_path

The path of this entry.

Type
string
    FileSystemEntry.prototype._path = null;
Private

_stat

Cached stat object for this file.

Type
?FileSystemStats
    FileSystemEntry.prototype._stat = null;
Private

_watchedRoot

Cached copy of this entry's watched root

Type
entry: File, Directory, filter: function(FileSystemEntry):boolean, active: boolean
    FileSystemEntry.prototype._watchedRoot = undefined;
Private

_watchedRootFilterResult

Cached result of _watchedRoot.filter(this.name, this.parentPath).

Type
boolean
    FileSystemEntry.prototype._watchedRootFilterResult = undefined;

Methods

Private

_clearCachedData

Clear any cached data for this entry

    FileSystemEntry.prototype._clearCachedData = function () {
        this._stat = undefined;
    };
Private

_isWatched

Determines whether or not the entry is watched.

relaxed optional boolean
If falsey, the method will only return true if the watched root is fully active. If true, the method will return true if the watched root is either starting up or fully active.
Returns: boolean
    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;
    };
Private

_setPath

Update the path for this entry

newPath String
    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

_visitHelper

Private helper function for FileSystemEntry.visit that requires sanitized options.

stats FileSystemStats
the stats for this entry
visitedPaths {string: boolean}
the set of fullPaths that have already been visited
visitor function(FileSystemEntry): boolean
A visitor function, which is applied to descendent FileSystemEntry objects. If the function returns false for a particular Directory entry, that directory's descendents will not be visited.
options {maxDepth: number,maxEntriesCounter: {value: number},sortList: boolean}
callback optional function(?string)
Callback with single FileSystemError string parameter.
    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));
    };

exists

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.

callback function (?string,boolean)
Callback with a FileSystemError string or a boolean indicating whether or not the file exists.
    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));
    };

moveToTrash

Move this entry to the trash. If the underlying file system doesn't support move to trash, the item is permanently deleted.

callback optional function (?string)
Callback with a single FileSystemError string parameter.
    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

Rename this entry.

newFullPath string
New path & name for this entry.
callback optional function (?string)
Callback with a single FileSystemError string parameter.
    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));
    };

stat

Returns the stats for the entry.

callback function (?string,FileSystemStats=)
Callback with a FileSystemError string or FileSystemStats object.
    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));
    };

toString

Helpful toString for debugging purposes

    FileSystemEntry.prototype.toString = function () {
        return "[" + (this.isDirectory ? "Directory " : "File ") + this._path + "]";
    };

unlink

Permanently delete this entry. For Directories, this will delete the directory and all of its contents. For reversible delete, see moveToTrash().

callback optional function (?string)
Callback with a single FileSystemError string parameter.
    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

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.

visitor function(FileSystemEntry): boolean
A visitor function, which is applied to this entry and all descendent FileSystemEntry objects. If the function returns false for a particular Directory entry, that directory's descendents will not be visited.
options optional {maxDepth: number=, maxEntries: number=}
callback optional function(?string)
Callback with single FileSystemError string parameter.
    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;
});