1 // Copyright (c) 2008-2012 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 defineModule("storage", {
8 exports: ["File", "Storage", "storage"],
9 require: ["services", "util"]
12 lazyRequire("config", ["config"]);
13 lazyRequire("io", ["IO"]);
14 lazyRequire("overlay", ["overlay"]);
16 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
17 var myObject = JSON.parse("{}").constructor;
19 var StoreBase = Class("StoreBase", {
20 OPTIONS: ["privateData", "replacer"],
22 fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
24 get serial() JSON.stringify(this._object, this.replacer),
26 init: function (name, store, load, options) {
28 this._options = options;
30 this.__defineGetter__("store", function () store);
31 this.__defineGetter__("name", function () name);
32 for (let [k, v] in Iterator(options))
33 if (this.OPTIONS.indexOf(k) >= 0)
38 clone: function (storage) {
39 let store = storage.privateMode ? false : this.store;
40 let res = this.constructor(this.name, store, this._load, this._options);
41 res.storage = storage;
45 changed: function () { this.timer && this.timer.tell(); },
47 reload: function reload() {
48 this._object = this._load() || this._constructor();
49 this.fireEvent("change", null);
52 delete: function delete_() {
53 delete storage.keys[this.name];
54 delete storage[this.name];
55 storage.infoPath.child(this.name).remove(false);
58 save: function () { (self.storage || storage)._saveData(this); },
60 __iterator__: function () Iterator(this._object)
63 var ArrayStore = Class("ArrayStore", StoreBase, {
66 get length() this._object.length,
68 set: function set(index, value, quiet) {
69 var orig = this._object[index];
70 this._object[index] = value;
72 this.fireEvent("change", index);
77 push: function push(value) {
78 this._object.push(value);
79 this.fireEvent("push", this._object.length);
82 pop: function pop(value, ord) {
84 var res = this._object.pop();
86 res = this._object.splice(ord, 1)[0];
88 this.fireEvent("pop", this._object.length, ord);
92 shift: function shift(value) {
93 var res = this._object.shift();
94 this.fireEvent("shift", this._object.length);
98 insert: function insert(value, ord) {
100 this._object.unshift(value);
102 this._object = this._object.slice(0, ord)
104 .concat(this._object.slice(ord));
105 this.fireEvent("insert", this._object.length, ord);
108 truncate: function truncate(length, fromEnd) {
109 var res = this._object.length;
110 if (this._object.length > length) {
112 this._object.splice(0, this._object.length - length);
113 this._object.length = length;
114 this.fireEvent("truncate", length);
120 mutate: function mutate(funcName) {
121 var _funcName = funcName;
122 arguments[0] = this._object;
123 this._object = Array[_funcName].apply(Array, arguments);
124 this.fireEvent("change", null);
127 get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
130 var ObjectStore = Class("ObjectStore", StoreBase, {
131 _constructor: myObject,
135 this.fireEvent("clear");
138 get: function get(key, default_) {
139 return key in this._object ? this._object[key] :
140 arguments.length > 1 ? this.set(key, default_) :
144 keys: function keys() Object.keys(this._object),
146 remove: function remove(key) {
147 var res = this._object[key];
148 delete this._object[key];
149 this.fireEvent("remove", key);
153 set: function set(key, val) {
154 var defined = key in this._object;
155 var orig = this._object[key];
156 this._object[key] = val;
158 this.fireEvent("add", key);
159 else if (orig != val)
160 this.fireEvent("change", key);
165 var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
167 var Storage = Module("Storage", {
168 Local: function Local(dactyl, modules, window) ({
169 init: function init() {
170 this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
176 init: function init() {
179 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
180 if (!Services.dactylSession)
181 Services.dactylSession = Cu.createObjectIn(sessionGlobal);
182 this.session = Services.dactylSession;
185 cleanup: function () {
188 for (let key in keys(this.keys)) {
190 this[key].timer.flush();
198 _loadData: function loadData(name, store, type) {
200 let file = storage.infoPath.child(name);
202 let data = file.read();
203 let result = JSON.parse(data);
204 if (result instanceof type)
213 _saveData: function saveData(obj) {
214 if (obj.privateData && storage.privateMode)
216 if (obj.store && storage.infoPath)
217 storage.infoPath.child(obj.name).write(obj.serial);
220 storeForSession: function storeForSession(key, val) {
222 this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
224 delete this.dactylSession[key];
227 infoPath: Class.Memoize(function ()
228 File(IO.runtimePath.replace(/,.*/, ""))
229 .child("info").child(config.profileName)),
231 exists: function exists(key) this.infoPath.child(key).exists(),
233 remove: function remove(key) {
234 if (this.exists(key)) {
235 if (this[key] && this[key].timer)
236 this[key].timer.flush();
238 delete this.keys[key];
239 this.infoPath.child(key).remove(false);
243 newObject: function newObject(key, constructor, params) {
244 if (params == null || !isObject(params))
245 throw Error("Invalid argument type");
247 if (this.isLocalModule) {
248 this.globalInstance.newObject.apply(this.globalInstance, arguments);
250 if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
251 let obj = this.globalInstance.keys[key];
252 this.keys[key] = this._privatize(obj);
255 return this.keys[key];
258 let reload = params.reload || this.alwaysReload[key];
259 if (!(key in this.keys) || reload) {
260 if (key in this && !reload)
261 throw Error("Cannot add storage key with that name.");
263 let load = () => this._loadData(key, params.store, params.type || myObject);
265 this.keys[key] = new constructor(key, params.store, load, params);
266 this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
267 this.__defineGetter__(key, function () this.keys[key]);
269 return this.keys[key];
272 newMap: function newMap(key, options) {
273 return this.newObject(key, ObjectStore, options);
276 newArray: function newArray(key, options) {
277 return this.newObject(key, ArrayStore, update({ type: Array }, options));
280 addObserver: function addObserver(key, callback, ref) {
282 let refs = overlay.getData(ref, "storage-refs");
284 var callbackRef = util.weakReference(callback);
287 callbackRef = { get: function () callback };
290 this.removeDeadObservers();
292 if (!(key in this.observers))
293 this.observers[key] = [];
295 if (!this.observers[key].some(function (o) o.callback.get() == callback))
296 this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
299 removeObserver: function (key, callback) {
300 this.removeDeadObservers();
302 if (!(key in this.observers))
305 this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
306 if (this.observers[key].length == 0)
307 delete obsevers[key];
310 removeDeadObservers: function () {
312 if (!o.callback.get())
315 let ref = o.ref && o.ref.get();
316 return ref && !ref.closed && overlay.getData(ref, "storage-refs", null);
319 for (let [key, ary] in Iterator(this.observers)) {
320 this.observers[key] = ary = ary.filter(filter);
322 delete this.observers[key];
326 fireEvent: function fireEvent(key, event, arg) {
327 this.removeDeadObservers();
329 if (key in this.observers)
330 // Safe, since we have our own Array object here.
331 for each (let observer in this.observers[key])
332 observer.callback.get()(key, event, arg);
334 if (key in this.keys && this.keys[key].timer)
335 this[key].timer.tell();
338 load: function load(key) {
339 if (this[key].store && this[key].reload)
343 save: function save(key) {
345 this._saveData(this.keys[key]);
348 saveAll: function storeAll() {
349 for each (let obj in this.keys)
354 get privateMode() this._privateMode,
355 set privateMode(enabled) {
356 this._privateMode = Boolean(enabled);
358 if (this.isLocalModule) {
366 for (let [k, v] in Iterator(keys))
367 this.keys[k] = this._privatize(v);
370 return this._privateMode;
373 _privatize: function privatize(obj) {
374 if (obj.privateData && obj.clone)
375 return obj.clone(this);
380 skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
383 cleanup: function (dactyl, modules, window) {
384 overlay.setData(window, "storage-refs", null);
385 this.removeDeadObservers();
390 * @class File A class to wrap nsIFile objects and simplify operations
393 * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
394 * @param {boolean} checkPWD Whether to allow expansion relative to the
395 * current directory. @default true
396 * @param {string} charset The charset of the file. @default File.defaultEncoding
398 var File = Class("File", {
399 init: function (path, checkPWD, charset) {
400 let file = services.File();
403 this.charset = charset;
405 if (path instanceof Ci.nsIFileURL)
408 if (path instanceof Ci.nsIFile || path instanceof File)
410 else if (/file:\/\//.test(path))
411 file = services["file:"].getFileFromURLSpec(path);
414 let expandedPath = File.expandPath(path);
416 if (!File.isAbsolutePath(expandedPath) && checkPWD)
417 file = checkPWD.child(expandedPath);
419 file.initWithPath(expandedPath);
423 return File.DoesNotExist(path, e);
426 this.file = file.QueryInterface(Ci.nsILocalFile);
430 charset: Class.Memoize(function () File.defaultEncoding),
433 * @property {nsIFileURL} Returns the nsIFileURL object for this file.
435 URI: Class.Memoize(function () {
436 let uri = services.io.newFileURI(this.file)
437 .QueryInterface(Ci.nsIFileURL);
438 uri.QueryInterface(Ci.nsIMutable).mutable = false;
443 * Iterates over the objects in this directory.
445 iterDirectory: function iterDirectory() {
447 throw Error(_("io.noSuchFile"));
448 if (!this.isDirectory())
449 throw Error(_("io.eNotDir"));
450 for (let file in iter(this.directoryEntries))
455 * Returns a new file for the given child of this directory entry.
457 child: function child() {
458 let f = this.constructor(this);
459 for (let [, name] in Iterator(arguments))
460 for each (let elem in name.split(File.pathSplit))
466 * Returns an iterator for all lines in a file.
468 get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
472 * Reads this file's entire contents in "text" mode and returns the
473 * content as a string.
475 * @param {string} encoding The encoding from which to decode the file.
479 read: function read(encoding) {
480 let ifstream = services.FileInStream(this.file, -1, 0, 0);
482 return File.readStream(ifstream, encoding || this.charset);
486 * Returns the list of files in this directory.
488 * @param {boolean} sort Whether to sort the returned directory
490 * @returns {[nsIFile]}
492 readDirectory: function readDirectory(sort) {
493 if (!this.isDirectory())
494 throw Error(_("io.eNotDir"));
496 let array = [e for (e in this.iterDirectory())];
498 array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
503 * Returns a new nsIFileURL object for this file.
505 * @returns {nsIFileURL}
507 toURI: function toURI() services.io.newFileURI(this.file),
510 * Writes the string *buf* to this file.
512 * @param {string} buf The file content.
513 * @param {string|number} mode The file access mode, a bitwise OR of
514 * the following flags:
515 * {@link #MODE_RDONLY}: 0x01
516 * {@link #MODE_WRONLY}: 0x02
517 * {@link #MODE_RDWR}: 0x04
518 * {@link #MODE_CREATE}: 0x08
519 * {@link #MODE_APPEND}: 0x10
520 * {@link #MODE_TRUNCATE}: 0x20
521 * {@link #MODE_SYNC}: 0x40
522 * Alternatively, the following abbreviations may be used:
523 * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
524 * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
526 * @param {number} perms The file mode bits of the created file. This
527 * is only used when creating a new file and does not change
528 * permissions if the file exists.
530 * @param {string} encoding The encoding to used to write the file.
533 write: function write(buf, mode, perms, encoding) {
534 function getStream(defaultChar) {
535 return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
537 if (buf instanceof File)
541 encoding = this.charset;
544 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
545 else if (!mode || mode == ">")
546 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
550 if (!this.exists()) // OCREAT won't create the directory
551 this.create(this.NORMAL_FILE_TYPE, perms);
553 let ofstream = services.FileOutStream(this.file, mode, perms, 0);
555 var ocstream = getStream(0);
556 ocstream.writeString(buf);
558 catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
560 ocstream = getStream("?".charCodeAt(0));
561 ocstream.writeString(buf);
574 // Wrapped native methods:
575 copyTo: function copyTo(dir, name)
576 this.file.copyTo(this.constructor(dir).file,
579 copyToFollowingLinks: function copyToFollowingLinks(dir, name)
580 this.file.copyToFollowingLinks(this.constructor(dir).file,
583 moveTo: function moveTo(dir, name)
584 this.file.moveTo(this.constructor(dir).file,
587 equals: function equals(file)
588 this.file.equals(this.constructor(file).file),
590 contains: function contains(dir, recur)
591 this.file.contains(this.constructor(dir).file,
594 getRelativeDescriptor: function getRelativeDescriptor(file)
595 this.file.getRelativeDescriptor(this.constructor(file).file),
597 setRelativeDescriptor: function setRelativeDescriptor(file, path)
598 this.file.setRelativeDescriptor(this.constructor(file).file,
602 * @property {number} Open for reading only.
608 * @property {number} Open for writing only.
614 * @property {number} Open for reading and writing.
620 * @property {number} If the file does not exist, the file is created.
621 * If the file exists, this flag has no effect.
627 * @property {number} The file pointer is set to the end of the file
628 * prior to each write.
634 * @property {number} If the file exists, its length is truncated to 0.
640 * @property {number} If set, each write will wait for both the file
641 * data and file status to be physically updated.
647 * @property {number} With MODE_CREATE, if the file does not exist, the
648 * file is created. If the file already exists, no action and NULL
655 * @property {string} The current platform's path separator.
657 PATH_SEP: Class.Memoize(function () {
658 let f = services.directory.get("CurProcD", Ci.nsIFile);
660 return f.path.substr(f.parent.path.length, 1);
663 pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
665 DoesNotExist: function DoesNotExist(path, error) ({
667 exists: function () false,
668 __noSuchMethod__: function () { throw error || Error("Does not exist"); }
671 defaultEncoding: "UTF-8",
674 * Expands "~" and environment variables in *path*.
676 * "~" is expanded to to the value of $HOME. On Windows if this is not
677 * set then the following are tried in order:
679 * ${HOMDRIVE}$HOMEPATH
681 * The variable notation is $VAR (terminated by a non-word character)
682 * or ${VAR}. %VAR% is also supported on Windows.
684 * @param {string} path The unexpanded path string.
685 * @param {boolean} relative Whether the path is relative or absolute.
688 expandPath: function expandPath(path, relative) {
689 function getenv(name) services.environment.get(name);
691 // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
692 // TODO: Vim does not expand variables set to an empty string (and documents it).
693 // Kris reckons we shouldn't replicate this 'bug'. --djk
694 // TODO: should we be doing this for all paths?
695 function expand(path) path.replace(
696 !win32 ? /\$(\w+)\b|\${(\w+)}/g
697 : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
698 function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
704 if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
705 // Try $HOME first, on all systems
706 let home = getenv("HOME");
708 // Windows has its own idiosyncratic $HOME variables.
709 if (win32 && (!home || !File(home).exists()))
710 home = getenv("USERPROFILE") ||
711 getenv("HOMEDRIVE") + getenv("HOMEPATH");
713 path = home + path.substr(1);
716 // TODO: Vim expands paths twice, once before checking for ~, once
717 // after, but doesn't document it. Is this just a bug? --Kris
719 return path.replace("/", File.PATH_SEP, "g");
722 expandPathList: function (list) list.map(this.expandPath),
724 readURL: function readURL(url, encoding) {
725 let channel = services.io.newChannel(url, null, null);
726 channel.contentType = "text/plain";
727 return this.readStream(channel.open(), encoding);
730 readStream: function readStream(ifstream, encoding) {
732 var icstream = services.CharsetStream(
733 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
734 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
738 while (icstream.readString(4096, str) != 0)
739 buffer.push(str.value);
740 return buffer.join("");
748 readLines: function readLines(ifstream, encoding) {
750 var icstream = services.CharsetStream(
751 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
752 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
755 while (icstream.readLine(value))
764 isAbsolutePath: function isAbsolutePath(path) {
766 services.File().initWithPath(path);
774 joinPaths: function joinPaths(head, tail, cwd) {
775 let path = this(head, cwd);
777 // FIXME: should only expand environment vars and normalize path separators
778 path.appendRelativePath(this.expandPath(tail, true));
781 return File.DoesNotExist(e);
786 replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
789 let (file = services.directory.get("ProfD", Ci.nsIFile)) {
790 Object.keys(file).forEach(function (prop) {
791 if (!(prop in File.prototype)) {
794 isFunction = callable(file[prop]);
799 File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
801 Object.defineProperty(File.prototype, prop, {
803 get: function wrap_get() this.file[prop],
804 set: function wrap_set(val) { this.file[prop] = val; }
813 // catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
815 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: