]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/storage.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / modules / storage.jsm
1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("storage", {
9     exports: ["File", "Storage", "storage"],
10     require: ["services", "util"]
11 }, this);
12
13 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
14 var myObject = JSON.parse("{}").constructor;
15
16 function loadData(name, store, type) {
17     try {
18         let data = storage.infoPath.child(name).read();
19         let result = JSON.parse(data);
20         if (result instanceof type)
21             return result;
22     }
23     catch (e) {}
24 }
25
26 function saveData(obj) {
27     if (obj.privateData && storage.privateMode)
28         return;
29     if (obj.store && storage.infoPath)
30         storage.infoPath.child(obj.name).write(obj.serial);
31 }
32
33 var StoreBase = Class("StoreBase", {
34     OPTIONS: ["privateData", "replacer"],
35
36     fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
37
38     get serial() JSON.stringify(this._object, this.replacer),
39
40     init: function (name, store, load, options) {
41         this._load = load;
42
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)
47                 this[k] = v;
48         this.reload();
49     },
50
51     changed: function () { this.timer.tell(); },
52
53     reload: function reload() {
54         this._object = this._load() || this._constructor();
55         this.fireEvent("change", null);
56     },
57
58     delete: function delete_() {
59         delete storage.keys[this.name];
60         delete storage[this.name];
61         storage.infoPath.child(this.name).remove(false);
62     },
63
64     save: function () { saveData(this); },
65
66     __iterator__: function () Iterator(this._object)
67 });
68
69 var ArrayStore = Class("ArrayStore", StoreBase, {
70     _constructor: Array,
71
72     get length() this._object.length,
73
74     set: function set(index, value) {
75         var orig = this._object[index];
76         this._object[index] = value;
77         this.fireEvent("change", index);
78     },
79
80     push: function push(value) {
81         this._object.push(value);
82         this.fireEvent("push", this._object.length);
83     },
84
85     pop: function pop(value) {
86         var res = this._object.pop();
87         this.fireEvent("pop", this._object.length);
88         return res;
89     },
90
91     truncate: function truncate(length, fromEnd) {
92         var res = this._object.length;
93         if (this._object.length > length) {
94             if (fromEnd)
95                 this._object.splice(0, this._object.length - length);
96             this._object.length = length;
97             this.fireEvent("truncate", length);
98         }
99         return res;
100     },
101
102     // XXX: Awkward.
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);
108     },
109
110     get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
111 });
112
113 var ObjectStore = Class("ObjectStore", StoreBase, {
114     _constructor: myObject,
115
116     clear: function () {
117         this._object = {};
118         this.fireEvent("clear");
119     },
120
121     get: function get(key, default_) {
122         return key in this._object  ? this._object[key] :
123                arguments.length > 1 ? this.set(key, default_) :
124                                       undefined;
125     },
126
127     keys: function keys() Object.keys(this._object),
128
129     remove: function remove(key) {
130         var res = this._object[key];
131         delete this._object[key];
132         this.fireEvent("remove", key);
133         return res;
134     },
135
136     set: function set(key, val) {
137         var defined = key in this._object;
138         var orig = this._object[key];
139         this._object[key] = val;
140         if (!defined)
141             this.fireEvent("add", key);
142         else if (orig != val)
143             this.fireEvent("change", key);
144         return val;
145     }
146 });
147
148 var Storage = Module("Storage", {
149     alwaysReload: {},
150
151     init: function () {
152         this.cleanup();
153
154         if (services.bootstrap && !services.bootstrap.session)
155             services.bootstrap.session = {};
156         this.session = services.bootstrap ? services.bootstrap.session : {};
157     },
158
159     cleanup: function () {
160         this.saveAll();
161
162         for (let key in keys(this.keys)) {
163             if (this[key].timer)
164                 this[key].timer.flush();
165             delete this[key];
166         }
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;
171
172         this.keys = {};
173         this.observers = {};
174     },
175
176     exists: function exists(name) this.infoPath.child(name).exists(),
177
178     newObject: function newObject(key, constructor, params) {
179         if (params == null || !isObject(params))
180             throw Error("Invalid argument type");
181
182         if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
183             if (key in this && !(params.reload || this.alwaysReload[key]))
184                 throw Error();
185             let load = function () loadData(key, params.store, params.type || myObject);
186
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]);
190         }
191         return this.keys[key];
192     },
193
194     newMap: function newMap(key, options) {
195         return this.newObject(key, ObjectStore, options);
196     },
197
198     newArray: function newArray(key, options) {
199         return this.newObject(key, ArrayStore, update({ type: Array }, options));
200     },
201
202     addObserver: function addObserver(key, callback, ref) {
203         if (ref) {
204             if (!ref.dactylStorageRefs)
205                 ref.dactylStorageRefs = [];
206             ref.dactylStorageRefs.push(callback);
207             var callbackRef = Cu.getWeakReference(callback);
208         }
209         else {
210             callbackRef = { get: function () callback };
211         }
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 });
217     },
218
219     removeObserver: function (key, callback) {
220         this.removeDeadObservers();
221         if (!(key in this.observers))
222             return;
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];
226     },
227
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));
231             if (!ary.length)
232                 delete this.observers[key];
233         }
234     },
235
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();
244     },
245
246     load: function load(key) {
247         if (this[key].store && this[key].reload)
248             this[key].reload();
249     },
250
251     save: function save(key) {
252         if (this[key])
253             saveData(this.keys[key]);
254     },
255
256     saveAll: function storeAll() {
257         for each (let obj in this.keys)
258             saveData(obj);
259     },
260
261     _privateMode: false,
262     get privateMode() this._privateMode,
263     set privateMode(val) {
264         if (val && !this._privateMode)
265             this.saveAll();
266         if (!val && this._privateMode)
267             for (let key in this.keys)
268                 this.load(key);
269         return this._privateMode = Boolean(val);
270     }
271 }, {
272     Replacer: {
273         skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
274     }
275 }, {
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);
280     },
281
282     cleanup: function (dactyl, modules, window) {
283         delete window.dactylStorageRefs;
284         this.removeDeadObservers();
285     }
286 });
287
288 /**
289  * @class File A class to wrap nsIFile objects and simplify operations
290  * thereon.
291  *
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
295  */
296 var File = Class("File", {
297     init: function (path, checkPWD) {
298         let file = services.File();
299
300         if (path instanceof Ci.nsIFile)
301             file = path.clone();
302         else if (/file:\/\//.test(path))
303             file = services["file:"].getFileFromURLSpec(path);
304         else {
305             try {
306                 let expandedPath = File.expandPath(path);
307
308                 if (!File.isAbsolutePath(expandedPath) && checkPWD)
309                     file = checkPWD.child(expandedPath);
310                 else
311                     file.initWithPath(expandedPath);
312             }
313             catch (e) {
314                 util.reportError(e);
315                 return File.DoesNotExist(path, e);
316             }
317         }
318         let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
319         self.__proto__ = this;
320         return self;
321     },
322
323     /**
324      * @property {nsIFileURL} Returns the nsIFileURL object for this file.
325      */
326     get URI() services.io.newFileURI(this),
327
328     /**
329      * Iterates over the objects in this directory.
330      */
331     iterDirectory: function () {
332         if (!this.exists())
333             throw Error(_("io.noSuchFile"));
334         if (!this.isDirectory())
335             throw Error(_("io.eNotDir"));
336         for (let file in iter(this.directoryEntries))
337             yield File(file);
338     },
339
340     /**
341      * Returns a new file for the given child of this directory entry.
342      */
343     child: function (name) {
344         let f = this.constructor(this);
345         for each (let elem in name.split(File.pathSplit))
346             f.append(elem);
347         return f;
348     },
349
350     /**
351      * Reads this file's entire contents in "text" mode and returns the
352      * content as a string.
353      *
354      * @param {string} encoding The encoding from which to decode the file.
355      *          @default options["fileencoding"]
356      * @returns {string}
357      */
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);
361
362         return File.readStream(ifstream, encoding);
363     },
364
365     /**
366      * Returns the list of files in this directory.
367      *
368      * @param {boolean} sort Whether to sort the returned directory
369      *     entries.
370      * @returns {[nsIFile]}
371      */
372     readDirectory: function (sort) {
373         if (!this.isDirectory())
374             throw Error(_("io.eNotDir"));
375
376         let array = [e for (e in this.iterDirectory())];
377         if (sort)
378             array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
379         return array;
380     },
381
382     /**
383      * Returns a new nsIFileURL object for this file.
384      *
385      * @returns {nsIFileURL}
386      */
387     toURI: function toURI() services.io.newFileURI(this),
388
389     /**
390      * Writes the string *buf* to this file.
391      *
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}
405      * @default ">"
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.
409      * @default 0644
410      * @param {string} encoding The encoding to used to write the file.
411      * @default options["fileencoding"]
412      */
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);
418             return stream;
419         }
420         if (buf instanceof File)
421             buf = buf.read();
422
423         if (!encoding)
424             encoding = File.defaultEncoding;
425
426         if (mode == ">>")
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;
430
431         if (!perms)
432             perms = octal(644);
433         if (!this.exists()) // OCREAT won't create the directory
434             this.create(this.NORMAL_FILE_TYPE, perms);
435
436         ofstream.init(this, mode, perms, 0);
437         try {
438             var ocstream = getStream(0);
439             ocstream.writeString(buf);
440         }
441         catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
442             ocstream.close();
443             ocstream = getStream("?".charCodeAt(0));
444             ocstream.writeString(buf);
445             return false;
446         }
447         finally {
448             try {
449                 ocstream.close();
450             }
451             catch (e) {}
452             ofstream.close();
453         }
454         return true;
455     }
456 }, {
457     /**
458      * @property {number} Open for reading only.
459      * @final
460      */
461     MODE_RDONLY: 0x01,
462
463     /**
464      * @property {number} Open for writing only.
465      * @final
466      */
467     MODE_WRONLY: 0x02,
468
469     /**
470      * @property {number} Open for reading and writing.
471      * @final
472      */
473     MODE_RDWR: 0x04,
474
475     /**
476      * @property {number} If the file does not exist, the file is created.
477      *     If the file exists, this flag has no effect.
478      * @final
479      */
480     MODE_CREATE: 0x08,
481
482     /**
483      * @property {number} The file pointer is set to the end of the file
484      *     prior to each write.
485      * @final
486      */
487     MODE_APPEND: 0x10,
488
489     /**
490      * @property {number} If the file exists, its length is truncated to 0.
491      * @final
492      */
493     MODE_TRUNCATE: 0x20,
494
495     /**
496      * @property {number} If set, each write will wait for both the file
497      *     data and file status to be physically updated.
498      * @final
499      */
500     MODE_SYNC: 0x40,
501
502     /**
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
505      *     is returned.
506      * @final
507      */
508     MODE_EXCL: 0x80,
509
510     /**
511      * @property {string} The current platform's path separator.
512      */
513     PATH_SEP: Class.memoize(function () {
514         let f = services.directory.get("CurProcD", Ci.nsIFile);
515         f.append("foo");
516         return f.path.substr(f.parent.path.length, 1);
517     }),
518
519     pathSplit: Class.memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
520
521     DoesNotExist: function (path, error) ({
522         path: path,
523         exists: function () false,
524         __noSuchMethod__: function () { throw error || Error("Does not exist"); }
525     }),
526
527     defaultEncoding: "UTF-8",
528
529     /**
530      * Expands "~" and environment variables in *path*.
531      *
532      * "~" is expanded to to the value of $HOME. On Windows if this is not
533      * set then the following are tried in order:
534      *   $USERPROFILE
535      *   ${HOMDRIVE}$HOMEPATH
536      *
537      * The variable notation is $VAR (terminated by a non-word character)
538      * or ${VAR}. %VAR% is also supported on Windows.
539      *
540      * @param {string} path The unexpanded path string.
541      * @param {boolean} relative Whether the path is relative or absolute.
542      * @returns {string}
543      */
544     expandPath: function (path, relative) {
545         function getenv(name) services.environment.get(name);
546
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
555         );
556         path = expand(path);
557
558         // expand ~
559         // Yuck.
560         if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
561             // Try $HOME first, on all systems
562             let home = getenv("HOME");
563
564             // Windows has its own idiosyncratic $HOME variables.
565             if (win32 && (!home || !File(home).exists()))
566                 home = getenv("USERPROFILE") ||
567                        getenv("HOMEDRIVE") + getenv("HOMEPATH");
568
569             path = home + path.substr(1);
570         }
571
572         // TODO: Vim expands paths twice, once before checking for ~, once
573         // after, but doesn't document it. Is this just a bug? --Kris
574         path = expand(path);
575         return path.replace("/", File.PATH_SEP, "g");
576     },
577
578     expandPathList: function (list) list.map(this.expandPath),
579
580     readStream: function (ifstream, encoding) {
581         try {
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);
585             let buffer = [];
586             let str = {};
587             while (icstream.readString(4096, str) != 0)
588                 buffer.push(str.value);
589             return buffer.join("");
590         }
591         finally {
592             icstream.close();
593             ifstream.close();
594         }
595     },
596
597     isAbsolutePath: function (path) {
598         try {
599             services.File().initWithPath(path);
600             return true;
601         }
602         catch (e) {
603             return false;
604         }
605     },
606
607     joinPaths: function (head, tail, cwd) {
608         let path = this(head, cwd);
609         try {
610             // FIXME: should only expand environment vars and normalize path separators
611             path.appendRelativePath(this.expandPath(tail, true));
612         }
613         catch (e) {
614             return File.DoesNotExist(e);
615         }
616         return path;
617     },
618
619     replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
620 });
621
622 endModule();
623
624 // catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
625
626 // vim: set fdm=marker sw=4 sts=4 et ft=javascript: