src/filesystem/FileSystem.js
/*
* Copyright (c) 2013 - present 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.
*
*/
/**
* FileSystem is a model object representing a complete file system. This object creates
* and manages File and Directory instances, dispatches events when the file system changes,
* and provides methods for showing 'open' and 'save' dialogs.
*
* FileSystem automatically initializes when loaded. It depends on a pluggable "impl" layer, which
* it loads itself but must be designated in the require.config() that loads FileSystem. For details
* see: https://github.com/adobe/brackets/wiki/File-System-Implementations
*
* There are three ways to get File or Directory instances:
* * Use FileSystem.resolve() to convert a path to a File/Directory object. This will only
* succeed if the file/directory already exists.
* * Use FileSystem.getFileForPath()/FileSystem.getDirectoryForPath() if you know the
* file/directory already exists, or if you want to create a new entry.
* * Use Directory.getContents() to return all entries for the specified Directory.
*
* All paths passed *to* FileSystem APIs must be in the following format:
* * The path separator is "/" regardless of platform
* * Paths begin with "/" on Mac/Linux and "c:/" (or some other drive letter) on Windows
*
* All paths returned *from* FileSystem APIs additionally meet the following guarantees:
* * No ".." segments
* * No consecutive "/"s
* * Paths to a directory always end with a trailing "/"
* (Because FileSystem normalizes paths automatically, paths passed *to* FileSystem do not need
* to meet these requirements)
*
* FileSystem dispatches the following events:
* (NOTE: attach to these events via `FileSystem.on()` - not `$(FileSystem).on()`)
*
* __change__ - Sent whenever there is a change in the file system. The handler
* is passed up to three arguments: the changed entry and, if that changed entry
* is a Directory, a list of entries added to the directory and a list of entries
* removed from the Directory. The entry argument can be:
* * a File - the contents of the file have changed, and should be reloaded.
* * a Directory - an immediate child of the directory has been added, removed,
* or renamed/moved. Not triggered for "grandchildren".
* - If the added & removed arguments are null, we don't know what was added/removed:
* clients should assume the whole subtree may have changed.
* - If the added & removed arguments are 0-length, there's no net change in the set
* of files but a file may have been replaced: clients should assume the contents
* of any immediate child file may have changed.
* * null - a 'wholesale' change happened, and you should assume everything may
* have changed.
* For changes made externally, there may be a significant delay before a "change" event
* is dispatched.
*
* __rename__ - Sent whenever a File or Directory is renamed. All affected File and Directory
* objects have been updated to reflect the new path by the time this event is dispatched.
* This event should be used to trigger any UI updates that may need to occur when a path
* has changed. Note that these events will only be sent for rename operations that happen
* within the filesystem. If a file is renamed externally, a change event on the parent
* directory will be sent instead.
*
* FileSystem may perform caching. But it guarantees:
* * File contents & metadata - reads are guaranteed to be up to date (cached data is not used
* without first veryifying it is up to date).
* * Directory structure / file listing - reads may return cached data immediately, which may not
* reflect external changes made recently. (However, changes made via FileSystem itself are always
* reflected immediately, as soon as the change operation's callback signals success).
*
* The FileSystem doesn't directly read or write contents--this work is done by a low-level
* implementation object. This allows client code to use the FileSystem API without having to
* worry about the underlying storage, which could be a local filesystem or a remote server.
*/
define(function (require, exports, module) {
"use strict";
var Directory = require("filesystem/Directory"),
File = require("filesystem/File"),
FileIndex = require("filesystem/FileIndex"),
FileSystemError = require("filesystem/FileSystemError"),
WatchedRoot = require("filesystem/WatchedRoot"),
EventDispatcher = require("utils/EventDispatcher"),
PathUtils = require("thirdparty/path-utils/path-utils"),
_ = require("thirdparty/lodash");
// Collection of registered protocol adapters
var _fileProtocolPlugins = {};
/**
* Typical signature of a file protocol adapter.
* @typedef {Object} FileProtocol~Adapter
* @property {Number} priority - Indicates the priority.
* @property {Object} fileImpl - Handle for the custom file implementation prototype.
* @property {function} canRead - To check if this impl can read a file for a given path.
*/
/**
* FileSystem hook to register file protocol adapter
* @param {string} protocol ex: "https:"|"http:"|"ftp:"|"file:"
* @param {...FileProtocol~Adapter} adapter wrapper over file implementation
*/
function registerProtocolAdapter(protocol, adapter) {
var adapters;
if (protocol) {
adapters = _fileProtocolPlugins[protocol] || [];
adapters.push(adapter);
// We will keep a sorted adapter list on 'priority'
// If priority is not provided a default of '0' is assumed
adapters.sort(function (a, b) {
return (b.priority || 0) - (a.priority || 0);
});
_fileProtocolPlugins[protocol] = adapters;
}
}
/**
* @param {string} protocol ex: "https:"|"http:"|"ftp:"|"file:"
* @param {string} filePath fullPath of the file
* @return adapter adapter wrapper over file implementation
*/
function _getProtocolAdapter(protocol, filePath) {
var protocolAdapters = _fileProtocolPlugins[protocol] || [],
selectedAdapter;
// Find the fisrt compatible adapter having highest priority
_.forEach(protocolAdapters, function (adapter) {
if (adapter.canRead && adapter.canRead(filePath)) {
selectedAdapter = adapter;
// Break at first compatible adapter
return false;
}
});
return selectedAdapter;
}
/**
* The FileSystem is not usable until init() signals its callback.
* @constructor
*/
function FileSystem() {
// Create a file index
this._index = new FileIndex();
// Initialize the set of watched roots
this._watchedRoots = {};
// Initialize the watch/unwatch request queue
this._watchRequests = [];
// Initialize the queue of pending external changes
this._externalChanges = [];
}
EventDispatcher.makeEventDispatcher(FileSystem.prototype);
/**
* The low-level file system implementation used by this object.
* This is set in the init() function and cannot be changed.
*/
FileSystem.prototype._impl = null;
/**
* The FileIndex used by this object. This is initialized in the constructor.
*/
FileSystem.prototype._index = null;
/**
* Refcount of any pending filesystem mutation operations (e.g., writes,
* unlinks, etc.). Used to ensure that external change events aren't processed
* until after index fixups, operation-specific callbacks, and internal change
* events are complete. (This is important for distinguishing rename from
* an unrelated delete-add pair).
* @type {number}
*/
FileSystem.prototype._activeChangeCount = 0;
// For unit testing only
FileSystem.prototype._getActiveChangeCount = function () {
return this._activeChangeCount;
};
/**
* Queue of arguments with which to invoke _handleExternalChanges(); triggered
* once _activeChangeCount drops to zero.
* @type {!Array.<{path:?string, stat:FileSystemStats=}>}
*/
FileSystem.prototype._externalChanges = null;
/** Process all queued watcher results, by calling _handleExternalChange() on each */
FileSystem.prototype._triggerExternalChangesNow = function () {
this._externalChanges.forEach(function (info) {
this._handleExternalChange(info.path, info.stat);
}, this);
this._externalChanges.length = 0;
};
/**
* Receives a result from the impl's watcher callback, and either processes it
* immediately (if _activeChangeCount is 0) or otherwise stores it for later
* processing.
* @param {?string} path The fullPath of the changed entry
* @param {FileSystemStats=} stat An optional stat object for the changed entry
*/
FileSystem.prototype._enqueueExternalChange = function (path, stat) {
this._externalChanges.push({path: path, stat: stat});
if (!this._activeChangeCount) {
this._triggerExternalChangesNow();
}
};
/**
* The queue of pending watch/unwatch requests.
* @type {Array.<{fn: function(), cb: function()}>}
*/
FileSystem.prototype._watchRequests = null;
/**
* Dequeue and process all pending watch/unwatch requests
*/
FileSystem.prototype._dequeueWatchRequest = function () {
if (this._watchRequests.length > 0) {
var request = this._watchRequests[0];
request.fn.call(null, function () {
// Apply the given callback
var callbackArgs = arguments;
try {
request.cb.apply(null, callbackArgs);
} finally {
// Process the remaining watch/unwatch requests
this._watchRequests.shift();
this._dequeueWatchRequest();
}
}.bind(this));
}
};
/**
* Enqueue a new watch/unwatch request.
*
* @param {function()} fn - The watch/unwatch request function.
* @param {callback()} cb - The callback for the provided watch/unwatch
* request function.
*/
FileSystem.prototype._enqueueWatchRequest = function (fn, cb) {
// Enqueue the given watch/unwatch request
this._watchRequests.push({fn: fn, cb: cb});
// Begin processing the queue if it is not already being processed
if (this._watchRequests.length === 1) {
this._dequeueWatchRequest();
}
};
/**
* The set of watched roots, encoded as a mapping from full paths to WatchedRoot
* objects which contain a file entry, filter function, and an indication of
* whether the watched root is inactive, starting up or fully active.
*
* @type {Object.<string, WatchedRoot>}
*/
FileSystem.prototype._watchedRoots = null;
/**
* Finds a parent watched root for a given path, or returns null if a parent
* watched root does not exist.
*
* @param {string} fullPath The child path for which a parent watched root is to be found
* @return {?{entry: FileSystemEntry, filter: function(string) boolean}} The parent
* watched root, if it exists, or null.
*/
FileSystem.prototype._findWatchedRootForPath = function (fullPath) {
var watchedRoot = null;
Object.keys(this._watchedRoots).some(function (watchedPath) {
if (fullPath.indexOf(watchedPath) === 0) {
watchedRoot = this._watchedRoots[watchedPath];
return true;
}
}, this);
return watchedRoot;
};
/**
* Helper function to watch or unwatch a filesystem entry beneath a given
* watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
* @param {boolean} shouldWatch - Whether the entry should be watched (true)
* or unwatched (false).
*/
FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) {
var impl = this._impl,
recursiveWatch = impl.recursiveWatch,
commandName = shouldWatch ? "watchPath" : "unwatchPath",
filterGlobs = watchedRoot.filterGlobs;
if (recursiveWatch) {
// The impl can watch the entire subtree with one call on the root (we also fall into this case for
// unwatch, although that never requires us to do the recursion - see similar final case below)
if (entry !== watchedRoot.entry) {
// Watch and unwatch calls to children of the watched root are
// no-ops if the impl supports recursiveWatch
callback(null);
} else {
// The impl will handle finding all subdirectories to watch.
this._enqueueWatchRequest(function (requestCb) {
impl[commandName].call(impl, entry.fullPath, filterGlobs, requestCb);
}.bind(this), callback);
}
} else if (shouldWatch) {
// The impl can't handle recursive watch requests, so it's up to the
// filesystem to recursively watch all subdirectories.
this._enqueueWatchRequest(function (requestCb) {
// First construct a list of entries to watch or unwatch
var entriesToWatch = [];
var visitor = function (child) {
if (watchedRoot.filter(child.name, child.parentPath)) {
if (child.isDirectory || child === watchedRoot.entry) {
entriesToWatch.push(child);
}
return true;
}
return false;
};
entry.visit(visitor, function (err) {
if (err) {
// Unexpected error
requestCb(err);
return;
}
// Then watch or unwatched all these entries
var count = entriesToWatch.length;
if (count === 0) {
requestCb(null);
return;
}
var watchCallback = function () {
if (--count === 0) {
requestCb(null);
}
};
entriesToWatch.forEach(function (entry) {
impl.watchPath(entry.fullPath, filterGlobs, watchCallback);
});
});
}, callback);
} else {
// Unwatching never requires enumerating the subfolders (which is good, since after a
// delete/rename we may be unable to do so anyway)
this._enqueueWatchRequest(function (requestCb) {
impl.unwatchPath(entry.fullPath, requestCb);
}, callback);
}
};
/**
* Watch a filesystem entry beneath a given watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
*/
FileSystem.prototype._watchEntry = function (entry, watchedRoot, callback) {
this._watchOrUnwatchEntry(entry, watchedRoot, callback, true);
};
/**
* Unwatch a filesystem entry beneath a given watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
*/
FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) {
this._watchOrUnwatchEntry(entry, watchedRoot, function (err) {
// Make sure to clear cached data for all unwatched entries because
// entries always return cached data if it exists!
this._index.visitAll(function (child) {
if (child.fullPath.indexOf(entry.fullPath) === 0) {
// 'true' so entry doesn't try to clear its immediate childrens' caches too. That would be redundant
// with the visitAll() here, and could be slow if we've already cleared its parent (#7150).
child._clearCachedData(true);
}
}.bind(this));
callback(err);
}.bind(this), false);
};
/**
* Initialize this FileSystem instance.
*
* @param {FileSystemImpl} impl The back-end implementation for this
* FileSystem instance.
*/
FileSystem.prototype.init = function (impl) {
console.assert(!this._impl, "This FileSystem has already been initialized!");
var changeCallback = this._enqueueExternalChange.bind(this),
offlineCallback = this._unwatchAll.bind(this);
this._impl = impl;
this._impl.initWatchers(changeCallback, offlineCallback);
};
/**
* Close a file system. Clear all caches, indexes, and file watchers.
*/
FileSystem.prototype.close = function () {
this._impl.unwatchAll();
this._index.clear();
};
/**
* Returns true if the given path should be automatically added to the index & watch list when one of its ancestors
* is a watch-root. (Files are added automatically when the watch-root is first established, or later when a new
* directory is created and its children enumerated).
*
* Entries explicitly created via FileSystem.getFile/DirectoryForPath() are *always* added to the index regardless
* of this filtering - but they will not be watched if the watch-root's filter excludes them.
*
* @param {string} path Full path
* @param {string} name Name portion of the path
*/
FileSystem.prototype._indexFilter = function (path, name) {
var parentRoot = this._findWatchedRootForPath(path);
if (parentRoot) {
return parentRoot.filter(name, path);
}
// It might seem more sensible to return false (exclude) for files outside the watch roots, but
// that would break usage of appFileSystem for 'system'-level things like enumerating extensions.
// (Or in general, Directory.getContents() for any Directory outside the watch roots).
return true;
};
/**
* Indicates that a filesystem-mutating operation has begun. As long as there
* are changes taking place, change events from the external watchers are
* blocked and queued, to be handled once changes have finished. This is done
* because for mutating operations that originate from within the filesystem,
* synthetic change events are fired that do not depend on external file
* watchers, and we prefer the former over the latter for the following
* reasons: 1) there is no delay; and 2) they may have higher fidelity ---
* e.g., a rename operation can be detected as such, instead of as a nearly
* simultaneous addition and deletion.
*
* All operations that mutate the file system MUST begin with a call to
* _beginChange and must end with a call to _endChange.
*/
FileSystem.prototype._beginChange = function () {
this._activeChangeCount++;
//console.log("> beginChange -> " + this._activeChangeCount);
};
/**
* Indicates that a filesystem-mutating operation has completed. See
* FileSystem._beginChange above.
*/
FileSystem.prototype._endChange = function () {
this._activeChangeCount--;
//console.log("< endChange -> " + this._activeChangeCount);
if (this._activeChangeCount < 0) {
console.error("FileSystem _activeChangeCount has fallen below zero!");
}
if (!this._activeChangeCount) {
this._triggerExternalChangesNow();
}
};
/**
* Determines whether or not the supplied path is absolute, as opposed to relative.
*
* @param {!string} fullPath
* @return {boolean} True if the fullPath is absolute and false otherwise.
*/
FileSystem.isAbsolutePath = function (fullPath) {
return (fullPath[0] === "/" || (fullPath[1] === ":" && fullPath[2] === "/"));
};
function _ensureTrailingSlash(path) {
if (path[path.length - 1] !== "/") {
path += "/";
}
return path;
}
/*
* Matches continguous groups of forward slashes
* @const
*/
var _DUPLICATED_SLASH_RE = /\/{2,}/g;
/**
* Returns a canonical version of the path: no duplicated "/"es, no ".."s,
* and directories guaranteed to end in a trailing "/"
* @param {!string} path Absolute path, using "/" as path separator
* @param {boolean=} isDirectory
* @return {!string}
*/
FileSystem.prototype._normalizePath = function (path, isDirectory) {
if (!FileSystem.isAbsolutePath(path)) {
throw new Error("Paths must be absolute: '" + path + "'"); // expect only absolute paths
}
var isUNCPath = this._impl.normalizeUNCPaths && path.search(_DUPLICATED_SLASH_RE) === 0;
// Remove duplicated "/"es
path = path.replace(_DUPLICATED_SLASH_RE, "/");
// Remove ".." segments
if (path.indexOf("..") !== -1) {
var segments = path.split("/"),
i;
for (i = 1; i < segments.length; i++) {
if (segments[i] === "..") {
if (i < 2) {
throw new Error("Invalid absolute path: '" + path + "'");
}
segments.splice(i - 1, 2);
i -= 2; // compensate so we start on the right index next iteration
}
}
path = segments.join("/");
}
if (isDirectory) {
// Make sure path DOES include trailing slash
path = _ensureTrailingSlash(path);
}
if (isUNCPath) {
// Restore the leading double slash that was removed previously
path = "/" + path;
}
return path;
};
/**
* This method adds an entry for a file in the file Index. Files on disk are added
* to the file index either on load or on open. This method is primarily needed to add
* in memory files to the index
*
* @param {File} The fileEntry which needs to be added
* @param {String} The full path to the file
*/
FileSystem.prototype.addEntryForPathIfRequired = function (fileEntry, path) {
var entry = this._index.getEntry(path);
if (!entry) {
this._index.addEntry(fileEntry);
}
};
/**
* Return a (strict subclass of a) FileSystemEntry object for the specified
* path using the provided constuctor. For now, the provided constructor
* should be either File or Directory.
*
* @private
* @param {function(string, FileSystem)} EntryConstructor Constructor with
* which to initialize new FileSystemEntry objects.
* @param {string} path Absolute path of file.
* @return {File|Directory} The File or Directory object. This file may not
* yet exist on disk.
*/
FileSystem.prototype._getEntryForPath = function (EntryConstructor, path) {
var isDirectory = EntryConstructor === Directory;
path = this._normalizePath(path, isDirectory);
var entry = this._index.getEntry(path);
if (!entry) {
entry = new EntryConstructor(path, this);
this._index.addEntry(entry);
}
return entry;
};
/**
* Return a File object for the specified path.
*
* @param {string} path Absolute path of file.
*
* @return {File} The File object. This file may not yet exist on disk.
*/
FileSystem.prototype.getFileForPath = function (path) {
var protocol = PathUtils.parseUrl(path).protocol,
protocolAdapter = _getProtocolAdapter(protocol);
if (protocolAdapter && protocolAdapter.fileImpl) {
return new protocolAdapter.fileImpl(protocol, path, this);
} else {
return this._getEntryForPath(File, path);
}
};
/**
* Return a Directory object for the specified path.
*
* @param {string} path Absolute path of directory.
*
* @return {Directory} The Directory object. This directory may not yet exist on disk.
*/
FileSystem.prototype.getDirectoryForPath = function (path) {
return this._getEntryForPath(Directory, path);
};
/**
* Resolve a path.
*
* @param {string} path The path to resolve
* @param {function (?string, FileSystemEntry=, FileSystemStats=)} callback Callback resolved
* with a FileSystemError string or with the entry for the provided path.
*/
FileSystem.prototype.resolve = function (path, callback) {
var normalizedPath = this._normalizePath(path, false),
item = this._index.getEntry(normalizedPath);
if (!item) {
normalizedPath = _ensureTrailingSlash(normalizedPath);
item = this._index.getEntry(normalizedPath);
}
if (item) {
item.stat(function (err, stat) {
if (err) {
callback(err);
return;
}
callback(null, item, stat);
});
} else {
this._impl.stat(path, function (err, stat) {
if (err) {
callback(err);
return;
}
if (stat.isFile) {
item = this.getFileForPath(path);
} else {
item = this.getDirectoryForPath(path);
}
if (item._isWatched()) {
item._stat = stat;
}
callback(null, item, stat);
}.bind(this));
}
};
/**
* Show an "Open" dialog and return the file(s)/directories selected by the user.
*
* @param {boolean} allowMultipleSelection Allows selecting more than one file at a time
* @param {boolean} chooseDirectories Allows directories to be opened
* @param {string} title The title of the dialog
* @param {string} initialPath The folder opened inside the window initially. If initialPath
* is not set, or it doesn't exist, the window would show the last
* browsed folder depending on the OS preferences
* @param {?Array.<string>} fileTypes (Currently *ignored* except on Mac - https://trello.com/c/430aXkpq)
* List of extensions that are allowed to be opened, without leading ".".
* Null or empty array allows all files to be selected. Not applicable
* when chooseDirectories = true.
* @param {function (?string, Array.<string>=)} callback Callback resolved with a FileSystemError
* string or the selected file(s)/directories. If the user cancels the
* open dialog, the error will be falsy and the file/directory array will
* be empty.
*/
FileSystem.prototype.showOpenDialog = function (allowMultipleSelection,
chooseDirectories,
title,
initialPath,
fileTypes,
callback) {
this._impl.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback);
};
/**
* Show a "Save" dialog and return the path of the file to save.
*
* @param {string} title The title of the dialog.
* @param {string} initialPath The folder opened inside the window initially. If initialPath
* is not set, or it doesn't exist, the window would show the last
* browsed folder depending on the OS preferences.
* @param {string} proposedNewFilename Provide a new file name for the user. This could be based on
* on the current file name plus an additional suffix
* @param {function (?string, string=)} callback Callback that is resolved with a FileSystemError
* string or the name of the file to save. If the user cancels the save,
* the error will be falsy and the name will be empty.
*/
FileSystem.prototype.showSaveDialog = function (title, initialPath, proposedNewFilename, callback) {
this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback);
};
/**
* Fire a rename event. Clients listen for these events using FileSystem.on.
*
* @param {string} oldPath The entry's previous fullPath
* @param {string} newPath The entry's current fullPath
*/
FileSystem.prototype._fireRenameEvent = function (oldPath, newPath) {
this.trigger("rename", oldPath, newPath);
};
/**
* Fire a change event. Clients listen for these events using FileSystem.on.
*
* @param {File|Directory} entry The entry that has changed
* @param {Array<File|Directory>=} added If the entry is a directory, this
* is a set of new entries in the directory.
* @param {Array<File|Directory>=} removed If the entry is a directory, this
* is a set of removed entries from the directory.
*/
FileSystem.prototype._fireChangeEvent = function (entry, added, removed) {
this.trigger("change", entry, added, removed);
};
/**
* @private
* Notify the system when an entry name has changed.
*
* @param {string} oldFullPath
* @param {string} newFullPath
* @param {boolean} isDirectory
*/
FileSystem.prototype._handleRename = function (oldFullPath, newFullPath, isDirectory) {
// Update all affected entries in the index
this._index.entryRenamed(oldFullPath, newFullPath, isDirectory);
};
/**
* Notify the filesystem that the given directory has changed. Updates the filesystem's
* internal state as a result of the change, and calls back with the set of added and
* removed entries. Mutating FileSystemEntry operations should call this method before
* applying the operation's callback, and pass along the resulting change sets in the
* internal change event.
*
* @param {Directory} directory The directory that has changed.
* @param {function(Array<File|Directory>=, Array<File|Directory>=)} callback
* The callback that will be applied to a set of added and a set of removed
* FileSystemEntry objects.
*/
FileSystem.prototype._handleDirectoryChange = function (directory, callback) {
var oldContents = directory._contents;
directory._clearCachedData();
directory.getContents(function (err, contents) {
var addedEntries = oldContents && contents.filter(function (entry) {
return oldContents.indexOf(entry) === -1;
});
var removedEntries = oldContents && oldContents.filter(function (entry) {
return contents.indexOf(entry) === -1;
});
// If directory is not watched, clear children's caches manually.
var watchedRoot = this._findWatchedRootForPath(directory.fullPath);
if (!watchedRoot || !watchedRoot.filter(directory.name, directory.parentPath)) {
this._index.visitAll(function (entry) {
if (entry.fullPath.indexOf(directory.fullPath) === 0) {
// Passing 'true' for a similar reason as in _unwatchEntry() - see #7150
entry._clearCachedData(true);
}
}.bind(this));
callback(addedEntries, removedEntries);
return;
}
var addedCounter = addedEntries ? addedEntries.length : 0,
removedCounter = removedEntries ? removedEntries.length : 0,
counter = addedCounter + removedCounter;
if (counter === 0) {
callback(addedEntries, removedEntries);
return;
}
var watchOrUnwatchCallback = function (err) {
if (err) {
console.error("FileSystem error in _handleDirectoryChange after watch/unwatch entries: " + err);
}
if (--counter === 0) {
callback(addedEntries, removedEntries);
}
};
if (addedEntries) {
addedEntries.forEach(function (entry) {
this._watchEntry(entry, watchedRoot, watchOrUnwatchCallback);
}, this);
}
if (removedEntries) {
removedEntries.forEach(function (entry) {
this._unwatchEntry(entry, watchedRoot, watchOrUnwatchCallback);
}, this);
}
}.bind(this));
};
/**
* @private
* Processes a result from the file/directory watchers. Watch results are sent from the low-level implementation
* whenever a directory or file is changed.
*
* @param {string} path The path that changed. This could be a file or a directory.
* @param {FileSystemStats=} stat Optional stat for the item that changed. This param is not always
* passed.
*/
FileSystem.prototype._handleExternalChange = function (path, stat) {
if (!path) {
// This is a "wholesale" change event; clear all caches
this._index.visitAll(function (entry) {
// Passing 'true' for a similar reason as in _unwatchEntry() - see #7150
entry._clearCachedData(true);
});
this._fireChangeEvent(null);
return;
}
path = this._normalizePath(path, false);
var entry = this._index.getEntry(path);
if (entry) {
var oldStat = entry._stat;
if (entry.isFile) {
// Update stat and clear contents, but only if out of date
if (!(stat && oldStat && stat.mtime.getTime() <= oldStat.mtime.getTime())) {
entry._clearCachedData();
entry._stat = stat;
this._fireChangeEvent(entry);
}
} else {
this._handleDirectoryChange(entry, function (added, removed) {
entry._stat = stat;
if (entry._isWatched()) {
// We send a change even if added & removed are both zero-length. Something may still have changed,
// e.g. a file may have been quickly removed & re-added before we got a chance to reread the directory
// listing.
this._fireChangeEvent(entry, added, removed);
}
}.bind(this));
}
}
};
/**
* Clears all cached content. Because of the performance implications of this, this should only be used if
* there is a suspicion that the file system has not been updated through the normal file watchers
* mechanism.
*/
FileSystem.prototype.clearAllCaches = function () {
this._handleExternalChange(null);
};
/**
* Start watching a filesystem root entry.
*
* @param {FileSystemEntry} entry - The root entry to watch. If entry is a directory,
* all subdirectories that aren't explicitly filtered will also be watched.
* @param {function(string): boolean} filter - Returns true if a particular item should
* be watched, given its name (not full path). Items that are ignored are also
* filtered from Directory.getContents() results within this subtree.
* @param {Array<string>} filterGlobs - glob compatible string definitions for
* filtering out events on the node side.
* @param {function(?string)=} callback - A function that is called when the watch has
* completed. If the watch fails, the function will have a non-null FileSystemError
* string parametr.
*/
FileSystem.prototype.watch = function (entry, filter, filterGlobs, callback) {
// make filterGlobs an optional argument to stay backwards compatible
if (typeof callback === "undefined" && typeof filterGlobs === "function") {
callback = filterGlobs;
filterGlobs = null;
}
var fullPath = entry.fullPath;
callback = callback || function () {};
var watchingParentRoot = this._findWatchedRootForPath(fullPath);
if (watchingParentRoot &&
(watchingParentRoot.status === WatchedRoot.STARTING ||
watchingParentRoot.status === WatchedRoot.ACTIVE)) {
callback("A parent of this root is already watched");
return;
}
var watchingChildRoot = Object.keys(this._watchedRoots).some(function (path) {
var watchedRoot = this._watchedRoots[path],
watchedPath = watchedRoot.entry.fullPath;
return watchedPath.indexOf(fullPath) === 0;
}, this);
if (watchingChildRoot &&
(watchingChildRoot.status === WatchedRoot.STARTING ||
watchingChildRoot.status === WatchedRoot.ACTIVE)) {
callback("A child of this root is already watched");
return;
}
var watchedRoot = new WatchedRoot(entry, filter, filterGlobs);
this._watchedRoots[fullPath] = watchedRoot;
// Enter the STARTING state early to indiate that watched Directory
// objects may cache their contents. See FileSystemEntry._isWatched.
watchedRoot.status = WatchedRoot.STARTING;
this._watchEntry(entry, watchedRoot, function (err) {
if (err) {
console.warn("Failed to watch root: ", entry.fullPath, err);
delete this._watchedRoots[fullPath];
callback(err);
return;
}
watchedRoot.status = WatchedRoot.ACTIVE;
callback(null);
}.bind(this));
};
/**
* Stop watching a filesystem root entry.
*
* @param {FileSystemEntry} entry - The root entry to stop watching. The unwatch will
* if the entry is not currently being watched.
* @param {function(?string)=} callback - A function that is called when the unwatch has
* completed. If the unwatch fails, the function will have a non-null FileSystemError
* string parameter.
*/
FileSystem.prototype.unwatch = function (entry, callback) {
var fullPath = entry.fullPath,
watchedRoot = this._watchedRoots[fullPath];
callback = callback || function () {};
if (!watchedRoot) {
callback(FileSystemError.ROOT_NOT_WATCHED);
return;
}
// Mark this as inactive, but don't delete the entry until the unwatch is complete.
// This is useful for making sure we don't try to concurrently watch overlapping roots.
watchedRoot.status = WatchedRoot.INACTIVE;
this._unwatchEntry(entry, watchedRoot, function (err) {
delete this._watchedRoots[fullPath];
this._index.visitAll(function (child) {
if (child.fullPath.indexOf(entry.fullPath) === 0) {
this._index.removeEntry(child);
}
}.bind(this));
if (err) {
console.warn("Failed to unwatch root: ", entry.fullPath, err);
callback(err);
return;
}
callback(null);
}.bind(this));
};
/**
* Unwatch all watched roots. Calls unwatch on the underlying impl for each
* watched root and ignores errors.
* @private
*/
FileSystem.prototype._unwatchAll = function () {
console.warn("File watchers went offline!");
Object.keys(this._watchedRoots).forEach(function (path) {
var watchedRoot = this._watchedRoots[path];
watchedRoot.status = WatchedRoot.INACTIVE;
delete this._watchedRoots[path];
this._unwatchEntry(watchedRoot.entry, watchedRoot, function () {
console.warn("Watching disabled for", watchedRoot.entry.fullPath);
});
}, this);
// Fire a wholesale change event, clearing all caches and request that
// clients manually update their state.
this._handleExternalChange(null);
};
// The singleton instance
var _instance;
function _wrap(func) {
return function () {
return func.apply(_instance, arguments);
};
}
// Export public methods as proxies to the singleton instance
exports.init = _wrap(FileSystem.prototype.init);
exports.close = _wrap(FileSystem.prototype.close);
exports.shouldShow = _wrap(FileSystem.prototype.shouldShow);
exports.getFileForPath = _wrap(FileSystem.prototype.getFileForPath);
exports.addEntryForPathIfRequired = _wrap(FileSystem.prototype.addEntryForPathIfRequired);
exports.getDirectoryForPath = _wrap(FileSystem.prototype.getDirectoryForPath);
exports.resolve = _wrap(FileSystem.prototype.resolve);
exports.showOpenDialog = _wrap(FileSystem.prototype.showOpenDialog);
exports.showSaveDialog = _wrap(FileSystem.prototype.showSaveDialog);
exports.watch = _wrap(FileSystem.prototype.watch);
exports.unwatch = _wrap(FileSystem.prototype.unwatch);
exports.clearAllCaches = _wrap(FileSystem.prototype.clearAllCaches);
// Static public utility methods
exports.isAbsolutePath = FileSystem.isAbsolutePath;
exports.registerProtocolAdapter = registerProtocolAdapter;
// For testing only
exports._getActiveChangeCount = _wrap(FileSystem.prototype._getActiveChangeCount);
/**
* Add an event listener for a FileSystem event.
*
* @param {string} event The name of the event
* @param {function} handler The handler for the event
*/
exports.on = function (event, handler) {
_instance.on(event, handler);
};
/**
* Remove an event listener for a FileSystem event.
*
* @param {string} event The name of the event
* @param {function} handler The handler for the event
*/
exports.off = function (event, handler) {
_instance.off(event, handler);
};
// Export the FileSystem class as "private" for unit testing only.
exports._FileSystem = FileSystem;
// Create the singleton instance
_instance = new FileSystem();
// Initialize the singleton instance
_instance.init(require("fileSystemImpl"));
});