1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-20 09:48:00 +01:00
Files
pentadactyl-pm/common/modules/storage.jsm
2011-09-30 19:40:20 -04:00

672 lines
20 KiB
JavaScript

// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("storage", {
exports: ["File", "Storage", "storage"],
require: ["services", "util"]
}, this);
this.lazyRequire("config", ["config"]);
this.lazyRequire("io", ["IO"]);
var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
var myObject = JSON.parse("{}").constructor;
function loadData(name, store, type) {
try {
let file = storage.infoPath.child(name);
if (file.exists()) {
let data = file.read();
let result = JSON.parse(data);
if (result instanceof type)
return result;
}
}
catch (e) {
util.reportError(e);
}
}
function saveData(obj) {
if (obj.privateData && storage.privateMode)
return;
if (obj.store && storage.infoPath)
storage.infoPath.child(obj.name).write(obj.serial);
}
var StoreBase = Class("StoreBase", {
OPTIONS: ["privateData", "replacer"],
fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
get serial() JSON.stringify(this._object, this.replacer),
init: function (name, store, load, options) {
this._load = load;
this.__defineGetter__("store", function () store);
this.__defineGetter__("name", function () name);
for (let [k, v] in Iterator(options))
if (this.OPTIONS.indexOf(k) >= 0)
this[k] = v;
this.reload();
},
changed: function () { this.timer.tell(); },
reload: function reload() {
this._object = this._load() || this._constructor();
this.fireEvent("change", null);
},
delete: function delete_() {
delete storage.keys[this.name];
delete storage[this.name];
storage.infoPath.child(this.name).remove(false);
},
save: function () { saveData(this); },
__iterator__: function () Iterator(this._object)
});
var ArrayStore = Class("ArrayStore", StoreBase, {
_constructor: Array,
get length() this._object.length,
set: function set(index, value, quiet) {
var orig = this._object[index];
this._object[index] = value;
if (!quiet)
this.fireEvent("change", index);
return orig;
},
push: function push(value) {
this._object.push(value);
this.fireEvent("push", this._object.length);
},
pop: function pop(value) {
var res = this._object.pop();
this.fireEvent("pop", this._object.length);
return res;
},
truncate: function truncate(length, fromEnd) {
var res = this._object.length;
if (this._object.length > length) {
if (fromEnd)
this._object.splice(0, this._object.length - length);
this._object.length = length;
this.fireEvent("truncate", length);
}
return res;
},
// XXX: Awkward.
mutate: function mutate(funcName) {
var _funcName = funcName;
arguments[0] = this._object;
this._object = Array[_funcName].apply(Array, arguments);
this.fireEvent("change", null);
},
get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
});
var ObjectStore = Class("ObjectStore", StoreBase, {
_constructor: myObject,
clear: function () {
this._object = {};
this.fireEvent("clear");
},
get: function get(key, default_) {
return key in this._object ? this._object[key] :
arguments.length > 1 ? this.set(key, default_) :
undefined;
},
keys: function keys() Object.keys(this._object),
remove: function remove(key) {
var res = this._object[key];
delete this._object[key];
this.fireEvent("remove", key);
return res;
},
set: function set(key, val) {
var defined = key in this._object;
var orig = this._object[key];
this._object[key] = val;
if (!defined)
this.fireEvent("add", key);
else if (orig != val)
this.fireEvent("change", key);
return val;
}
});
var Storage = Module("Storage", {
alwaysReload: {},
init: function () {
this.cleanup();
if (services.bootstrap && !services.bootstrap.session)
services.bootstrap.session = {};
this.session = services.bootstrap ? services.bootstrap.session : {};
},
cleanup: function () {
this.saveAll();
for (let key in keys(this.keys)) {
if (this[key].timer)
this[key].timer.flush();
delete this[key];
}
this.keys = {};
this.observers = {};
},
infoPath: Class.Memoize(function ()
File(IO.runtimePath.replace(/,.*/, ""))
.child("info").child(config.profileName)),
exists: function exists(name) this.infoPath.child(name).exists(),
newObject: function newObject(key, constructor, params) {
if (params == null || !isObject(params))
throw Error("Invalid argument type");
if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
if (key in this && !(params.reload || this.alwaysReload[key]))
throw Error();
let load = function () loadData(key, params.store, params.type || myObject);
this.keys[key] = new constructor(key, params.store, load, params);
this.keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
this.__defineGetter__(key, function () this.keys[key]);
}
return this.keys[key];
},
newMap: function newMap(key, options) {
return this.newObject(key, ObjectStore, options);
},
newArray: function newArray(key, options) {
return this.newObject(key, ArrayStore, update({ type: Array }, options));
},
addObserver: function addObserver(key, callback, ref) {
if (ref) {
let refs = overlay.getData(ref, "storage-refs");
refs.push(callback);
var callbackRef = Cu.getWeakReference(callback);
}
else {
callbackRef = { get: function () callback };
}
this.removeDeadObservers();
if (!(key in this.observers))
this.observers[key] = [];
if (!this.observers[key].some(function (o) o.callback.get() == callback))
this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
},
removeObserver: function (key, callback) {
this.removeDeadObservers();
if (!(key in this.observers))
return;
this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
if (this.observers[key].length == 0)
delete obsevers[key];
},
removeDeadObservers: function () {
for (let [key, ary] in Iterator(this.observers)) {
this.observers[key] = ary = ary.filter(function (o) o.callback.get()
&& (!o.ref || o.ref.get()
&& overlay.getData(o.ref.get(), "storage-refs", null)));
if (!ary.length)
delete this.observers[key];
}
},
fireEvent: function fireEvent(key, event, arg) {
this.removeDeadObservers();
if (key in this.observers)
// Safe, since we have our own Array object here.
for each (let observer in this.observers[key])
observer.callback.get()(key, event, arg);
if (key in this.keys)
this[key].timer.tell();
},
load: function load(key) {
if (this[key].store && this[key].reload)
this[key].reload();
},
save: function save(key) {
if (this[key])
saveData(this.keys[key]);
},
saveAll: function storeAll() {
for each (let obj in this.keys)
saveData(obj);
},
_privateMode: false,
get privateMode() this._privateMode,
set privateMode(val) {
if (val && !this._privateMode)
this.saveAll();
if (!val && this._privateMode)
for (let key in this.keys)
this.load(key);
return this._privateMode = Boolean(val);
}
}, {
Replacer: {
skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
}
}, {
cleanup: function (dactyl, modules, window) {
overlay.setData(window, "storage-refs", null);
this.removeDeadObservers();
}
});
/**
* @class File A class to wrap nsIFile objects and simplify operations
* thereon.
*
* @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
* @param {boolean} checkPWD Whether to allow expansion relative to the
* current directory. @default true
* @param {string} charset The charset of the file. @default File.defaultEncoding
*/
var File = Class("File", {
init: function (path, checkPWD, charset) {
let file = services.File();
if (charset)
this.charset = charset;
if (path instanceof Ci.nsIFileURL)
path = path.file;
if (path instanceof Ci.nsIFile)
file = path.clone();
else if (/file:\/\//.test(path))
file = services["file:"].getFileFromURLSpec(path);
else {
try {
let expandedPath = File.expandPath(path);
if (!File.isAbsolutePath(expandedPath) && checkPWD)
file = checkPWD.child(expandedPath);
else
file.initWithPath(expandedPath);
}
catch (e) {
util.reportError(e);
return File.DoesNotExist(path, e);
}
}
let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
self.__proto__ = this;
return self;
},
charset: Class.Memoize(function () File.defaultEncoding),
/**
* @property {nsIFileURL} Returns the nsIFileURL object for this file.
*/
URI: Class.Memoize(function () {
let uri = services.io.newFileURI(this).QueryInterface(Ci.nsIFileURL);
uri.QueryInterface(Ci.nsIMutable).mutable = false;
return uri;
}),
/**
* Iterates over the objects in this directory.
*/
iterDirectory: function () {
if (!this.exists())
throw Error(_("io.noSuchFile"));
if (!this.isDirectory())
throw Error(_("io.eNotDir"));
for (let file in iter(this.directoryEntries))
yield File(file);
},
/**
* Returns a new file for the given child of this directory entry.
*/
child: function (name) {
let f = this.constructor(this);
for each (let elem in name.split(File.pathSplit))
f.append(elem);
return f;
},
/**
* Returns an iterator for all lines in a file.
*/
get lines() File.readLines(services.FileInStream(this, -1, 0, 0),
this.charset),
/**
* Reads this file's entire contents in "text" mode and returns the
* content as a string.
*
* @param {string} encoding The encoding from which to decode the file.
* @default #charset
* @returns {string}
*/
read: function (encoding) {
let ifstream = services.FileInStream(this, -1, 0, 0);
return File.readStream(ifstream, encoding || this.charset);
},
/**
* Returns the list of files in this directory.
*
* @param {boolean} sort Whether to sort the returned directory
* entries.
* @returns {[nsIFile]}
*/
readDirectory: function (sort) {
if (!this.isDirectory())
throw Error(_("io.eNotDir"));
let array = [e for (e in this.iterDirectory())];
if (sort)
array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
return array;
},
/**
* Returns a new nsIFileURL object for this file.
*
* @returns {nsIFileURL}
*/
toURI: function toURI() services.io.newFileURI(this),
/**
* Writes the string *buf* to this file.
*
* @param {string} buf The file content.
* @param {string|number} mode The file access mode, a bitwise OR of
* the following flags:
* {@link #MODE_RDONLY}: 0x01
* {@link #MODE_WRONLY}: 0x02
* {@link #MODE_RDWR}: 0x04
* {@link #MODE_CREATE}: 0x08
* {@link #MODE_APPEND}: 0x10
* {@link #MODE_TRUNCATE}: 0x20
* {@link #MODE_SYNC}: 0x40
* Alternatively, the following abbreviations may be used:
* ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
* ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
* @default ">"
* @param {number} perms The file mode bits of the created file. This
* is only used when creating a new file and does not change
* permissions if the file exists.
* @default 0644
* @param {string} encoding The encoding to used to write the file.
* @default #charset
*/
write: function (buf, mode, perms, encoding) {
function getStream(defaultChar) {
return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
}
if (buf instanceof File)
buf = buf.read();
if (!encoding)
encoding = this.charset;
if (mode == ">>")
mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
else if (!mode || mode == ">")
mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
if (!perms)
perms = octal(644);
if (!this.exists()) // OCREAT won't create the directory
this.create(this.NORMAL_FILE_TYPE, perms);
let ofstream = services.FileOutStream(this, mode, perms, 0);
try {
var ocstream = getStream(0);
ocstream.writeString(buf);
}
catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
ocstream.close();
ocstream = getStream("?".charCodeAt(0));
ocstream.writeString(buf);
return false;
}
finally {
try {
ocstream.close();
}
catch (e) {}
ofstream.close();
}
return true;
}
}, {
/**
* @property {number} Open for reading only.
* @final
*/
MODE_RDONLY: 0x01,
/**
* @property {number} Open for writing only.
* @final
*/
MODE_WRONLY: 0x02,
/**
* @property {number} Open for reading and writing.
* @final
*/
MODE_RDWR: 0x04,
/**
* @property {number} If the file does not exist, the file is created.
* If the file exists, this flag has no effect.
* @final
*/
MODE_CREATE: 0x08,
/**
* @property {number} The file pointer is set to the end of the file
* prior to each write.
* @final
*/
MODE_APPEND: 0x10,
/**
* @property {number} If the file exists, its length is truncated to 0.
* @final
*/
MODE_TRUNCATE: 0x20,
/**
* @property {number} If set, each write will wait for both the file
* data and file status to be physically updated.
* @final
*/
MODE_SYNC: 0x40,
/**
* @property {number} With MODE_CREATE, if the file does not exist, the
* file is created. If the file already exists, no action and NULL
* is returned.
* @final
*/
MODE_EXCL: 0x80,
/**
* @property {string} The current platform's path separator.
*/
PATH_SEP: Class.Memoize(function () {
let f = services.directory.get("CurProcD", Ci.nsIFile);
f.append("foo");
return f.path.substr(f.parent.path.length, 1);
}),
pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
DoesNotExist: function DoesNotExist(path, error) ({
path: path,
exists: function () false,
__noSuchMethod__: function () { throw error || Error("Does not exist"); }
}),
defaultEncoding: "UTF-8",
/**
* Expands "~" and environment variables in *path*.
*
* "~" is expanded to to the value of $HOME. On Windows if this is not
* set then the following are tried in order:
* $USERPROFILE
* ${HOMDRIVE}$HOMEPATH
*
* The variable notation is $VAR (terminated by a non-word character)
* or ${VAR}. %VAR% is also supported on Windows.
*
* @param {string} path The unexpanded path string.
* @param {boolean} relative Whether the path is relative or absolute.
* @returns {string}
*/
expandPath: function expandPath(path, relative) {
function getenv(name) services.environment.get(name);
// expand any $ENV vars - this is naive but so is Vim and we like to be compatible
// TODO: Vim does not expand variables set to an empty string (and documents it).
// Kris reckons we shouldn't replicate this 'bug'. --djk
// TODO: should we be doing this for all paths?
function expand(path) path.replace(
!win32 ? /\$(\w+)\b|\${(\w+)}/g
: /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
);
path = expand(path);
// expand ~
// Yuck.
if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
// Try $HOME first, on all systems
let home = getenv("HOME");
// Windows has its own idiosyncratic $HOME variables.
if (win32 && (!home || !File(home).exists()))
home = getenv("USERPROFILE") ||
getenv("HOMEDRIVE") + getenv("HOMEPATH");
path = home + path.substr(1);
}
// TODO: Vim expands paths twice, once before checking for ~, once
// after, but doesn't document it. Is this just a bug? --Kris
path = expand(path);
return path.replace("/", File.PATH_SEP, "g");
},
expandPathList: function (list) list.map(this.expandPath),
readURL: function readURL(url, encoding) {
let channel = services.io.newChannel(url, null, null);
channel.contentType = "text/plain";
return this.readStream(channel.open(), encoding);
},
readStream: function readStream(ifstream, encoding) {
try {
var icstream = services.CharsetStream(
ifstream, encoding || File.defaultEncoding, 4096, // buffer size
services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
let buffer = [];
let str = {};
while (icstream.readString(4096, str) != 0)
buffer.push(str.value);
return buffer.join("");
}
finally {
icstream.close();
ifstream.close();
}
},
readLines: function readLines(ifstream, encoding) {
try {
var icstream = services.CharsetStream(
ifstream, encoding || File.defaultEncoding, 4096, // buffer size
services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
var value = {};
while (icstream.readLine(value))
yield value.value;
}
finally {
icstream.close();
ifstream.close();
}
},
isAbsolutePath: function isAbsolutePath(path) {
try {
services.File().initWithPath(path);
return true;
}
catch (e) {
return false;
}
},
joinPaths: function joinPaths(head, tail, cwd) {
let path = this(head, cwd);
try {
// FIXME: should only expand environment vars and normalize path separators
path.appendRelativePath(this.expandPath(tail, true));
}
catch (e) {
return File.DoesNotExist(e);
}
return path;
},
replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
});
endModule();
// catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
// vim: set fdm=marker sw=4 sts=4 et ft=javascript: