-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
+// Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";
-Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("storage", {
exports: ["File", "Storage", "storage"],
- require: ["services", "util"]
-}, this);
+ require: ["promises", "services", "util"]
+});
+
+lazyRequire("config", ["config"]);
+lazyRequire("io", ["IO"]);
+lazyRequire("overlay", ["overlay"]);
+
+lazyRequire("resource://gre/modules/osfile.jsm", ["OS"]);
var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
var myObject = JSON.parse("{}").constructor;
-function loadData(name, store, type) {
- try {
- let data = storage.infoPath.child(name).read();
- let result = JSON.parse(data);
- if (result instanceof type)
- return result;
- }
- catch (e) {}
-}
-
-function saveData(obj) {
- if (obj.privateData && storage.privateMode)
- return;
- if (obj.store && storage.infoPath)
- storage.infoPath.child(obj.name).write(obj.serial);
-}
+var global = Cu.getGlobalForObject(this);
var StoreBase = Class("StoreBase", {
OPTIONS: ["privateData", "replacer"],
get serial() JSON.stringify(this._object, this.replacer),
- init: function (name, store, load, options) {
+ init: function init(name, store, load, options) {
this._load = load;
+ this._options = options;
- this.__defineGetter__("store", function () store);
- this.__defineGetter__("name", function () name);
+ this.__defineGetter__("store", () => store);
+ this.__defineGetter__("name", () => name);
for (let [k, v] in Iterator(options))
if (this.OPTIONS.indexOf(k) >= 0)
this[k] = v;
this.reload();
},
- changed: function () { this.timer.tell(); },
+ clone: function clone(storage) {
+ let store = storage.privateMode ? false : this.store;
+ let res = this.constructor(this.name, store, this._load, this._options);
+ res.storage = storage;
+ return res;
+ },
+
+ makeOwn: function makeOwn(val) {
+ if (typeof val != "object")
+ return val;
+ if (Cu.getGlobalForObject(val) == global)
+ return val;
+ return JSON.parse(JSON.stringify(val, this.replacer));
+ },
+
+ changed: function () { this.timer && this.timer.tell(); },
reload: function reload() {
this._object = this._load() || this._constructor();
delete: function delete_() {
delete storage.keys[this.name];
delete storage[this.name];
- storage.infoPath.child(this.name).remove(false);
+ return OS.File.remove(
+ storage.infoPath.child(this.name).path);
},
- save: function () { saveData(this); },
+ save: function () { (self.storage || storage)._saveData(this); },
__iterator__: function () Iterator(this._object)
});
get length() this._object.length,
- set: function set(index, value) {
+ set: function set(index, value, quiet) {
var orig = this._object[index];
- this._object[index] = value;
- this.fireEvent("change", index);
+ this._object[index] = this.makeOwn(value);
+ if (!quiet)
+ this.fireEvent("change", index);
+
+ return orig;
},
push: function push(value) {
- this._object.push(value);
+ this._object.push(this.makeOwn(value));
this.fireEvent("push", this._object.length);
},
- pop: function pop(value) {
- var res = this._object.pop();
- this.fireEvent("pop", this._object.length);
+ pop: function pop(value, ord) {
+ if (ord == null)
+ var res = this._object.pop();
+ else
+ res = this._object.splice(ord, 1)[0];
+
+ this.fireEvent("pop", this._object.length, ord);
return res;
},
+ shift: function shift(value) {
+ var res = this._object.shift();
+ this.fireEvent("shift", this._object.length);
+ return res;
+ },
+
+ insert: function insert(value, ord) {
+ value = this.makeOwn(value);
+ if (ord == 0)
+ this._object.unshift(value);
+ else
+ this._object = this._object.slice(0, ord)
+ .concat([value])
+ .concat(this._object.slice(ord));
+ this.fireEvent("insert", this._object.length, ord);
+ },
+
truncate: function truncate(length, fromEnd) {
var res = this._object.length;
if (this._object.length > length) {
mutate: function mutate(funcName) {
var _funcName = funcName;
arguments[0] = this._object;
- this._object = Array[_funcName].apply(Array, arguments);
+ this._object = Array[_funcName].apply(Array, arguments)
+ .map(this.makeOwn.bind(this));
this.fireEvent("change", null);
},
},
get: function get(key, default_) {
- return key in this._object ? this._object[key] :
+ return this.has(key) ? this._object[key] :
arguments.length > 1 ? this.set(key, default_) :
undefined;
},
+ has: function has(key) hasOwnProperty(this._object, key),
+
keys: function keys() Object.keys(this._object),
remove: function remove(key) {
set: function set(key, val) {
var defined = key in this._object;
var orig = this._object[key];
- this._object[key] = val;
+ this._object[key] = this.makeOwn(val);
if (!defined)
this.fireEvent("add", key);
else if (orig != val)
}
});
+var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
+
var Storage = Module("Storage", {
+ Local: function Local(dactyl, modules, window) ({
+ init: function init() {
+ this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
+ }
+ }),
+
alwaysReload: {},
- init: function () {
+ init: function init() {
this.cleanup();
+
+ let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+ if (!Services.dactylSession)
+ Services.dactylSession = Cu.createObjectIn(sessionGlobal);
+ this.session = Services.dactylSession;
},
cleanup: function () {
this[key].timer.flush();
delete this[key];
}
- for (let ary in values(this.observers))
- for (let obj in values(ary))
- if (obj.ref && obj.ref.get())
- delete obj.ref.get().dactylStorageRefs;
this.keys = {};
this.observers = {};
},
- exists: function exists(name) this.infoPath.child(name).exists(),
+ _loadData: function loadData(name, store, type) {
+ try {
+ let file = storage.infoPath.child(name);
+ if (file.exists()) {
+ let data = file.read();
+ let result = JSON.parse(data);
+ if (result instanceof type)
+ return result;
+ }
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+ },
+
+ _saveData: promises.task(function saveData(obj) {
+ if (obj.privateData && storage.privateMode)
+ return;
+ if (obj.store && storage.infoPath) {
+ var { path } = storage.infoPath.child(obj.name);
+ yield OS.File.makeDir(storage.infoPath.path,
+ { ignoreExisting: true });
+ yield OS.File.writeAtomic(
+ path, obj.serial,
+ { tmpPath: path + ".part" });
+ }
+ }),
+
+ storeForSession: function storeForSession(key, val) {
+ if (val)
+ this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
+ else
+ delete this.dactylSession[key];
+ },
+
+ infoPath: Class.Memoize(() =>
+ File(IO.runtimePath.replace(/,.*/, ""))
+ .child("info").child(config.profileName)),
+
+ exists: function exists(key) this.infoPath.child(key).exists(),
+
+ remove: function remove(key) {
+ if (this.exists(key)) {
+ if (this[key] && this[key].timer)
+ this[key].timer.flush();
+ delete this[key];
+ delete this.keys[key];
+ return OS.File.remove(
+ this.infoPath.child(key).path);
+ }
+ },
- newObject: function newObject(key, constructor, params) {
+ newObject: function newObject(key, constructor, params={}) {
if (params == null || !isObject(params))
throw Error("Invalid argument type");
- if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
- if (key in this && !(params.reload || this.alwaysReload[key]))
- throw Error();
- let load = function () loadData(key, params.store, params.type || myObject);
+ if (this.isLocalModule) {
+ this.globalInstance.newObject.apply(this.globalInstance, arguments);
+
+ if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
+ let obj = this.globalInstance.keys[key];
+ this.keys[key] = this._privatize(obj);
+ }
+
+ return this.keys[key];
+ }
+
+ let reload = params.reload || this.alwaysReload[key];
+ if (!(key in this.keys) || reload) {
+ if (key in this && !reload)
+ throw Error("Cannot add storage key with that name.");
+
+ let load = () => this._loadData(key, params.store, params.type || myObject);
this.keys[key] = new constructor(key, params.store, load, params);
- this.keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
+ this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
this.__defineGetter__(key, function () this.keys[key]);
}
return this.keys[key];
},
- newMap: function newMap(key, options) {
+ newMap: function newMap(key, options={}) {
return this.newObject(key, ObjectStore, options);
},
- newArray: function newArray(key, options) {
+ newArray: function newArray(key, options={}) {
return this.newObject(key, ArrayStore, update({ type: Array }, options));
},
- addObserver: function addObserver(key, callback, ref) {
- if (ref) {
- if (!ref.dactylStorageRefs)
- ref.dactylStorageRefs = [];
- ref.dactylStorageRefs.push(callback);
- var callbackRef = Cu.getWeakReference(callback);
- }
- else {
- callbackRef = { get: function () callback };
- }
- this.removeDeadObservers();
- if (!(key in this.observers))
- this.observers[key] = [];
- if (!this.observers[key].some(function (o) o.callback.get() == callback))
- this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
+ get observerMaps() {
+ yield this.observers;
+ for (let window of overlay.windows)
+ yield overlay.getData(window, "storage-observers", Object);
},
- removeObserver: function (key, callback) {
- this.removeDeadObservers();
- if (!(key in this.observers))
- return;
- this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
- if (this.observers[key].length == 0)
- delete obsevers[key];
+ addObserver: function addObserver(key, callback, window) {
+ var { observers } = this;
+ if (window instanceof Ci.nsIDOMWindow)
+ observers = overlay.getData(window, "storage-observers", Object);
+
+ if (!hasOwnProperty(observers, key))
+ observers[key] = RealSet();
+
+ observers[key].add(callback);
},
- removeDeadObservers: function () {
- for (let [key, ary] in Iterator(this.observers)) {
- this.observers[key] = ary = ary.filter(function (o) o.callback.get() && (!o.ref || o.ref.get() && o.ref.get().dactylStorageRefs));
- if (!ary.length)
- delete this.observers[key];
- }
+ removeObserver: function (key, callback) {
+ for (let observers in this.observerMaps)
+ if (key in observers)
+ observers[key].remove(callback);
},
fireEvent: function fireEvent(key, event, arg) {
- this.removeDeadObservers();
- if (key in this.observers)
- // Safe, since we have our own Array object here.
- for each (let observer in this.observers[key])
- observer.callback.get()(key, event, arg);
- if (key in this.keys)
+ for (let observers in this.observerMaps)
+ for (let observer of observers[key] || [])
+ observer(key, event, arg);
+
+ if (key in this.keys && this.keys[key].timer)
this[key].timer.tell();
},
save: function save(key) {
if (this[key])
- saveData(this.keys[key]);
+ this._saveData(this.keys[key]);
},
saveAll: function storeAll() {
for each (let obj in this.keys)
- saveData(obj);
+ this._saveData(obj);
},
_privateMode: false,
get privateMode() this._privateMode,
- set privateMode(val) {
- if (val && !this._privateMode)
+ set privateMode(enabled) {
+ this._privateMode = Boolean(enabled);
+
+ if (this.isLocalModule) {
this.saveAll();
- if (!val && this._privateMode)
- for (let key in this.keys)
- this.load(key);
- return this._privateMode = Boolean(val);
- }
+
+ if (!enabled)
+ delete this.keys;
+ else {
+ let { keys } = this;
+ this.keys = {};
+ for (let [k, v] in Iterator(keys))
+ this.keys[k] = this._privatize(v);
+ }
+ }
+ return this._privateMode;
+ },
+
+ _privatize: function privatize(obj) {
+ if (obj.privateData && obj.clone)
+ return obj.clone(this);
+ return obj;
+ },
}, {
Replacer: {
skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
}
}, {
- init: function init(dactyl, modules) {
- init.superapply(this, arguments);
- storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, ""))
- .child("info").child(dactyl.profileName);
- },
-
cleanup: function (dactyl, modules, window) {
- delete window.dactylStorageRefs;
- this.removeDeadObservers();
+ overlay.setData(window, "storage-callbacks", undefined);
}
});
* @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
* @param {boolean} checkPWD Whether to allow expansion relative to the
* current directory. @default true
+ * @param {string} charset The charset of the file. @default File.defaultEncoding
*/
var File = Class("File", {
- init: function (path, checkPWD) {
+ init: function (path, checkPWD, charset) {
let file = services.File();
- if (path instanceof Ci.nsIFile)
- file = path.QueryInterface(Ci.nsIFile).clone();
+ if (charset)
+ this.charset = charset;
+
+ if (path instanceof Ci.nsIFileURL)
+ path = path.file;
+
+ if (path instanceof Ci.nsIFile || path instanceof File)
+ file = path.clone();
else if (/file:\/\//.test(path))
- file = services["file:"]().getFileFromURLSpec(path);
+ file = services["file:"].getFileFromURLSpec(path);
else {
try {
let expandedPath = File.expandPath(path);
return File.DoesNotExist(path, e);
}
}
- let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
- self.__proto__ = this;
- return self;
+ this.file = file.QueryInterface(Ci.nsILocalFile);
+ return this;
},
+ charset: Class.Memoize(() => File.defaultEncoding),
+
+ /**
+ * @property {nsIFileURL} Returns the nsIFileURL object for this file.
+ */
+ URI: Class.Memoize(function () {
+ let uri = services.io.newFileURI(this.file)
+ .QueryInterface(Ci.nsIFileURL);
+ uri.QueryInterface(Ci.nsIMutable).mutable = false;
+ return uri;
+ }),
+
/**
* Iterates over the objects in this directory.
*/
- iterDirectory: function () {
+ iterDirectory: function iterDirectory() {
if (!this.exists())
- throw Error("File does not exist");
+ throw Error(_("io.noSuchFile"));
if (!this.isDirectory())
- throw Error("Not a directory");
+ throw Error(_("io.eNotDir"));
for (let file in iter(this.directoryEntries))
yield File(file);
},
/**
* Returns a new file for the given child of this directory entry.
*/
- child: function (name) {
+ child: function child() {
let f = this.constructor(this);
- for each (let elem in name.split(File.pathSplit))
- f.append(elem);
+ for (let [, name] in Iterator(arguments))
+ for (let elem of name.split(File.pathSplit))
+ f.append(elem);
return f;
},
+ /**
+ * Returns an iterator for all lines in a file.
+ */
+ get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
+ this.charset),
+
/**
* Reads this file's entire contents in "text" mode and returns the
* content as a string.
*
* @param {string} encoding The encoding from which to decode the file.
- * @default options["fileencoding"]
+ * @default #charset
* @returns {string}
*/
- read: function (encoding) {
- let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
- ifstream.init(this, -1, 0, 0);
+ read: function read(encoding) {
+ let ifstream = services.FileInStream(this.file, -1, 0, 0);
- return File.readStream(ifstream, encoding);
+ return File.readStream(ifstream, encoding || this.charset);
},
/**
*
* @param {boolean} sort Whether to sort the returned directory
* entries.
- * @returns {nsIFile[]}
+ * @returns {[nsIFile]}
*/
- readDirectory: function (sort) {
+ readDirectory: function readDirectory(sort) {
if (!this.isDirectory())
- throw Error("Not a directory");
+ throw Error(_("io.eNotDir"));
let array = [e for (e in this.iterDirectory())];
if (sort)
- array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
+ array.sort((a, b) => (b.isDirectory() - a.isDirectory() ||
+ String.localeCompare(a.path, b.path)));
return array;
},
*
* @returns {nsIFileURL}
*/
- toURI: function toURI() services.io.newFileURI(this),
+ toURI: function toURI() services.io.newFileURI(this.file),
/**
* Writes the string *buf* to this file.
* permissions if the file exists.
* @default 0644
* @param {string} encoding The encoding to used to write the file.
- * @default options["fileencoding"]
+ * @default #charset
*/
- write: function (buf, mode, perms, encoding) {
- let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+ write: function write(buf, mode, perms, encoding) {
function getStream(defaultChar) {
- let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
- stream.init(ofstream, encoding, 0, defaultChar);
- return stream;
+ return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
}
if (buf instanceof File)
buf = buf.read();
if (!encoding)
- encoding = File.defaultEncoding;
+ encoding = this.charset;
if (mode == ">>")
mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
if (!this.exists()) // OCREAT won't create the directory
this.create(this.NORMAL_FILE_TYPE, perms);
- ofstream.init(this, mode, perms, 0);
+ let ofstream = services.FileOutStream(this.file, mode, perms, 0);
try {
- if (callable(buf))
- buf(ofstream.QueryInterface(Ci.nsIOutputStream));
- else {
- var ocstream = getStream(0);
- ocstream.writeString(buf);
- }
+ var ocstream = getStream(0);
+ ocstream.writeString(buf);
}
- catch (e if callable(buf) && e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
+ catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
+ ocstream.close();
ocstream = getStream("?".charCodeAt(0));
ocstream.writeString(buf);
return false;
ofstream.close();
}
return true;
- }
+ },
+
+ // Wrapped native methods:
+ copyTo: function copyTo(dir, name)
+ this.file.copyTo(this.constructor(dir).file,
+ name),
+
+ copyToFollowingLinks: function copyToFollowingLinks(dir, name)
+ this.file.copyToFollowingLinks(this.constructor(dir).file,
+ name),
+
+ moveTo: function moveTo(dir, name)
+ this.file.moveTo(this.constructor(dir).file,
+ name),
+
+ equals: function equals(file)
+ this.file.equals(this.constructor(file).file),
+
+ contains: function contains(dir, recur)
+ this.file.contains(this.constructor(dir).file,
+ recur),
+
+ getRelativeDescriptor: function getRelativeDescriptor(file)
+ this.file.getRelativeDescriptor(this.constructor(file).file),
+
+ setRelativeDescriptor: function setRelativeDescriptor(file, path)
+ this.file.setRelativeDescriptor(this.constructor(file).file,
+ path)
}, {
/**
* @property {number} Open for reading only.
/**
* @property {string} The current platform's path separator.
*/
- PATH_SEP: Class.memoize(function () {
+ PATH_SEP: Class.Memoize(function () {
let f = services.directory.get("CurProcD", Ci.nsIFile);
f.append("foo");
return f.path.substr(f.parent.path.length, 1);
}),
- pathSplit: Class.memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
+ pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
- DoesNotExist: function (path, error) ({
+ DoesNotExist: function DoesNotExist(path, error) ({
path: path,
exists: function () false,
__noSuchMethod__: function () { throw error || Error("Does not exist"); }
* @param {boolean} relative Whether the path is relative or absolute.
* @returns {string}
*/
- expandPath: function (path, relative) {
+ expandPath: function expandPath(path, relative) {
function getenv(name) services.environment.get(name);
// expand any $ENV vars - this is naive but so is Vim and we like to be compatible
// Kris reckons we shouldn't replicate this 'bug'. --djk
// TODO: should we be doing this for all paths?
function expand(path) path.replace(
- !win32 ? /\$(\w+)\b|\${(\w+)}/g
- : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
- function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
- );
+ win32 ? /\$(\w+)\b|\${(\w+)}|%(\w+)%/g
+ : /\$(\w+)\b|\${(\w+)}/g,
+ (m, n1, n2, n3) => (getenv(n1 || n2 || n3) || m));
path = expand(path);
// expand ~
expandPathList: function (list) list.map(this.expandPath),
- readStream: function (ifstream, encoding) {
+ readURL: function readURL(url, encoding) {
+ let channel = services.io.newChannel(url, null, null);
+ channel.contentType = "text/plain";
+ return this.readStream(channel.open(), encoding);
+ },
+
+ readStream: function readStream(ifstream, encoding) {
try {
- var icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
- icstream.init(ifstream, encoding || File.defaultEncoding, 4096, // 4096 bytes buffering
- Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+ var icstream = services.CharsetStream(
+ ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+ services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
let buffer = [];
let str = {};
while (icstream.readString(4096, str) != 0)
}
},
- isAbsolutePath: function (path) {
+ readLines: function readLines(ifstream, encoding) {
+ try {
+ var icstream = services.CharsetStream(
+ ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+ services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ var value = {};
+ while (icstream.readLine(value))
+ yield value.value;
+ }
+ finally {
+ icstream.close();
+ ifstream.close();
+ }
+ },
+
+ isAbsolutePath: function isAbsolutePath(path) {
try {
services.File().initWithPath(path);
return true;
}
},
- joinPaths: function (head, tail, cwd) {
+ joinPaths: function joinPaths(head, tail, cwd) {
let path = this(head, cwd);
try {
// FIXME: should only expand environment vars and normalize path separators
replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
});
+let (file = services.directory.get("ProfD", Ci.nsIFile)) {
+ Object.keys(file).forEach(function (prop) {
+ if (!(prop in File.prototype)) {
+ let isFunction;
+ try {
+ isFunction = callable(file[prop]);
+ }
+ catch (e) {}
+
+ if (isFunction)
+ File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
+ else
+ Object.defineProperty(File.prototype, prop, {
+ configurable: true,
+ get: function wrap_get() this.file[prop],
+ set: function wrap_set(val) { this.file[prop] = val; }
+ });
+ }
+ });
+ file = null;
+}
+
endModule();
-// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
+// catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
-// vim: set fdm=marker sw=4 sts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: