1 /* coded by Ketmar // Invisible Vector (psyc://ketmar.no-ip.org/~Ketmar)
2 * Understanding is not required. Only obedience.
4 * This program is free software. It comes without any warranty, to
5 * the extent permitted by applicable law. You can redistribute it
6 * and/or modify it under the terms of the Do What The Fuck You Want
7 * To Public License, Version 2, as published by Sam Hocevar. See
8 * http://www.wtfpl.net/txt/copying/ for more details.
10 ////////////////////////////////////////////////////////////////////////////////
11 function nowSeconds () Math.floor(Date.now()/1000);
14 ////////////////////////////////////////////////////////////////////////////////
15 function PackageDB () {
21 PackageDB.prototype.STATE_OK = 0;
22 PackageDB.prototype.STATE_INACTIVE = 1;
23 PackageDB.prototype.STATE_DISABLED = 2; // due to some errors
26 PackageDB.prototype.__defineGetter__("db", function () {
27 if (this.dbObj == null) {
29 this.dbFile = getUserPkgDBDir();
30 //conlog("** package db dir: [", this.dbFile.path, "]");
31 //this.dbFile.append("database");
32 if (!this.dbFile.exists()) this.dbFile.create(this.dbFile.DIRECTORY_TYPE, 0700);
33 this.dbFile.append("packages.db");
34 //conlog("package db: [", this.dbFile.path, "]");
36 this.dbObj = Services.storage.openDatabase(this.dbFile);
37 // the auto_vacuum pragma has to be set before the table is created
38 this.dbObj.executeSimpleSQL("PRAGMA auto_vacuum = NONE;");
39 this.dbObj.executeSimpleSQL("PRAGMA journal_mode = DELETE;");
41 this.dbObj.executeSimpleSQL(
42 "CREATE TABLE IF NOT EXISTS packages (\n"+
43 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique package id */
44 " name TEXT NOT NULL UNIQUE,\n"+ /* package name */
45 " dirname TEXT NOT NULL UNIQUE,\n"+ /* package dir name, valid disk file name inside package dir */
46 " version TEXT NOT NULL,\n"+ /* package version */
47 " downurl TEXT NOT NULL,\n"+ /* package update url */
48 " state INTEGER NOT NULL DEFAULT 0\n"+ /* see PackageDB.STATE_* */
51 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name ON packages (name);");
52 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name_and_state ON packages (name, state);");
53 // i don't want to alter `packages` table, so...
54 this.dbObj.executeSimpleSQL(
55 "CREATE TABLE IF NOT EXISTS packagetimes (\n"+
56 " pkgid INTEGER NOT NULL,\n"+ /* unique package id */
57 " lastuptime INTEGER NOT NULL DEFAULT 0\n"+ /* last successfull update unixtime, in seconds */
60 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packageups_by_pkgid ON packagetimes (pkgid);");
62 this.dbObj.executeSimpleSQL(
63 "CREATE TABLE IF NOT EXISTS resources (\n"+
64 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id */
65 " pkgid INTEGER NOT NULL,\n"+ /* unique package id */
66 " url TEXT NOT NULL UNIQUE,\n"+ /* file url */
67 " diskname TEXT NOT NULL,\n"+ /* disk file name; ""' means "main package file" */
68 " sha512 TEXT NOT NULL,\n"+ /* sha-512 of the disk file */
69 " resname TEXT NOT NULL DEFAULT '',\n"+ /* resource name for named resources */
70 " contenttype TEXT NOT NULL DEFAULT 'application/octet-stream'\n"+ /* resource content type */
73 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid ON resources (pkgid);");
74 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid_and_name ON resources (pkgid, resname);");
76 this.dbObj.executeSimpleSQL(
77 "CREATE TABLE IF NOT EXISTS requires (\n"+
78 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id; first one is main js file */
79 " pkgid INTEGER NOT NULL,\n"+ /* unique package id */
80 " url TEXT NOT NULL UNIQUE,\n"+ /* file url */
81 " diskname TEXT NOT NULL,\n"+ /* disk file name */
82 " sha512 TEXT NOT NULL\n"+ /* sha-512 of the disk file */
85 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid ON requires (pkgid);");
86 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid_and_id ON requires (pkgid, id);");
87 // run vacuum once manually to switch to the correct auto_vacuum mode for
88 // databases that were created with incorrect auto_vacuum
89 this.dbObj.executeSimpleSQL("VACUUM;");
95 PackageDB.prototype.__defineGetter__("opened", function () {
96 return (this.dbObj != null);
100 PackageDB.prototype.close = function () {
109 PackageDB.prototype.getActivePackageByName = function (name) {
110 if (typeof(name) !== "string" || !name) return null;
111 let stmt = this.db.createStatement("SELECT id,name,dirname,version,downurl FROM packages WHERE state=0 AND name=:name LIMIT 1");
113 stmt.params.name = name;
114 if (stmt.executeStep()) {
118 dirname: stmt.row.dirname,
119 version: stmt.row.version,
120 url: stmt.row.downurl,
121 toString: function () "PkgInfo<id:"+this.id+";name:'"+this.name+"';version:'"+this.version+"';url:'"+this.url+"'>",
133 PackageDB.prototype.getPackageUpdateTimeById = function (pkgid) {
134 //conlog("*** pkgid=", pkgid);
135 if (typeof(pkgid) !== "number") return -1;
136 let stmt = this.db.createStatement("SELECT lastuptime FROM packagetimes WHERE pkgid=:pkgid LIMIT 1");
138 stmt.params.pkgid = pkgid;
139 if (stmt.executeStep()) {
140 let res = stmt.row.lastuptime;
141 //conlog("pkgid=", pkgid, "; lastuptime=", res);
142 if (isNaN(res)) res = -1;
146 //conlog("pkgid=", pkgid, "; alas");
148 logException("getPackageUpdateTimeById", e);
156 PackageDB.prototype.getPackageUpdateTimeByName = function (name) {
157 let nfo = this.getActivePackageByName(name);
159 return this.getPackageUpdateTimeById(nfo.id);
163 PackageDB.prototype.setPackageUpdateTimeById = function (pkgid, luptime) {
164 if (typeof(pkgid) !== "number") return false;
165 if (typeof(luptime) !== "number") return false;
166 let stmt = this.db.createStatement("UPDATE packagetimes SET lastuptime=:luptime WHERE pkgid=:pid");
168 stmt.params.pkgid = pkgid;
169 stmt.params.luptime = luptime;
177 PackageDB.prototype.getActivePackages = function () {
178 let res = new Array();
179 let stmt = this.db.createStatement("SELECT id,name,dirname,version,downurl FROM packages WHERE state=0 ORDER BY name ASC");
181 while (stmt.executeStep()) {
185 dirname: stmt.row.dirname,
186 version: stmt.row.version,
187 url: stmt.row.downurl,
188 toString: function () "PkgInfo<id:"+this.id+";name:'"+this.name+"';version:'"+this.version+"';url:'"+this.url+"'>",
200 PackageDB.prototype.getPackageJSFiles = function (pkgid) {
201 let res = new Array();
202 let stmt = this.db.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
204 stmt.params.pkgid = pkgid;
205 while (stmt.executeStep()) {
206 res.push(stmt.row.diskname);
216 ////////////////////////////////////////////////////////////////////////////////
217 // normalize string, so it can be used as disk file name
218 function normDiskStr (s) {
219 const validRE = /^[-A-Za-z_0-9.]$/;
222 if (!validRE.test(ch)) ch = "_";
225 if (res.length && res[0] == "_") res = "pkg"+res;
232 // bool ignoreVersion
234 // returns true if package should be updated
235 PackageDB.prototype.checkAndUpdate = function (pkgname, dnpkg, options) {
236 if (typeof(pkgname) !== "string" || !pkgname) throw new Error("invalid package name");
237 if (typeof(dnpkg) !== "object") throw new Error("package info object expected");
238 if (typeof(options) !== "object") options = {};
242 function getPkgInfo () {
243 let stmt = pdb.createStatement("SELECT id,version,dirname FROM packages WHERE name=:name LIMIT 1");
244 stmt.params.name = pkgname;
245 if (stmt.step()) return {id:stmt.row.id, version:stmt.row.version, dirname:stmt.row.dirname};
249 let pi = getPkgInfo();
250 if (!options.forceUpdate && !options.ignoreVersion && pi) {
251 if (pi.version == dnpkg.version) {
252 // no need to do anything
253 if (!options.checkOnly) {
254 // update update time ;-)
255 this.setPackageUpdateTimeById(pi.id, nowSeconds());
262 if (options.checkOnly) return true;
265 pdb.executeSimpleSQL("BEGIN TRANSACTION;");
267 // build package directory, and create it if necessary
268 let pkdir = getUserPkgDir();
270 // we want new directory
272 pi.version = ""+dnpkg.version;
273 pi.dirname = normDiskStr(pkgname);
274 pkdir.append(pi.dirname);
275 pkdir.createUnique(pkdir.DIRECTORY_TYPE, 0700);
276 pi.dirname = pkdir.leafName;
277 // create database record, so we can get package id from it
279 let stmt = pdb.createStatement("INSERT OR REPLACE INTO packages (name, dirname, version, downurl, state) VALUES (:name, :dirname, :version, :downurl, 0)");
281 stmt.params.name = pkgname;
282 stmt.params.dirname = pi.dirname;
283 stmt.params.version = pi.version;
284 stmt.params.downurl = dnpkg.downurl;
291 if (!pi) throw new Error("something is wrong with package database");
293 // we want existing directory
294 pkdir.append(pi.dirname);
295 if (pkdir.exists() && !pkdir.isDirectory()) throw new Error("something is wrong with package storage");
298 // clear package directory
299 try { pkdir.remove(true); } catch (e) {}
300 try { if (!pkdir.exists()) pkdir.create(pkdir.DIRECTORY_TYPE, 0700); } catch (e) {}
302 function copyFile (srcpath) {
303 let fl = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
304 fl.initWithPath(srcpath);
305 //conlog("copying ["+srcpath+"] to ["+pkdir.path+"/]");
306 fl.copyTo(pkdir, fl.leafName);
307 let nn = pkdir.clone();
308 nn.append(fl.leafName);
312 //TODO: make this async?
313 // copy package files
314 dnpkg.diskname = copyFile(dnpkg.diskname);
315 for (let res of dnpkg.resources) res.diskname = copyFile(res.diskname);
318 // ah, id is always integer here
319 pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
320 pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
324 let stmt = pdb.createStatement("INSERT OR REPLACE INTO requires (pkgid, url, diskname, sha512) VALUES (:pkgid, :url, :diskname, :sha512)");
326 stmt.params.pkgid = pi.id;
327 stmt.params.url = dnpkg.downurl;
328 stmt.params.diskname = dnpkg.diskname;
329 stmt.params.sha512 = dnpkg.sha512;
332 // insert other js files
333 for (let res of dnpkg.resources) {
334 if (res.resname !== null) continue;
336 stmt.params.pkgid = pi.id;
337 stmt.params.url = res.url;
338 stmt.params.diskname = res.diskname;
339 stmt.params.sha512 = res.sha512;
347 let stmt = pdb.createStatement("INSERT OR REPLACE INTO resources (pkgid, url, diskname, sha512, resname, contenttype) VALUES (:pkgid, :url, :diskname, :sha512, :resname, :ct)");
348 for (let res of dnpkg.resources) {
349 if (res.resname === null) continue;
351 stmt.params.pkgid = pi.id;
352 stmt.params.url = res.url;
353 stmt.params.diskname = res.diskname;
354 stmt.params.sha512 = res.sha512;
355 stmt.params.resname = res.resname;
356 stmt.params.ct = res.contenttype||"application/octet-stream";
362 pdb.executeSimpleSQL("COMMIT;");
363 // update update time ;-)
364 this.setPackageUpdateTimeById(pi.id, nowSeconds());
367 pdb.executeSimpleSQL("ROLLBACK;");
373 ////////////////////////////////////////////////////////////////////////////////
374 // this returns array of xnfos:
375 // string path: path to main script
376 // string[] reqs: list of required files
377 // {string name} rsrc: resources dict, keyed by resource name, value is string path
378 PackageDB.prototype.getActivePackagesForCache = function () {
379 let res = new Array();
383 function getPackageIds () {
384 let stmt = pdb.createStatement("SELECT id FROM packages WHERE state=0 ORDER BY name ASC");
386 while (stmt.executeStep()) res.push({id:stmt.row.id});
394 //conlog("packages: "+res.length);
396 let reqmt = pdb.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
397 let resmt = pdb.createStatement("SELECT diskname,resname,contenttype FROM resources WHERE pkgid=:pkgid");
398 for (let pi of res) {
399 //conlog("package: "+pi.id);
402 reqmt.params.pkgid = pi.id;
403 while (reqmt.executeStep()) {
404 //conlog("req: ", reqmt.row.diskname);
406 pi.path = reqmt.row.diskname;
407 pi.reqs = new Array();
409 pi.reqs.push(reqmt.row.diskname);
415 resmt.params.pkgid = pi.id;
417 while (resmt.executeStep()) {
418 //conlog("res: ", resmt.row.resname, " : ", resmt.row.diskname);
419 pi.rsrc[resmt.row.resname] = {path:resmt.row.diskname, contenttype:resmt.row.contenttype};
429 ////////////////////////////////////////////////////////////////////////////////
430 PackageDB.prototype.removePackage = function (name) {
431 if (typeof(name) !== "string" || !name) throw new Error("invalid package name");
432 let pi = this.getActivePackageByName(name);
433 if (!pi) throw new Error("unknown package '"+name+"'");
436 pdb.executeSimpleSQL("BEGIN TRANSACTION;");
437 pdb.executeSimpleSQL("DELETE FROM packages WHERE id="+pi.id);
438 pdb.executeSimpleSQL("DELETE FROM packagetimes WHERE pkgid="+pi.id);
439 pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
440 pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
441 pdb.executeSimpleSQL("COMMIT;");
442 // clear package directory
444 pdb.executeSimpleSQL("ROLLBACK;");
447 let pkdir = getUserPkgDir();
448 pkdir.append(pi.dirname);
449 if (pkdir.exists() && pkdir.isDirectory()) {
450 try { pkdir.remove(true); } catch (e) {}
455 ////////////////////////////////////////////////////////////////////////////////
456 exports.PackageDB = PackageDB;