1 // Copyright (c) 2008-2014 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: ["promises", "services", "util"]
12 lazyRequire("config", ["config"]);
13 lazyRequire("io", ["IO"]);
14 lazyRequire("overlay", ["overlay"]);
16 lazyRequire("resource://gre/modules/osfile.jsm", ["OS"]);
18 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
19 var myObject = JSON.parse("{}").constructor;
21 var global = Cu.getGlobalForObject(this);
23 var StoreBase = Class("StoreBase", {
24 OPTIONS: ["privateData", "replacer"],
26 fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
28 get serial() JSON.stringify(this._object, this.replacer),
30 init: function init(name, store, load, options) {
32 this._options = options;
34 this.__defineGetter__("store", () => store);
35 this.__defineGetter__("name", () => name);
36 for (let [k, v] in Iterator(options))
37 if (this.OPTIONS.indexOf(k) >= 0)
42 clone: function clone(storage) {
43 let store = storage.privateMode ? false : this.store;
44 let res = this.constructor(this.name, store, this._load, this._options);
45 res.storage = storage;
49 makeOwn: function makeOwn(val) {
50 if (typeof val != "object")
52 if (Cu.getGlobalForObject(val) == global)
54 return JSON.parse(JSON.stringify(val, this.replacer));
57 changed: function () { this.timer && this.timer.tell(); },
59 reload: function reload() {
60 this._object = this._load() || this._constructor();
61 this.fireEvent("change", null);
64 delete: function delete_() {
65 delete storage.keys[this.name];
66 delete storage[this.name];
67 return OS.File.remove(
68 storage.infoPath.child(this.name).path);
71 save: function () { (self.storage || storage)._saveData(this); },
73 __iterator__: function () Iterator(this._object)
76 var ArrayStore = Class("ArrayStore", StoreBase, {
79 get length() this._object.length,
81 set: function set(index, value, quiet) {
82 var orig = this._object[index];
83 this._object[index] = this.makeOwn(value);
85 this.fireEvent("change", index);
90 push: function push(value) {
91 this._object.push(this.makeOwn(value));
92 this.fireEvent("push", this._object.length);
95 pop: function pop(value, ord) {
97 var res = this._object.pop();
99 res = this._object.splice(ord, 1)[0];
101 this.fireEvent("pop", this._object.length, ord);
105 shift: function shift(value) {
106 var res = this._object.shift();
107 this.fireEvent("shift", this._object.length);
111 insert: function insert(value, ord) {
112 value = this.makeOwn(value);
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 .map(this.makeOwn.bind(this));
139 this.fireEvent("change", null);
142 get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
145 var ObjectStore = Class("ObjectStore", StoreBase, {
146 _constructor: myObject,
150 this.fireEvent("clear");
153 get: function get(key, default_) {
154 return this.has(key) ? this._object[key] :
155 arguments.length > 1 ? this.set(key, default_) :
159 has: function has(key) hasOwnProperty(this._object, key),
161 keys: function keys() Object.keys(this._object),
163 remove: function remove(key) {
164 var res = this._object[key];
165 delete this._object[key];
166 this.fireEvent("remove", key);
170 set: function set(key, val) {
171 var defined = key in this._object;
172 var orig = this._object[key];
173 this._object[key] = this.makeOwn(val);
175 this.fireEvent("add", key);
176 else if (orig != val)
177 this.fireEvent("change", key);
182 var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
184 var Storage = Module("Storage", {
185 Local: function Local(dactyl, modules, window) ({
186 init: function init() {
187 this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
193 init: function init() {
196 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
197 if (!Services.dactylSession)
198 Services.dactylSession = Cu.createObjectIn(sessionGlobal);
199 this.session = Services.dactylSession;
202 cleanup: function () {
205 for (let key in keys(this.keys)) {
207 this[key].timer.flush();
215 _loadData: function loadData(name, store, type) {
217 let file = storage.infoPath.child(name);
219 let data = file.read();
220 let result = JSON.parse(data);
221 if (result instanceof type)
230 _saveData: promises.task(function saveData(obj) {
231 if (obj.privateData && storage.privateMode)
233 if (obj.store && storage.infoPath) {
234 var { path } = storage.infoPath.child(obj.name);
235 yield OS.File.makeDir(storage.infoPath.path,
236 { ignoreExisting: true });
237 yield OS.File.writeAtomic(
239 { tmpPath: path + ".part" });
243 storeForSession: function storeForSession(key, val) {
245 this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
247 delete this.dactylSession[key];
250 infoPath: Class.Memoize(() =>
251 File(IO.runtimePath.replace(/,.*/, ""))
252 .child("info").child(config.profileName)),
254 exists: function exists(key) this.infoPath.child(key).exists(),
256 remove: function remove(key) {
257 if (this.exists(key)) {
258 if (this[key] && this[key].timer)
259 this[key].timer.flush();
261 delete this.keys[key];
262 return OS.File.remove(
263 this.infoPath.child(key).path);
267 newObject: function newObject(key, constructor, params={}) {
268 if (params == null || !isObject(params))
269 throw Error("Invalid argument type");
271 if (this.isLocalModule) {
272 this.globalInstance.newObject.apply(this.globalInstance, arguments);
274 if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
275 let obj = this.globalInstance.keys[key];
276 this.keys[key] = this._privatize(obj);
279 return this.keys[key];
282 let reload = params.reload || this.alwaysReload[key];
283 if (!(key in this.keys) || reload) {
284 if (key in this && !reload)
285 throw Error("Cannot add storage key with that name.");
287 let load = () => this._loadData(key, params.store, params.type || myObject);
289 this.keys[key] = new constructor(key, params.store, load, params);
290 this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
291 this.__defineGetter__(key, function () this.keys[key]);
293 return this.keys[key];
296 newMap: function newMap(key, options={}) {
297 return this.newObject(key, ObjectStore, options);
300 newArray: function newArray(key, options={}) {
301 return this.newObject(key, ArrayStore, update({ type: Array }, options));
305 yield this.observers;
306 for (let window of overlay.windows)
307 yield overlay.getData(window, "storage-observers", Object);
310 addObserver: function addObserver(key, callback, window) {
311 var { observers } = this;
312 if (window instanceof Ci.nsIDOMWindow)
313 observers = overlay.getData(window, "storage-observers", Object);
315 if (!hasOwnProperty(observers, key))
316 observers[key] = RealSet();
318 observers[key].add(callback);
321 removeObserver: function (key, callback) {
322 for (let observers in this.observerMaps)
323 if (key in observers)
324 observers[key].remove(callback);
327 fireEvent: function fireEvent(key, event, arg) {
328 for (let observers in this.observerMaps)
329 for (let observer of observers[key] || [])
330 observer(key, event, arg);
332 if (key in this.keys && this.keys[key].timer)
333 this[key].timer.tell();
336 load: function load(key) {
337 if (this[key].store && this[key].reload)
341 save: function save(key) {
343 this._saveData(this.keys[key]);
346 saveAll: function storeAll() {
347 for each (let obj in this.keys)
352 get privateMode() this._privateMode,
353 set privateMode(enabled) {
354 this._privateMode = Boolean(enabled);
356 if (this.isLocalModule) {
364 for (let [k, v] in Iterator(keys))
365 this.keys[k] = this._privatize(v);
368 return this._privateMode;
371 _privatize: function privatize(obj) {
372 if (obj.privateData && obj.clone)
373 return obj.clone(this);
378 skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
381 cleanup: function (dactyl, modules, window) {
382 overlay.setData(window, "storage-callbacks", undefined);
387 * @class File A class to wrap nsIFile objects and simplify operations
390 * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
391 * @param {boolean} checkPWD Whether to allow expansion relative to the
392 * current directory. @default true
393 * @param {string} charset The charset of the file. @default File.defaultEncoding
395 var File = Class("File", {
396 init: function (path, checkPWD, charset) {
397 let file = services.File();
400 this.charset = charset;
402 if (path instanceof Ci.nsIFileURL)
405 if (path instanceof Ci.nsIFile || path instanceof File)
407 else if (/file:\/\//.test(path))
408 file = services["file:"].getFileFromURLSpec(path);
411 let expandedPath = File.expandPath(path);
413 if (!File.isAbsolutePath(expandedPath) && checkPWD)
414 file = checkPWD.child(expandedPath);
416 file.initWithPath(expandedPath);
420 return File.DoesNotExist(path, e);
423 this.file = file.QueryInterface(Ci.nsILocalFile);
427 charset: Class.Memoize(() => File.defaultEncoding),
430 * @property {nsIFileURL} Returns the nsIFileURL object for this file.
432 URI: Class.Memoize(function () {
433 let uri = services.io.newFileURI(this.file)
434 .QueryInterface(Ci.nsIFileURL);
435 uri.QueryInterface(Ci.nsIMutable).mutable = false;
440 * Iterates over the objects in this directory.
442 iterDirectory: function iterDirectory() {
444 throw Error(_("io.noSuchFile"));
445 if (!this.isDirectory())
446 throw Error(_("io.eNotDir"));
447 for (let file in iter(this.directoryEntries))
452 * Returns a new file for the given child of this directory entry.
454 child: function child() {
455 let f = this.constructor(this);
456 for (let [, name] in Iterator(arguments))
457 for (let elem of name.split(File.pathSplit))
463 * Returns an iterator for all lines in a file.
465 get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
469 * Reads this file's entire contents in "text" mode and returns the
470 * content as a string.
472 * @param {string} encoding The encoding from which to decode the file.
476 read: function read(encoding) {
477 let ifstream = services.FileInStream(this.file, -1, 0, 0);
479 return File.readStream(ifstream, encoding || this.charset);
483 * Returns the list of files in this directory.
485 * @param {boolean} sort Whether to sort the returned directory
487 * @returns {[nsIFile]}
489 readDirectory: function readDirectory(sort) {
490 if (!this.isDirectory())
491 throw Error(_("io.eNotDir"));
493 let array = [e for (e in this.iterDirectory())];
495 array.sort((a, b) => (b.isDirectory() - a.isDirectory() ||
496 String.localeCompare(a.path, b.path)));
501 * Returns a new nsIFileURL object for this file.
503 * @returns {nsIFileURL}
505 toURI: function toURI() services.io.newFileURI(this.file),
508 * Writes the string *buf* to this file.
510 * @param {string} buf The file content.
511 * @param {string|number} mode The file access mode, a bitwise OR of
512 * the following flags:
513 * {@link #MODE_RDONLY}: 0x01
514 * {@link #MODE_WRONLY}: 0x02
515 * {@link #MODE_RDWR}: 0x04
516 * {@link #MODE_CREATE}: 0x08
517 * {@link #MODE_APPEND}: 0x10
518 * {@link #MODE_TRUNCATE}: 0x20
519 * {@link #MODE_SYNC}: 0x40
520 * Alternatively, the following abbreviations may be used:
521 * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
522 * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
524 * @param {number} perms The file mode bits of the created file. This
525 * is only used when creating a new file and does not change
526 * permissions if the file exists.
528 * @param {string} encoding The encoding to used to write the file.
531 write: function write(buf, mode, perms, encoding) {
532 function getStream(defaultChar) {
533 return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
535 if (buf instanceof File)
539 encoding = this.charset;
542 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
543 else if (!mode || mode == ">")
544 mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
548 if (!this.exists()) // OCREAT won't create the directory
549 this.create(this.NORMAL_FILE_TYPE, perms);
551 let ofstream = services.FileOutStream(this.file, mode, perms, 0);
553 var ocstream = getStream(0);
554 ocstream.writeString(buf);
556 catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
558 ocstream = getStream("?".charCodeAt(0));
559 ocstream.writeString(buf);
572 // Wrapped native methods:
573 copyTo: function copyTo(dir, name)
574 this.file.copyTo(this.constructor(dir).file,
577 copyToFollowingLinks: function copyToFollowingLinks(dir, name)
578 this.file.copyToFollowingLinks(this.constructor(dir).file,
581 moveTo: function moveTo(dir, name)
582 this.file.moveTo(this.constructor(dir).file,
585 equals: function equals(file)
586 this.file.equals(this.constructor(file).file),
588 contains: function contains(dir, recur)
589 this.file.contains(this.constructor(dir).file,
592 getRelativeDescriptor: function getRelativeDescriptor(file)
593 this.file.getRelativeDescriptor(this.constructor(file).file),
595 setRelativeDescriptor: function setRelativeDescriptor(file, path)
596 this.file.setRelativeDescriptor(this.constructor(file).file,
600 * @property {number} Open for reading only.
606 * @property {number} Open for writing only.
612 * @property {number} Open for reading and writing.
618 * @property {number} If the file does not exist, the file is created.
619 * If the file exists, this flag has no effect.
625 * @property {number} The file pointer is set to the end of the file
626 * prior to each write.
632 * @property {number} If the file exists, its length is truncated to 0.
638 * @property {number} If set, each write will wait for both the file
639 * data and file status to be physically updated.
645 * @property {number} With MODE_CREATE, if the file does not exist, the
646 * file is created. If the file already exists, no action and NULL
653 * @property {string} The current platform's path separator.
655 PATH_SEP: Class.Memoize(function () {
656 let f = services.directory.get("CurProcD", Ci.nsIFile);
658 return f.path.substr(f.parent.path.length, 1);
661 pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
663 DoesNotExist: function DoesNotExist(path, error) ({
665 exists: function () false,
666 __noSuchMethod__: function () { throw error || Error("Does not exist"); }
669 defaultEncoding: "UTF-8",
672 * Expands "~" and environment variables in *path*.
674 * "~" is expanded to to the value of $HOME. On Windows if this is not
675 * set then the following are tried in order:
677 * ${HOMDRIVE}$HOMEPATH
679 * The variable notation is $VAR (terminated by a non-word character)
680 * or ${VAR}. %VAR% is also supported on Windows.
682 * @param {string} path The unexpanded path string.
683 * @param {boolean} relative Whether the path is relative or absolute.
686 expandPath: function expandPath(path, relative) {
687 function getenv(name) services.environment.get(name);
689 // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
690 // TODO: Vim does not expand variables set to an empty string (and documents it).
691 // Kris reckons we shouldn't replicate this 'bug'. --djk
692 // TODO: should we be doing this for all paths?
693 function expand(path) path.replace(
694 win32 ? /\$(\w+)\b|\${(\w+)}|%(\w+)%/g
695 : /\$(\w+)\b|\${(\w+)}/g,
696 (m, n1, n2, n3) => (getenv(n1 || n2 || n3) || m));
701 if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
702 // Try $HOME first, on all systems
703 let home = getenv("HOME");
705 // Windows has its own idiosyncratic $HOME variables.
706 if (win32 && (!home || !File(home).exists()))
707 home = getenv("USERPROFILE") ||
708 getenv("HOMEDRIVE") + getenv("HOMEPATH");
710 path = home + path.substr(1);
713 // TODO: Vim expands paths twice, once before checking for ~, once
714 // after, but doesn't document it. Is this just a bug? --Kris
716 return path.replace("/", File.PATH_SEP, "g");
719 expandPathList: function (list) list.map(this.expandPath),
721 readURL: function readURL(url, encoding) {
722 let channel = services.io.newChannel(url, null, null);
723 channel.contentType = "text/plain";
724 return this.readStream(channel.open(), encoding);
727 readStream: function readStream(ifstream, encoding) {
729 var icstream = services.CharsetStream(
730 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
731 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
735 while (icstream.readString(4096, str) != 0)
736 buffer.push(str.value);
737 return buffer.join("");
745 readLines: function readLines(ifstream, encoding) {
747 var icstream = services.CharsetStream(
748 ifstream, encoding || File.defaultEncoding, 4096, // buffer size
749 services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
752 while (icstream.readLine(value))
761 isAbsolutePath: function isAbsolutePath(path) {
763 services.File().initWithPath(path);
771 joinPaths: function joinPaths(head, tail, cwd) {
772 let path = this(head, cwd);
774 // FIXME: should only expand environment vars and normalize path separators
775 path.appendRelativePath(this.expandPath(tail, true));
778 return File.DoesNotExist(e);
783 replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
786 let (file = services.directory.get("ProfD", Ci.nsIFile)) {
787 Object.keys(file).forEach(function (prop) {
788 if (!(prop in File.prototype)) {
791 isFunction = callable(file[prop]);
796 File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
798 Object.defineProperty(File.prototype, prop, {
800 get: function wrap_get() this.file[prop],
801 set: function wrap_set(val) { this.file[prop] = val; }
810 // catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
812 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: