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 this.lazyRequire("config", ["config"]);
14 this.lazyRequire("io", ["IO"]);
16 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
17 var myObject = JSON.parse("{}").constructor;
19 function loadData(name, store, type) {
21 let file = storage.infoPath.child(name);
23 let data = file.read();
24 let result = JSON.parse(data);
25 if (result instanceof type)
34 function saveData(obj) {
35 if (obj.privateData && storage.privateMode)
37 if (obj.store && storage.infoPath)
38 storage.infoPath.child(obj.name).write(obj.serial);
41 var StoreBase = Class("StoreBase", {
42 OPTIONS: ["privateData", "replacer"],
44 fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
46 get serial() JSON.stringify(this._object, this.replacer),
48 init: function (name, store, load, options) {
51 this.__defineGetter__("store", function () store);
52 this.__defineGetter__("name", function () name);
53 for (let [k, v] in Iterator(options))
54 if (this.OPTIONS.indexOf(k) >= 0)
59 changed: function () { this.timer.tell(); },
61 reload: function reload() {
62 this._object = this._load() || this._constructor();
63 this.fireEvent("change", null);
66 delete: function delete_() {
67 delete storage.keys[this.name];
68 delete storage[this.name];
69 storage.infoPath.child(this.name).remove(false);
72 save: function () { saveData(this); },
74 __iterator__: function () Iterator(this._object)
77 var ArrayStore = Class("ArrayStore", StoreBase, {
80 get length() this._object.length,
82 set: function set(index, value, quiet) {
83 var orig = this._object[index];
84 this._object[index] = value;
86 this.fireEvent("change", index);
91 push: function push(value) {
92 this._object.push(value);
93 this.fireEvent("push", this._object.length);
96 pop: function pop(value, ord) {
98 var res = this._object.pop();
100 res = this._object.splice(ord, 1)[0];
102 this.fireEvent("pop", this._object.length, ord);
106 shift: function shift(value) {
107 var res = this._object.shift();
108 this.fireEvent("shift", this._object.length);
112 insert: function insert(value, ord) {
114 this._object.unshift(value);
116 this._object = this._object.slice(0, ord)
118 .concat(this._object.slice(ord));
119 this.fireEvent("insert", this._object.length, ord);
122 truncate: function truncate(length, fromEnd) {
123 var res = this._object.length;
124 if (this._object.length > length) {
126 this._object.splice(0, this._object.length - length);
127 this._object.length = length;
128 this.fireEvent("truncate", length);
134 mutate: function mutate(funcName) {
135 var _funcName = funcName;
136 arguments[0] = this._object;
137 this._object = Array[_funcName].apply(Array, arguments);
138 this.fireEvent("change", null);
141 get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
144 var ObjectStore = Class("ObjectStore", StoreBase, {
145 _constructor: myObject,
149 this.fireEvent("clear");
152 get: function get(key, default_) {
153 return key in this._object ? this._object[key] :
154 arguments.length > 1 ? this.set(key, default_) :
158 keys: function keys() Object.keys(this._object),
160 remove: function remove(key) {
161 var res = this._object[key];
162 delete this._object[key];
163 this.fireEvent("remove", key);
167 set: function set(key, val) {
168 var defined = key in this._object;
169 var orig = this._object[key];
170 this._object[key] = val;
172 this.fireEvent("add", key);
173 else if (orig != val)
174 this.fireEvent("change", key);
179 var Storage = Module("Storage", {
185 if (services.bootstrap && !services.bootstrap.session)
186 services.bootstrap.session = {};
187 this.session = services.bootstrap ? services.bootstrap.session : {};
190 cleanup: function () {
193 for (let key in keys(this.keys)) {
195 this[key].timer.flush();
203 infoPath: Class.Memoize(function ()
204 File(IO.runtimePath.replace(/,.*/, ""))
205 .child("info").child(config.profileName)),
207 exists: function exists(key) this.infoPath.child(key).exists(),
209 remove: function remove(key) {
210 if (this.exists(key)) {
211 if (this[key] && this[key].timer)
212 this[key].timer.flush();
214 delete this.keys[key];
215 this.infoPath.child(key).remove(false);
219 newObject: function newObject(key, constructor, params) {
220 if (params == null || !isObject(params))
221 throw Error("Invalid argument type");
223 if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
224 if (key in this && !(params.reload || this.alwaysReload[key]))
226 let load = function () loadData(key, params.store, params.type || myObject);
228 this.keys[key] = new constructor(key, params.store, load, params);
229 this.keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
230 this.__defineGetter__(key, function () this.keys[key]);
232 return this.keys[key];
235 newMap: function newMap(key, options) {
236 return this.newObject(key, ObjectStore, options);
239 newArray: function newArray(key, options) {
240 return this.newObject(key, ArrayStore, update({ type: Array }, options));
243 addObserver: function addObserver(key, callback, ref) {
245 let refs = overlay.getData(ref, "storage-refs");
247 var callbackRef = util.weakReference(callback);
250 callbackRef = { get: function () callback };
252 this.removeDeadObservers();
253 if (!(key in this.observers))
254 this.observers[key] = [];
255 if (!this.observers[key].some(function (o) o.callback.get() == callback))
256 this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
259 removeObserver: function (key, callback) {
260 this.removeDeadObservers();
261 if (!(key in this.observers))
263 this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
264 if (this.observers[key].length == 0)
265 delete obsevers[key];
268 removeDeadObservers: function () {
269 for (let [key, ary] in Iterator(this.observers)) {
270 this.observers[key] = ary = ary.filter(function (o) o.callback.get()
271 && (!o.ref || o.ref.get()
272 && overlay.getData(o.ref.get(), "storage-refs", null)));
274 delete this.observers[key];
278 fireEvent: function fireEvent(key, event, arg) {
279 this.removeDeadObservers();
280 if (key in this.observers)
281 // Safe, since we have our own Array object here.
282 for each (let observer in this.observers[key])
283 observer.callback.get()(key, event, arg);
284 if (key in this.keys)
285 this[key].timer.tell();
288 load: function load(key) {
289 if (this[key].store && this[key].reload)
293 save: function save(key) {
295 saveData(this.keys[key]);
298 saveAll: function storeAll() {
299 for each (let obj in this.keys)
304 get privateMode() this._privateMode,
305 set privateMode(val) {
306 if (val && !this._privateMode)
308 if (!val && this._privateMode)
309 for (let key in this.keys)
311 return this._privateMode = Boolean(val);
315 skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
318 cleanup: function (dactyl, modules, window) {
319 overlay.setData(window, "storage-refs", null);
320 this.removeDeadObservers();
325 * @class File A class to wrap nsIFile objects and simplify operations
328 * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
329 * @param {boolean} checkPWD Whether to allow expansion relative to the
330 * current directory. @default true
331 * @param {string} charset The charset of the file. @default File.defaultEncoding
333 var File = Class("File", {
334 init: function (path, checkPWD, charset) {
335 let file = services.File();
338 this.charset = charset;
340 if (path instanceof Ci.nsIFileURL)
343 if (path instanceof Ci.nsIFile)
345 else if (/file:\/\//.test(path))
346 file = services["file:"].getFileFromURLSpec(path);
349 let expandedPath = File.expandPath(path);
351 if (!File.isAbsolutePath(expandedPath) && checkPWD)
352 file = checkPWD.child(expandedPath);
354 file.initWithPath(expandedPath);
358 return File.DoesNotExist(path, e);
361 let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
362 self.__proto__ = this;
366 charset: Class.Memoize(function () File.defaultEncoding),
369 * @property {nsIFileURL} Returns the nsIFileURL object for this file.
371 URI: Class.Memoize(function () {
372 let uri = services.io.newFileURI(this).QueryInterface(Ci.nsIFileURL);
373 uri.QueryInterface(Ci.nsIMutable).mutable = false;
378 * Iterates over the objects in this directory.
380 iterDirectory: function () {
382 throw Error(_("io.noSuchFile"));
383 if (!this.isDirectory())
384 throw Error(_("io.eNotDir"));
385 for (let file in iter(this.directoryEntries))
390 * Returns a new file for the given child of this directory entry.
392 child: function (name) {
393 let f = this.constructor(this);
394 for each (let elem in name.split(File.pathSplit))
400 * Returns an iterator for all lines in a file.
402 get lines() File.readLines(services.FileInStream(this, -1, 0, 0),
406 * Reads this file's entire contents in "text" mode and returns the
407 * content as a string.
409 * @param {string} encoding The encoding from which to decode the file.
413 read: function (encoding) {
414 let ifstream = services.FileInStream(this, -1, 0, 0);
416 return File.readStream(ifstream, encoding || this.charset);
420 * Returns the list of files in this directory.
422 * @param {boolean} sort Whether to sort the returned directory
424 * @returns {[nsIFile]}
426 readDirectory: function (sort) {
427 if (!this.isDirectory())
428 throw Error(_("io.eNotDir"));
430 let array = [e for (e in this.iterDirectory())];
432 array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
437 * Returns a new nsIFileURL object for this file.
439 * @returns {nsIFileURL}
441 toURI: function toURI() services.io.newFileURI(this),
444 * Writes the string *buf* to this file.
446 * @param {string} buf The file content.
447 * @param {string|number} mode The file access mode, a bitwise OR of
448 * the following flags:
449 * {@link #MODE_RDONLY}: 0x01
450 * {@link #MODE_WRONLY}: 0x02
451 * {@link #MODE_RDWR}: 0x04
452 * {@link #MODE_CREATE}: 0x08
453 * {@link #MODE_APPEND}: 0x10
454 * {@link #MODE_TRUNCATE}: 0x20
455 * {@link #MODE_SYNC}: 0x40
456 * Alternatively, the following abbreviations may be used:
457 * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
458 * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
460 * @param {number} perms The file mode bits of the created file. This
461 * is only used when creating a new file and does not change
462 * permissions if the file exists.
464 * @param {string} encoding The encoding to used to write the file.
467 write: function (buf, mode, perms, encoding) {
468 function getStream(defaultChar) {
469 return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
471 if (buf instanceof File)
475 encoding = this.charset;
478 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
479 else if (!mode || mode == ">")
480 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
484 if (!this.exists()) // OCREAT won't create the directory
485 this.create(this.NORMAL_FILE_TYPE, perms);
487 let ofstream = services.FileOutStream(this, mode, perms, 0);
489 var ocstream = getStream(0);
490 ocstream.writeString(buf);
492 catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
494 ocstream = getStream("?".charCodeAt(0));
495 ocstream.writeString(buf);
509 * @property {number} Open for reading only.
515 * @property {number} Open for writing only.
521 * @property {number} Open for reading and writing.
527 * @property {number} If the file does not exist, the file is created.
528 * If the file exists, this flag has no effect.
534 * @property {number} The file pointer is set to the end of the file
535 * prior to each write.
541 * @property {number} If the file exists, its length is truncated to 0.
547 * @property {number} If set, each write will wait for both the file
548 * data and file status to be physically updated.
554 * @property {number} With MODE_CREATE, if the file does not exist, the
555 * file is created. If the file already exists, no action and NULL
562 * @property {string} The current platform's path separator.
564 PATH_SEP: Class.Memoize(function () {
565 let f = services.directory.get("CurProcD", Ci.nsIFile);
567 return f.path.substr(f.parent.path.length, 1);
570 pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
572 DoesNotExist: function DoesNotExist(path, error) ({
574 exists: function () false,
575 __noSuchMethod__: function () { throw error || Error("Does not exist"); }
578 defaultEncoding: "UTF-8",
581 * Expands "~" and environment variables in *path*.
583 * "~" is expanded to to the value of $HOME. On Windows if this is not
584 * set then the following are tried in order:
586 * ${HOMDRIVE}$HOMEPATH
588 * The variable notation is $VAR (terminated by a non-word character)
589 * or ${VAR}. %VAR% is also supported on Windows.
591 * @param {string} path The unexpanded path string.
592 * @param {boolean} relative Whether the path is relative or absolute.
595 expandPath: function expandPath(path, relative) {
596 function getenv(name) services.environment.get(name);
598 // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
599 // TODO: Vim does not expand variables set to an empty string (and documents it).
600 // Kris reckons we shouldn't replicate this 'bug'. --djk
601 // TODO: should we be doing this for all paths?
602 function expand(path) path.replace(
603 !win32 ? /\$(\w+)\b|\${(\w+)}/g
604 : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
605 function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
611 if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
612 // Try $HOME first, on all systems
613 let home = getenv("HOME");
615 // Windows has its own idiosyncratic $HOME variables.
616 if (win32 && (!home || !File(home).exists()))
617 home = getenv("USERPROFILE") ||
618 getenv("HOMEDRIVE") + getenv("HOMEPATH");
620 path = home + path.substr(1);
623 // TODO: Vim expands paths twice, once before checking for ~, once
624 // after, but doesn't document it. Is this just a bug? --Kris
626 return path.replace("/", File.PATH_SEP, "g");
629 expandPathList: function (list) list.map(this.expandPath),
631 readURL: function readURL(url, encoding) {
632 let channel = services.io.newChannel(url, null, null);
633 channel.contentType = "text/plain";
634 return this.readStream(channel.open(), encoding);
637 readStream: function readStream(ifstream, encoding) {
639 var icstream = services.CharsetStream(
640 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
641 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
645 while (icstream.readString(4096, str) != 0)
646 buffer.push(str.value);
647 return buffer.join("");
655 readLines: function readLines(ifstream, encoding) {
657 var icstream = services.CharsetStream(
658 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
659 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
662 while (icstream.readLine(value))
672 isAbsolutePath: function isAbsolutePath(path) {
674 services.File().initWithPath(path);
682 joinPaths: function joinPaths(head, tail, cwd) {
683 let path = this(head, cwd);
685 // FIXME: should only expand environment vars and normalize path separators
686 path.appendRelativePath(this.expandPath(tail, true));
689 return File.DoesNotExist(e);
694 replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
699 // catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
701 // vim: set fdm=marker sw=4 sts=4 et ft=javascript: