1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("storage", {
9 exports: ["File", "Storage", "storage"],
10 require: ["services", "util"]
13 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
14 var myObject = JSON.parse("{}").constructor;
16 function loadData(name, store, type) {
18 let data = storage.infoPath.child(name).read();
19 let result = JSON.parse(data);
20 if (result instanceof type)
26 function saveData(obj) {
27 if (obj.privateData && storage.privateMode)
29 if (obj.store && storage.infoPath)
30 storage.infoPath.child(obj.name).write(obj.serial);
33 var StoreBase = Class("StoreBase", {
34 OPTIONS: ["privateData", "replacer"],
36 fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
38 get serial() JSON.stringify(this._object, this.replacer),
40 init: function (name, store, load, options) {
43 this.__defineGetter__("store", function () store);
44 this.__defineGetter__("name", function () name);
45 for (let [k, v] in Iterator(options))
46 if (this.OPTIONS.indexOf(k) >= 0)
51 changed: function () { this.timer.tell(); },
53 reload: function reload() {
54 this._object = this._load() || this._constructor();
55 this.fireEvent("change", null);
58 delete: function delete_() {
59 delete storage.keys[this.name];
60 delete storage[this.name];
61 storage.infoPath.child(this.name).remove(false);
64 save: function () { saveData(this); },
66 __iterator__: function () Iterator(this._object)
69 var ArrayStore = Class("ArrayStore", StoreBase, {
72 get length() this._object.length,
74 set: function set(index, value) {
75 var orig = this._object[index];
76 this._object[index] = value;
77 this.fireEvent("change", index);
80 push: function push(value) {
81 this._object.push(value);
82 this.fireEvent("push", this._object.length);
85 pop: function pop(value) {
86 var res = this._object.pop();
87 this.fireEvent("pop", this._object.length);
91 truncate: function truncate(length, fromEnd) {
92 var res = this._object.length;
93 if (this._object.length > length) {
95 this._object.splice(0, this._object.length - length);
96 this._object.length = length;
97 this.fireEvent("truncate", length);
103 mutate: function mutate(funcName) {
104 var _funcName = funcName;
105 arguments[0] = this._object;
106 this._object = Array[_funcName].apply(Array, arguments);
107 this.fireEvent("change", null);
110 get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
113 var ObjectStore = Class("ObjectStore", StoreBase, {
114 _constructor: myObject,
118 this.fireEvent("clear");
121 get: function get(key, default_) {
122 return key in this._object ? this._object[key] :
123 arguments.length > 1 ? this.set(key, default_) :
127 keys: function keys() Object.keys(this._object),
129 remove: function remove(key) {
130 var res = this._object[key];
131 delete this._object[key];
132 this.fireEvent("remove", key);
136 set: function set(key, val) {
137 var defined = key in this._object;
138 var orig = this._object[key];
139 this._object[key] = val;
141 this.fireEvent("add", key);
142 else if (orig != val)
143 this.fireEvent("change", key);
148 var Storage = Module("Storage", {
154 if (services.bootstrap && !services.bootstrap.session)
155 services.bootstrap.session = {};
156 this.session = services.bootstrap ? services.bootstrap.session : {};
159 cleanup: function () {
162 for (let key in keys(this.keys)) {
164 this[key].timer.flush();
167 for (let ary in values(this.observers))
168 for (let obj in values(ary))
169 if (obj.ref && obj.ref.get())
170 delete obj.ref.get().dactylStorageRefs;
176 exists: function exists(name) this.infoPath.child(name).exists(),
178 newObject: function newObject(key, constructor, params) {
179 if (params == null || !isObject(params))
180 throw Error("Invalid argument type");
182 if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
183 if (key in this && !(params.reload || this.alwaysReload[key]))
185 let load = function () loadData(key, params.store, params.type || myObject);
187 this.keys[key] = new constructor(key, params.store, load, params);
188 this.keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
189 this.__defineGetter__(key, function () this.keys[key]);
191 return this.keys[key];
194 newMap: function newMap(key, options) {
195 return this.newObject(key, ObjectStore, options);
198 newArray: function newArray(key, options) {
199 return this.newObject(key, ArrayStore, update({ type: Array }, options));
202 addObserver: function addObserver(key, callback, ref) {
204 if (!ref.dactylStorageRefs)
205 ref.dactylStorageRefs = [];
206 ref.dactylStorageRefs.push(callback);
207 var callbackRef = Cu.getWeakReference(callback);
210 callbackRef = { get: function () callback };
212 this.removeDeadObservers();
213 if (!(key in this.observers))
214 this.observers[key] = [];
215 if (!this.observers[key].some(function (o) o.callback.get() == callback))
216 this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
219 removeObserver: function (key, callback) {
220 this.removeDeadObservers();
221 if (!(key in this.observers))
223 this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
224 if (this.observers[key].length == 0)
225 delete obsevers[key];
228 removeDeadObservers: function () {
229 for (let [key, ary] in Iterator(this.observers)) {
230 this.observers[key] = ary = ary.filter(function (o) o.callback.get() && (!o.ref || o.ref.get() && o.ref.get().dactylStorageRefs));
232 delete this.observers[key];
236 fireEvent: function fireEvent(key, event, arg) {
237 this.removeDeadObservers();
238 if (key in this.observers)
239 // Safe, since we have our own Array object here.
240 for each (let observer in this.observers[key])
241 observer.callback.get()(key, event, arg);
242 if (key in this.keys)
243 this[key].timer.tell();
246 load: function load(key) {
247 if (this[key].store && this[key].reload)
251 save: function save(key) {
253 saveData(this.keys[key]);
256 saveAll: function storeAll() {
257 for each (let obj in this.keys)
262 get privateMode() this._privateMode,
263 set privateMode(val) {
264 if (val && !this._privateMode)
266 if (!val && this._privateMode)
267 for (let key in this.keys)
269 return this._privateMode = Boolean(val);
273 skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
276 init: function init(dactyl, modules) {
277 init.superapply(this, arguments);
278 storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, ""))
279 .child("info").child(dactyl.profileName);
282 cleanup: function (dactyl, modules, window) {
283 delete window.dactylStorageRefs;
284 this.removeDeadObservers();
289 * @class File A class to wrap nsIFile objects and simplify operations
292 * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
293 * @param {boolean} checkPWD Whether to allow expansion relative to the
294 * current directory. @default true
296 var File = Class("File", {
297 init: function (path, checkPWD) {
298 let file = services.File();
300 if (path instanceof Ci.nsIFile)
302 else if (/file:\/\//.test(path))
303 file = services["file:"].getFileFromURLSpec(path);
306 let expandedPath = File.expandPath(path);
308 if (!File.isAbsolutePath(expandedPath) && checkPWD)
309 file = checkPWD.child(expandedPath);
311 file.initWithPath(expandedPath);
315 return File.DoesNotExist(path, e);
318 let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
319 self.__proto__ = this;
324 * @property {nsIFileURL} Returns the nsIFileURL object for this file.
326 get URI() services.io.newFileURI(this),
329 * Iterates over the objects in this directory.
331 iterDirectory: function () {
333 throw Error(_("io.noSuchFile"));
334 if (!this.isDirectory())
335 throw Error(_("io.eNotDir"));
336 for (let file in iter(this.directoryEntries))
341 * Returns a new file for the given child of this directory entry.
343 child: function (name) {
344 let f = this.constructor(this);
345 for each (let elem in name.split(File.pathSplit))
351 * Reads this file's entire contents in "text" mode and returns the
352 * content as a string.
354 * @param {string} encoding The encoding from which to decode the file.
355 * @default options["fileencoding"]
358 read: function (encoding) {
359 let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
360 ifstream.init(this, -1, 0, 0);
362 return File.readStream(ifstream, encoding);
366 * Returns the list of files in this directory.
368 * @param {boolean} sort Whether to sort the returned directory
370 * @returns {[nsIFile]}
372 readDirectory: function (sort) {
373 if (!this.isDirectory())
374 throw Error(_("io.eNotDir"));
376 let array = [e for (e in this.iterDirectory())];
378 array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
383 * Returns a new nsIFileURL object for this file.
385 * @returns {nsIFileURL}
387 toURI: function toURI() services.io.newFileURI(this),
390 * Writes the string *buf* to this file.
392 * @param {string} buf The file content.
393 * @param {string|number} mode The file access mode, a bitwise OR of
394 * the following flags:
395 * {@link #MODE_RDONLY}: 0x01
396 * {@link #MODE_WRONLY}: 0x02
397 * {@link #MODE_RDWR}: 0x04
398 * {@link #MODE_CREATE}: 0x08
399 * {@link #MODE_APPEND}: 0x10
400 * {@link #MODE_TRUNCATE}: 0x20
401 * {@link #MODE_SYNC}: 0x40
402 * Alternatively, the following abbreviations may be used:
403 * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
404 * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
406 * @param {number} perms The file mode bits of the created file. This
407 * is only used when creating a new file and does not change
408 * permissions if the file exists.
410 * @param {string} encoding The encoding to used to write the file.
411 * @default options["fileencoding"]
413 write: function (buf, mode, perms, encoding) {
414 let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
415 function getStream(defaultChar) {
416 let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
417 stream.init(ofstream, encoding, 0, defaultChar);
420 if (buf instanceof File)
424 encoding = File.defaultEncoding;
427 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
428 else if (!mode || mode == ">")
429 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
433 if (!this.exists()) // OCREAT won't create the directory
434 this.create(this.NORMAL_FILE_TYPE, perms);
436 ofstream.init(this, mode, perms, 0);
438 var ocstream = getStream(0);
439 ocstream.writeString(buf);
441 catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
443 ocstream = getStream("?".charCodeAt(0));
444 ocstream.writeString(buf);
458 * @property {number} Open for reading only.
464 * @property {number} Open for writing only.
470 * @property {number} Open for reading and writing.
476 * @property {number} If the file does not exist, the file is created.
477 * If the file exists, this flag has no effect.
483 * @property {number} The file pointer is set to the end of the file
484 * prior to each write.
490 * @property {number} If the file exists, its length is truncated to 0.
496 * @property {number} If set, each write will wait for both the file
497 * data and file status to be physically updated.
503 * @property {number} With MODE_CREATE, if the file does not exist, the
504 * file is created. If the file already exists, no action and NULL
511 * @property {string} The current platform's path separator.
513 PATH_SEP: Class.memoize(function () {
514 let f = services.directory.get("CurProcD", Ci.nsIFile);
516 return f.path.substr(f.parent.path.length, 1);
519 pathSplit: Class.memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
521 DoesNotExist: function (path, error) ({
523 exists: function () false,
524 __noSuchMethod__: function () { throw error || Error("Does not exist"); }
527 defaultEncoding: "UTF-8",
530 * Expands "~" and environment variables in *path*.
532 * "~" is expanded to to the value of $HOME. On Windows if this is not
533 * set then the following are tried in order:
535 * ${HOMDRIVE}$HOMEPATH
537 * The variable notation is $VAR (terminated by a non-word character)
538 * or ${VAR}. %VAR% is also supported on Windows.
540 * @param {string} path The unexpanded path string.
541 * @param {boolean} relative Whether the path is relative or absolute.
544 expandPath: function (path, relative) {
545 function getenv(name) services.environment.get(name);
547 // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
548 // TODO: Vim does not expand variables set to an empty string (and documents it).
549 // Kris reckons we shouldn't replicate this 'bug'. --djk
550 // TODO: should we be doing this for all paths?
551 function expand(path) path.replace(
552 !win32 ? /\$(\w+)\b|\${(\w+)}/g
553 : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
554 function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
560 if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
561 // Try $HOME first, on all systems
562 let home = getenv("HOME");
564 // Windows has its own idiosyncratic $HOME variables.
565 if (win32 && (!home || !File(home).exists()))
566 home = getenv("USERPROFILE") ||
567 getenv("HOMEDRIVE") + getenv("HOMEPATH");
569 path = home + path.substr(1);
572 // TODO: Vim expands paths twice, once before checking for ~, once
573 // after, but doesn't document it. Is this just a bug? --Kris
575 return path.replace("/", File.PATH_SEP, "g");
578 expandPathList: function (list) list.map(this.expandPath),
580 readStream: function (ifstream, encoding) {
582 var icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
583 icstream.init(ifstream, encoding || File.defaultEncoding, 4096, // 4096 bytes buffering
584 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
587 while (icstream.readString(4096, str) != 0)
588 buffer.push(str.value);
589 return buffer.join("");
597 isAbsolutePath: function (path) {
599 services.File().initWithPath(path);
607 joinPaths: function (head, tail, cwd) {
608 let path = this(head, cwd);
610 // FIXME: should only expand environment vars and normalize path separators
611 path.appendRelativePath(this.expandPath(tail, true));
614 return File.DoesNotExist(e);
619 replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
624 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
626 // vim: set fdm=marker sw=4 sts=4 et ft=javascript: