diff options
Diffstat (limited to 'devtools/client/projecteditor/lib/stores/local.js')
-rw-r--r-- | devtools/client/projecteditor/lib/stores/local.js | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/devtools/client/projecteditor/lib/stores/local.js b/devtools/client/projecteditor/lib/stores/local.js new file mode 100644 index 000000000..1f782dadf --- /dev/null +++ b/devtools/client/projecteditor/lib/stores/local.js @@ -0,0 +1,215 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Cc, Ci, Cu, ChromeWorker } = require("chrome"); +const { Class } = require("sdk/core/heritage"); +const { OS } = require("resource://gre/modules/osfile.jsm"); +const { emit } = require("sdk/event/core"); +const { Store } = require("devtools/client/projecteditor/lib/stores/base"); +const { Task } = require("devtools/shared/task"); +const promise = require("promise"); +const Services = require("Services"); +const { on, forget } = require("devtools/client/projecteditor/lib/helpers/event"); +const { FileResource } = require("devtools/client/projecteditor/lib/stores/resource"); + +const CHECK_LINKED_DIRECTORY_DELAY = 5000; +const SHOULD_LIVE_REFRESH = true; +// XXX: Ignores should be customizable +const IGNORE_REGEX = /(^\.)|(\~$)|(^node_modules$)/; + +/** + * A LocalStore object maintains a collection of Resource objects + * from the file system. + * + * This object emits the following events: + * - "resource-added": When a resource is added + * - "resource-removed": When a resource is removed + */ +var LocalStore = Class({ + extends: Store, + + defaultCategory: "js", + + initialize: function(path) { + this.initStore(); + this.path = OS.Path.normalize(path); + this.rootPath = this.path; + this.displayName = this.path; + this.root = this._forPath(this.path); + this.notifyAdd(this.root); + this.refreshLoop = this.refreshLoop.bind(this); + this.refreshLoop(); + }, + + destroy: function() { + clearTimeout(this._refreshTimeout); + + if (this._refreshDeferred) { + this._refreshDeferred.reject("destroy"); + } + if (this.worker) { + this.worker.terminate(); + } + + this._refreshTimeout = null; + this._refreshDeferred = null; + this.worker = null; + + if (this.root) { + forget(this, this.root); + this.root.destroy(); + } + }, + + toString: function() { return "[LocalStore:" + this.path + "]" }, + + /** + * Return a FileResource object for the given path. If a FileInfo + * is provided the resource will use it, otherwise the FileResource + * might not have full information until the next refresh. + * + * The following parameters are passed into the FileResource constructor + * See resource.js for information about them + * + * @param String path + * @param FileInfo info + * @returns Resource + */ + _forPath: function(path, info=null) { + if (this.resources.has(path)) { + return this.resources.get(path); + } + + let resource = FileResource(this, path, info); + this.resources.set(path, resource); + return resource; + }, + + /** + * Return a promise that resolves to a fully-functional FileResource + * within this project. This will hit the disk for stat info. + * options: + * + * create: If true, a resource will be created even if the underlying + * file doesn't exist. + */ + resourceFor: function(path, options) { + path = OS.Path.normalize(path); + + if (this.resources.has(path)) { + return promise.resolve(this.resources.get(path)); + } + + if (!this.contains(path)) { + return promise.reject(new Error(path + " does not belong to " + this.path)); + } + + return Task.spawn(function*() { + let parent = yield this.resourceFor(OS.Path.dirname(path)); + + let info; + try { + info = yield OS.File.stat(path); + } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { + if (!options.create) { + throw ex; + } + } + + let resource = this._forPath(path, info); + parent.addChild(resource); + return resource; + }.bind(this)); + }, + + refreshLoop: function() { + // XXX: Once Bug 958280 adds a watch function, will not need to forever loop here. + this.refresh().then(() => { + if (SHOULD_LIVE_REFRESH) { + this._refreshTimeout = setTimeout(this.refreshLoop, + CHECK_LINKED_DIRECTORY_DELAY); + } + }); + }, + + _refreshTimeout: null, + _refreshDeferred: null, + + /** + * Refresh the directory structure. + */ + refresh: function(path=this.rootPath) { + if (this._refreshDeferred) { + return this._refreshDeferred.promise; + } + this._refreshDeferred = promise.defer(); + + let worker = this.worker = new ChromeWorker("chrome://devtools/content/projecteditor/lib/helpers/readdir.js"); + let start = Date.now(); + + worker.onmessage = evt => { + // console.log("Directory read finished in " + ( Date.now() - start ) +"ms", evt); + for (path in evt.data) { + let info = evt.data[path]; + info.path = path; + + let resource = this._forPath(path, info); + resource.info = info; + if (info.isDir) { + let newChildren = new Set(); + for (let childPath of info.children) { + childInfo = evt.data[childPath]; + newChildren.add(this._forPath(childPath, childInfo)); + } + resource.setChildren(newChildren); + } + resource.info.children = null; + } + + worker = null; + this._refreshDeferred.resolve(); + this._refreshDeferred = null; + }; + worker.onerror = ex => { + console.error(ex); + worker = null; + this._refreshDeferred.reject(ex); + this._refreshDeferred = null; + } + worker.postMessage({ path: this.rootPath, ignore: IGNORE_REGEX }); + return this._refreshDeferred.promise; + }, + + /** + * Returns true if the given path would be a child of the store's + * root directory. + */ + contains: function(path) { + path = OS.Path.normalize(path); + let thisPath = OS.Path.split(this.rootPath); + let thatPath = OS.Path.split(path) + + if (!(thisPath.absolute && thatPath.absolute)) { + throw new Error("Contains only works with absolute paths."); + } + + if (thisPath.winDrive && (thisPath.winDrive != thatPath.winDrive)) { + return false; + } + + if (thatPath.components.length <= thisPath.components.length) { + return false; + } + + for (let i = 0; i < thisPath.components.length; i++) { + if (thisPath.components[i] != thatPath.components[i]) { + return false; + } + } + return true; + } +}); +exports.LocalStore = LocalStore; |