1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-26 16:02:28 +01:00
Files
pentadactyl-pm/common/modules/storage.jsm
2010-09-17 06:15:13 -04:00

575 lines
18 KiB
JavaScript

/***** BEGIN LICENSE BLOCK ***** {{{
Copyright ©2008-2009 by Kris Maglione <maglione.k at Gmail>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
}}} ***** END LICENSE BLOCK *****/
"use strict";
const myObject = Object;
Components.utils.import("resource://dactyl/base.jsm");
defmodule("storage", this, {
exports: ["File", "storage"],
require: ["services", "util"]
});
var prefService = services.get("pref").getBranch("extensions.dactyl.datastore.");
const win32 = services.get("runtime").OS == "Win32";
function getFile(name) {
let file = storage.infoPath.clone();
file.append(name);
return File(file);
}
function getCharPref(name) {
try {
return prefService.getComplexValue(name, Ci.nsISupportsString).data;
}
catch (e) {}
}
function setCharPref(name, value) {
var str = Cc['@mozilla.org/supports-string;1'].createInstance(Ci.nsISupportsString);
str.data = value;
return prefService.setComplexValue(name, Ci.nsISupportsString, str);
}
function loadPref(name, store, type) {
try {
if (store)
var pref = getCharPref(name);
if (!pref && storage.infoPath)
var file = getFile(name).read();
if (pref || file)
var result = services.get("json").decode(pref || file);
if (pref) {
prefService.clearUserPref(name);
savePref({ name: name, store: true, serial: pref });
}
if (result instanceof type)
return result;
}
catch (e) {}
}
function savePref(obj) {
if (obj.privateData && storage.privateMode)
return;
if (obj.store && storage.infoPath)
getFile(obj.name).write(obj.serial);
}
const 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);
},
save: function () { savePref(this); },
});
const ObjectStore = Class("ObjectStore", StoreBase, {
_constructor: myObject,
clear: function () {
this._object = {};
this.fireEvent("clear");
},
get: function get(key, default_) key in this._object ? this._object[key] : this.set(key, default_),
keys: function keys() Object.keys(this._object),
remove: function remove(key) {
var ret = this._object[key];
delete this._object[key];
this.fireEvent("remove", key);
},
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;
},
__iterator__: function () Iterator(this._object),
});
const ArrayStore = Class("ArrayStore", StoreBase, {
_constructor: Array,
get length() this._object.length,
set: function set(index, value) {
var orig = this._object[index];
this._object[index] = value;
this.fireEvent("change", index);
},
push: function push(value) {
this._object.push(value);
this.fireEvent("push", this._object.length);
},
pop: function pop(value) {
var ret = this._object.pop();
this.fireEvent("pop", this._object.length);
return ret;
},
truncate: function truncate(length, fromEnd) {
var ret = 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 ret;
},
// 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],
__iterator__: function () Iterator(this._object),
});
var keys = {};
var observers = {};
const Storage = Module("Storage", {
alwaysReload: {},
newObject: function newObject(key, constructor, params) {
if (!(key in keys) || params.reload || this.alwaysReload[key]) {
if (key in this && !(params.reload || this.alwaysReload[key]))
throw Error();
let load = function () loadPref(key, params.store, params.type || myObject);
keys[key] = new constructor(key, params.store, load, params);
keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
this.__defineGetter__(key, function () keys[key]);
}
return keys[key];
},
newMap: function newMap(key, options) {
return this.newObject(key, ObjectStore, options);
},
newArray: function newArray(key, options) {
return this.newObject(key, ArrayStore, { type: Array, __proto__: options });
},
addObserver: function addObserver(key, callback, ref) {
if (ref) {
if (!ref.dactylStorageRefs)
ref.dactylStorageRefs = [];
ref.dactylStorageRefs.push(callback);
var callbackRef = Cu.getWeakReference(callback);
}
else {
callbackRef = { get: function () callback };
}
this.removeDeadObservers();
if (!(key in observers))
observers[key] = [];
if (!observers[key].some(function (o) o.callback.get() == callback))
observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
},
removeObserver: function (key, callback) {
this.removeDeadObservers();
if (!(key in observers))
return;
observers[key] = observers[key].filter(function (elem) elem.callback.get() != callback);
if (observers[key].length == 0)
delete obsevers[key];
},
removeDeadObservers: function () {
for (let [key, ary] in Iterator(observers)) {
observers[key] = ary = ary.filter(function (o) o.callback.get() && (!o.ref || o.ref.get() && o.ref.get().dactylStorageRefs));
if (!ary.length)
delete observers[key];
}
},
get observers() observers,
fireEvent: function fireEvent(key, event, arg) {
this.removeDeadObservers();
// Safe, since we have our own Array object here.
if (key in observers)
for each (let observer in observers[key])
observer.callback.get()(key, event, arg);
if (key in keys)
this[key].timer.tell();
},
load: function load(key) {
if (this[key].store && this[key].reload)
this[key].reload();
},
save: function save(key) {
savePref(keys[key]);
},
saveAll: function storeAll() {
for each (let obj in keys)
savePref(obj);
},
_privateMode: false,
get privateMode() this._privateMode,
set privateMode(val) {
if (val && !this._privateMode)
this.saveAll();
if (!val && this._privateMode)
for (let key in keys)
this.load(key);
return this._privateMode = Boolean(val);
}
}, {
}, {
init: function (dactyl, modules) {
let infoPath = services.create("file");
infoPath.initWithPath(File.expandPath(modules.IO.runtimePath.replace(/,.*/, "")));
infoPath.append("info");
infoPath.append(dactyl.profileName);
storage.infoPath = infoPath;
}
});
/**
* @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
*/
const File = Class("File", {
init: function (path, checkPWD) {
let file = services.create("file");
if (path instanceof Ci.nsIFile)
file = path;
else if (/file:\/\//.test(path))
file = services.create("file:").getFileFromURLSpec(path);
else {
let expandedPath = File.expandPath(path);
if (!File.isAbsolutePath(expandedPath) && checkPWD)
file = File.joinPaths(checkPWD, expandedPath);
else
file.initWithPath(expandedPath);
}
let self = XPCSafeJSObjectWrapper(file);
self.__proto__ = File.prototype;
return self;
},
/**
* Iterates over the objects in this directory.
*/
iterDirectory: function () {
if (!this.isDirectory())
throw Error("Not a directory");
let entries = this.directoryEntries;
while (entries.hasMoreElements())
yield File(entries.getNext().QueryInterface(Ci.nsIFile));
},
/**
* 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 options["fileencoding"]
* @returns {string}
*/
read: function (encoding) {
let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
if (!encoding)
encoding = File.defaultEncoding;
ifstream.init(this, -1, 0, 0);
icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering
let buffer = [];
let str = {};
while (icstream.readString(4096, str) != 0)
buffer.push(str.value);
icstream.close();
ifstream.close();
return buffer.join("");
},
/**
* 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("Not a directory");
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;
},
/**
* Writes the string <b>buf</b> 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 options["fileencoding"]
*/
write: function (buf, mode, perms, encoding) {
let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
function getStream(defaultChar) {
let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
stream.init(ofstream, encoding, 0, defaultChar);
return stream;
}
if (!encoding)
encoding = File.defaultEncoding;
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 = parseInt('0644', 8);
ofstream.init(this, mode, perms, 0);
let ocstream = getStream(0);
try {
ocstream.writeString(buf);
}
catch (e) {
if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
ocstream = getStream("?".charCodeAt(0));
ocstream.writeString(buf);
return false;
}
else
throw e;
}
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 seperator.
*/
get PATH_SEP() {
delete this.PATH_SEP;
let f = services.get("directory").get("CurProcD", Ci.nsIFile);
f.append("foo");
return this.PATH_SEP = f.path.substr(f.parent.path.length, 1);
},
defaultEncoding: "UTF-8",
expandPath: function (path, relative) {
// 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) services.get("environment").get(n1 || n2 || n3) || m
);
path = expand(path);
// expand ~
// Yuck.
if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(File.PATH_SEP) + "])").test(path)) {
// Try $HOME first, on all systems
let home = services.get("environment").get("HOME");
// Windows has its own idiosyncratic $HOME variables.
if (!home && win32)
home = services.get("environment").get("USERPROFILE") ||
services.get("environment").get("HOMEDRIVE") + services.get("environment").get("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),
getPathsFromPathList: function (list) {
if (!list)
return [];
// empty list item means the current directory
return list.replace(/,$/, "").split(",")
.map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir);
},
isAbsolutePath: function (path) {
try {
services.create("file").initWithPath(path);
return true;
}
catch (e) {
return false;
}
},
joinPaths: function (head, tail) {
let path = this(head);
try {
// FIXME: should only expand env vars and normalise path separators
path.appendRelativePath(this.expandPath(tail, true));
}
catch (e) {
return { exists: function () false, __noSuchMethod__: function () { throw e; } };
}
return path;
},
replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
});
// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");}
endmodule();
// vim: set fdm=marker sw=4 sts=4 et ft=javascript: