added simple package manager UI
[guerillascript.git] / main / modules / pkg / packagedb.js
blob550aca9d2abb41a40ed3f3cd2843c402a6a278b7
1 /* coded by Ketmar // Invisible Vector (psyc://ketmar.no-ip.org/~Ketmar)
2  * Understanding is not required. Only obedience.
3  *
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.
9  */
10 ////////////////////////////////////////////////////////////////////////////////
11 function nowSeconds () Math.floor(Date.now()/1000);
14 ////////////////////////////////////////////////////////////////////////////////
15 function PackageDB () {
16   this.dbObj = null;
17   return this;
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) {
28     if (!this.dbFile) {
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, "]");
35     }
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;");
40     //
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_* */
49       ");"
50     );
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 */
58       ");"
59     );
60     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packageups_by_pkgid ON packagetimes (pkgid);");
61     //
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 */
71       ");"
72     );
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);");
75     //
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 */
83       ");"
84     );
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;");
90   }
91   return this.dbObj;
92 });
95 PackageDB.prototype.__defineGetter__("opened", function () {
96   return (this.dbObj != null);
97 });
100 PackageDB.prototype.close = function () {
101   if (this.dbObj) {
102     this.dbObj.close();
103     this.dbObj = null;
104     this.dbFile = null;
105   }
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");
112   try {
113     stmt.params.name = name;
114     if (stmt.executeStep()) {
115       let res = {
116         id: stmt.row.id,
117         name: stmt.row.name,
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+"'>",
122       };
123       stmt.reset();
124       return res;
125     }
126   } catch (e) {}
127   stmt.reset();
128   return null;
132 // -1: unknown
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");
137   try {
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;
143       stmt.reset();
144       return res;
145     }
146     //conlog("pkgid=", pkgid, "; alas");
147   } catch (e) {
148     logException("getPackageUpdateTimeById", e);
149   }
150   stmt.reset();
151   return -1;
155 // -1: unknown
156 PackageDB.prototype.getPackageUpdateTimeByName = function (name) {
157   let nfo = this.getActivePackageByName(name);
158   if (!nfo) return -1;
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");
167   try {
168     stmt.params.pkgid = pkgid;
169     stmt.params.luptime = luptime;
170     stmt.execute();
171   } catch (e) {}
172   stmt.reset();
173   return true;
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");
180   try {
181     while (stmt.executeStep()) {
182       let rr = {
183         id: stmt.row.id,
184         name: stmt.row.name,
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+"'>",
189       };
190       res.push(rr);
191     }
192   } catch (e) {
193   } finally {
194     stmt.reset();
195   }
196   return res;
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");
203   try {
204     stmt.params.pkgid = pkgid;
205     while (stmt.executeStep()) {
206       res.push(stmt.row.diskname);
207     }
208   } catch (e) {
209   } finally {
210     stmt.reset();
211   }
212   return res;
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.]$/;
220   let res = "";
221   for (let ch of s) {
222     if (!validRE.test(ch)) ch = "_";
223     res += ch;
224   }
225   if (res.length && res[0] == "_") res = "pkg"+res;
226   return res;
230 // options:
231 //   bool checkOnly
232 //   bool ignoreVersion
233 //   bool forceUpdate
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 = {};
240   let pdb = this.db;
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};
246     return null;
247   }
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());
256       }
257       return false;
258     }
259   }
261   // need to update
262   if (options.checkOnly) return true;
264   try {
265     pdb.executeSimpleSQL("BEGIN TRANSACTION;");
267     // build package directory, and create it if necessary
268     let pkdir = getUserPkgDir();
269     if (!pi) {
270       // we want new directory
271       pi = {};
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
278       {
279         let stmt = pdb.createStatement("INSERT OR REPLACE INTO packages (name, dirname, version, downurl, state) VALUES (:name, :dirname, :version, :downurl, 0)");
280         try {
281           stmt.params.name = pkgname;
282           stmt.params.dirname = pi.dirname;
283           stmt.params.version = pi.version;
284           stmt.params.downurl = dnpkg.downurl;
285           stmt.execute();
286         } finally {
287           stmt.reset();
288         }
289       }
290       pi = getPkgInfo();
291       if (!pi) throw new Error("something is wrong with package database");
292     } else {
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");
296     }
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);
309       return nn.path;
310     }
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);
317     // update resources
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);
322     // fill requires
323     {
324       let stmt = pdb.createStatement("INSERT OR REPLACE INTO requires (pkgid, url, diskname, sha512) VALUES (:pkgid, :url, :diskname, :sha512)");
325       // insert main js
326       stmt.params.pkgid = pi.id;
327       stmt.params.url = dnpkg.downurl;
328       stmt.params.diskname = dnpkg.diskname;
329       stmt.params.sha512 = dnpkg.sha512;
330       stmt.execute();
331       stmt.reset();
332       // insert other js files
333       for (let res of dnpkg.resources) {
334         if (res.resname !== null) continue;
335         stmt.reset();
336         stmt.params.pkgid = pi.id;
337         stmt.params.url = res.url;
338         stmt.params.diskname = res.diskname;
339         stmt.params.sha512 = res.sha512;
340         stmt.execute();
341         stmt.reset();
342       }
343     }
345     // fill resources
346     {
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;
350         stmt.reset();
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";
357         stmt.execute();
358         stmt.reset();
359       }
360     }
362     pdb.executeSimpleSQL("COMMIT;");
363     // update update time ;-)
364     this.setPackageUpdateTimeById(pi.id, nowSeconds());
365     return true;
366   } catch (e) {
367     pdb.executeSimpleSQL("ROLLBACK;");
368     throw e;
369   }
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();
381   let pdb = this.db;
383   function getPackageIds () {
384     let stmt = pdb.createStatement("SELECT id FROM packages WHERE state=0 ORDER BY name ASC");
385     try {
386       while (stmt.executeStep()) res.push({id:stmt.row.id});
387     } catch (e) {
388     } finally {
389       stmt.reset();
390     }
391   }
393   getPackageIds();
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);
400     // first JS files
401     reqmt.reset();
402     reqmt.params.pkgid = pi.id;
403     while (reqmt.executeStep()) {
404       //conlog("req: ", reqmt.row.diskname);
405       if (!pi.reqs) {
406         pi.path = reqmt.row.diskname;
407         pi.reqs = new Array();
408       } else {
409         pi.reqs.push(reqmt.row.diskname);
410       }
411     }
413     // then resources
414     resmt.reset();
415     resmt.params.pkgid = pi.id;
416     pi.rsrc = {};
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};
420     }
421   }
422   reqmt.reset();
423   resmt.reset();
425   return res;
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+"'");
434   let pdb = this.db;
435   try {
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
443   } catch (e) {
444     pdb.executeSimpleSQL("ROLLBACK;");
445     throw e;
446   }
447   let pkdir = getUserPkgDir();
448   pkdir.append(pi.dirname);
449   if (pkdir.exists() && pkdir.isDirectory()) {
450     try { pkdir.remove(true); } catch (e) {}
451   }
455 ////////////////////////////////////////////////////////////////////////////////
456 exports.PackageDB = PackageDB;