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:
All paths passed to FileSystem APIs must be in the following format:
All paths returned from FileSystem APIs additionally meet the following guarantees:
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:
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:
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.
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;
}
FileSystem hook to register file protocol adapter
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;
}
}
The FileSystem is not usable until init() signals its callback.
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);
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).
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.
FileSystem.prototype._externalChanges = null;
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;
The queue of pending watch/unwatch requests.
FileSystem.prototype._watchRequests = null;
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.
FileSystem.prototype._watchedRoots = null;
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);
};
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));
}
};
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();
}
};
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.
FileSystem.prototype._enqueueExternalChange = function (path, stat) {
this._externalChanges.push({path: path, stat: stat});
if (!this._activeChangeCount) {
this._triggerExternalChangesNow();
}
};
Enqueue a new watch/unwatch request.
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();
}
};
Finds a parent watched root for a given path, or returns null if a parent watched root does not exist.
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;
};
Fire a change event. Clients listen for these events using FileSystem.on.
FileSystem.prototype._fireChangeEvent = function (entry, added, removed) {
this.trigger("change", entry, added, removed);
};
Fire a rename event. Clients listen for these events using FileSystem.on.
FileSystem.prototype._fireRenameEvent = function (oldPath, newPath) {
this.trigger("rename", oldPath, newPath);
};
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.
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;
};
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.
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));
};
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));
}
}
};
FileSystem.prototype._handleRename = function (oldFullPath, newFullPath, isDirectory) {
// Update all affected entries in the index
this._index.entryRenamed(oldFullPath, newFullPath, isDirectory);
};
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.
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;
};
Returns a canonical version of the path: no duplicated "/"es, no ".."s, and directories guaranteed to end in a trailing "/"
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;
};
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;
};
Unwatch all watched roots. Calls unwatch on the underlying impl for each watched root and ignores errors.
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);
Unwatch a filesystem entry beneath a given watchedRoot.
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);
};
Watch a filesystem entry beneath a given watchedRoot.
FileSystem.prototype._watchEntry = function (entry, watchedRoot, callback) {
this._watchOrUnwatchEntry(entry, watchedRoot, callback, true);
};
Helper function to watch or unwatch a filesystem entry beneath a given watchedRoot.
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);
}
};
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
FileSystem.prototype.addEntryForPathIfRequired = function (fileEntry, path) {
var entry = this._index.getEntry(path);
if (!entry) {
this._index.addEntry(fileEntry);
}
};
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);
};
Close a file system. Clear all caches, indexes, and file watchers.
FileSystem.prototype.close = function () {
this._impl.unwatchAll();
this._index.clear();
};
Return a Directory object for the specified path.
FileSystem.prototype.getDirectoryForPath = function (path) {
return this._getEntryForPath(Directory, path);
};
Return a File object for the specified path.
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);
}
};
Initialize 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);
};
Determines whether or not the supplied path is absolute, as opposed to relative.
FileSystem.isAbsolutePath = function (fullPath) {
return (fullPath[0] === "/" || (fullPath[1] === ":" && fullPath[2] === "/"));
};
function _ensureTrailingSlash(path) {
if (path[path.length - 1] !== "/") {
path += "/";
}
return path;
}
Resolve a 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.
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.
FileSystem.prototype.showSaveDialog = function (title, initialPath, proposedNewFilename, callback) {
this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback);
};
Stop watching a filesystem root entry.
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));
};
Start watching a filesystem root entry.
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));
};