/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.6
src/filesystem/FileSystemEntry.js
598 строк
21 KB
ficristo
ESLint: enabled no-trailing-spaces and eol-last rules
12 дек 2015, 01:02
12 дек 2015, 01:02
ef6048c
Код
Авторство
О чём код?
/* * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define */ /* * 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. */ define(function (require, exports, module) { "use strict"; var FileSystemError = require("filesystem/FileSystemError"), WatchedRoot = require("filesystem/WatchedRoot"); var VISIT_DEFAULT_MAX_DEPTH = 100, VISIT_DEFAULT_MAX_ENTRIES = 30000; /* Counter to give every entry a unique id */ var nextId = 0; /** * 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. * * @constructor * @param {string} path The path for this entry. * @param {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"); } } }); /** * Cached stat object for this file. * @type {?FileSystemStats} */ FileSystemEntry.prototype._stat = null; /** * Parent file system. * @type {!FileSystem} */ FileSystemEntry.prototype._fileSystem = null; /** * The path of this entry. * @type {string} */ FileSystemEntry.prototype._path = null; /** * The name of this entry. * @type {string} */ FileSystemEntry.prototype._name = null; /** * The parent of this entry. * @type {string} */ FileSystemEntry.prototype._parentPath = null; /** * Whether or not the entry is a file * @type {boolean} */ FileSystemEntry.prototype._isFile = false; /** * Whether or not the entry is a directory * @type {boolean} */ FileSystemEntry.prototype._isDirectory = false; /** * Cached copy of this entry's watched root * @type {entry: File|Directory, filter: function(FileSystemEntry):boolean, active: boolean} */ FileSystemEntry.prototype._watchedRoot = undefined; /** * Cached result of _watchedRoot.filter(this.name, this.parentPath). * @type {boolean} */ FileSystemEntry.prototype._watchedRootFilterResult = undefined; /** * Determines whether or not the entry is watched. * @param {boolean=} relaxed 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. * @return {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; }; /** * Update the path for this entry * @private * @param {String} newPath */ 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; } } }; /** * Clear any cached data for this entry * @private */ FileSystemEntry.prototype._clearCachedData = function () { this._stat = undefined; }; /** * Helpful toString for debugging purposes */ FileSystemEntry.prototype.toString = function () { return "[" + (this.isDirectory ? "Directory " : "File ") + this._path + "]"; }; /** * 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. * * @param {function (?string, boolean)} callback 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)); }; /** * Returns the stats for the entry. * * @param {function (?string, FileSystemStats=)} callback 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)); }; /** * Rename this entry. * * @param {string} newFullPath New path & name for this entry. * @param {function (?string)=} callback 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)); }; /** * Permanently delete this entry. For Directories, this will delete the directory * and all of its contents. For reversible delete, see moveToTrash(). * * @param {function (?string)=} callback 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)); }; /** * Move this entry to the trash. If the underlying file system doesn't support move * to trash, the item is permanently deleted. * * @param {function (?string)=} callback 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)); }; /** * Private helper function for FileSystemEntry.visit that requires sanitized options. * * @private * @param {FileSystemStats} stats - the stats for this entry * @param {{string: boolean}} visitedPaths - the set of fullPaths that have already been visited * @param {function(FileSystemEntry): boolean} visitor - 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. * @param {{maxDepth: number, maxEntriesCounter: {value: number}, sortList: boolean}} options * @param {function(?string)=} callback 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)); }; /** * 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. * * @param {function(FileSystemEntry): boolean} visitor - 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. * @param {{maxDepth: number=, maxEntries: number=}=} options * @param {function(?string)=} callback 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; });