added "guerilla package" commands
[guerillascript.git] / modules / pkg / packagedb.js
blob0a8000607bbb69c33ff77797b8c55882ef4005b1
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 PackageDB () {
12   this.dbObj = null;
13   return this;
17 PackageDB.prototype.STATE_OK = 0;
18 PackageDB.prototype.STATE_INACTIVE = 1;
19 PackageDB.prototype.STATE_DISABLED = 2; // due to some errors
22 PackageDB.prototype.__defineGetter__("db", function () {
23   if (this.dbObj == null) {
24     if (!this.dbFile) {
25       this.dbFile = getUserPkgDBDir();
26       //this.dbFile.append("database");
27       if (!this.dbFile.exists()) this.dbFile.create(this.dbFile.DIRECTORY_TYPE, 0700);
28       this.dbFile.append("packages.db");
29     }
30     this.dbObj = Services.storage.openDatabase(this.dbFile);
31     // the auto_vacuum pragma has to be set before the table is created
32     this.dbObj.executeSimpleSQL("PRAGMA auto_vacuum = NONE;");
33     this.dbObj.executeSimpleSQL("PRAGMA journal_mode = DELETE;");
34     //
35     this.dbObj.executeSimpleSQL(
36       "CREATE TABLE IF NOT EXISTS packages (\n"+
37       "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique package id */
38       "  name TEXT NOT NULL UNIQUE,\n"+ /* package name */
39       "  dirname TEXT NOT NULL UNIQUE,\n"+ /* package dir name, valid disk file name inside package dir */
40       "  version TEXT NOT NULL,\n"+ /* package version */
41       "  downurl TEXT NOT NULL,\n"+ /* package update url */
42       "  state INTEGER NOT NULL DEFAULT 0\n"+ /* see PackageDB.STATE_* */
43       ");"
44     );
45     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name ON packages (name);");
46     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name_and_state ON packages (name, state);");
47     //
48     this.dbObj.executeSimpleSQL(
49       "CREATE TABLE IF NOT EXISTS resources (\n"+
50       "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id */
51       "  pkgid INTEGER NOT NULL,\n"+ /* unique package id */
52       "  url TEXT NOT NULL UNIQUE,\n"+ /* file url */
53       "  diskname TEXT NOT NULL,\n"+ /* disk file name; ""' means "main package file" */
54       "  sha512 TEXT NOT NULL,\n"+ /* sha-512 of the disk file */
55       "  resname TEXT NOT NULL DEFAULT ''\n"+ /* resource name for named resources */
56       ");"
57     );
58     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid ON resources (pkgid);");
59     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid_and_name ON resources (pkgid, resname);");
60     //
61     this.dbObj.executeSimpleSQL(
62       "CREATE TABLE IF NOT EXISTS requires (\n"+
63       "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id; first one is main js file */
64       "  pkgid INTEGER NOT NULL,\n"+ /* unique package id */
65       "  url TEXT NOT NULL UNIQUE,\n"+ /* file url */
66       "  diskname TEXT NOT NULL,\n"+ /* disk file name */
67       "  sha512 TEXT NOT NULL\n"+ /* sha-512 of the disk file */
68       ");"
69     );
70     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid ON requires (pkgid);");
71     this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid_and_id ON requires (pkgid, id);");
72     // run vacuum once manually to switch to the correct auto_vacuum mode for
73     // databases that were created with incorrect auto_vacuum
74     this.dbObj.executeSimpleSQL("VACUUM;");
75   }
76   return this.dbObj;
77 });
80 PackageDB.prototype.__defineGetter__("opened", function () {
81   return (this.dbObj != null);
82 });
85 PackageDB.prototype.close = function () {
86   if (this.dbObj) {
87     this.dbObj.close();
88     this.dbObj = null;
89     this.dbFile = null;
90   }
94 PackageDB.prototype.getActivePackageByName = function (name) {
95   if (typeof(name) !== "string" || !name) return null;
96   let stmt = this.db.createStatement("SELECT id,name,dirname,version,downurl FROM packages WHERE state=0 AND name=:name LIMIT 1");
97   try {
98     stmt.params.name = name;
99     if (stmt.executeStep()) {
100       let res = {};
101       res.id = stmt.row.id;
102       res.name = stmt.row.name;
103       res.dirname = stmt.row.dirname;
104       res.version = stmt.row.version;
105       res.url = stmt.row.downurl;
106       stmt.reset();
107       return res;
108     }
109   } catch (e) {}
110   stmt.reset();
111   return null;
115 PackageDB.prototype.getActivePackages = function () {
116   let res = new Array();
117   let stmt = this.db.createStatement("SELECT id,name,version,downurl FROM packages WHERE state=0 ORDER BY name ASC");
118   try {
119     while (stmt.executeStep()) {
120       let rr = {};
121       rr.id = stmt.row.id;
122       rr.name = stmt.row.name;
123       rr.version = stmt.row.version;
124       rr.url = stmt.row.downurl;
125       res.push(rr);
126     }
127   } catch (e) {
128   } finally {
129     stmt.reset();
130   }
131   return res;
135 PackageDB.prototype.getPackageJSFiles = function (pkgid) {
136   let res = new Array();
137   let stmt = this.db.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
138   try {
139     stmt.params.pkgid = pkgid;
140     while (stmt.executeStep()) {
141       res.push(stmt.row.diskname);
142     }
143   } catch (e) {
144   } finally {
145     stmt.reset();
146   }
147   return res;
151 ////////////////////////////////////////////////////////////////////////////////
152 // normalize string, so it can be used as disk file name
153 function normDiskStr (s) {
154   const validRE = /^[-A-Za-z_0-9.]$/;
155   let res = "";
156   for (let ch of s) {
157     if (!validRE.test(ch)) ch = "_";
158     res += ch;
159   }
160   if (res.length && res[0] == "_") res = "pkg"+res;
161   return res;
165 // options:
166 //   bool checkOnly
167 //   bool ignoreVersion
168 //   bool forceUpdate
169 // returns true if package should be updated
170 PackageDB.prototype.checkAndUpdate = function (pkgname, dnpkg, options) {
171   if (typeof(pkgname) !== "string" || !pkgname) throw new Error("invalid package name");
172   if (typeof(dnpkg) !== "object") throw new Error("package info object expected");
173   if (typeof(options) !== "object") options = {};
175   let pdb = this.db;
177   function getPkgInfo () {
178     let stmt = pdb.createStatement("SELECT id,version,dirname FROM packages WHERE name=:name LIMIT 1");
179     stmt.params.name = pkgname;
180     if (stmt.step()) return {id:stmt.row.id, version:stmt.row.version, dirname:stmt.row.dirname};
181     return null;
182   }
184   let pi = getPkgInfo();
185   if (!options.forceUpdate && !options.ignoreVersion && pi) {
186     if (pi.version == dnpkg.version) return false; // no need to do anything
187   }
189   // need to update
190   if (options.checkOnly) return true;
192   try {
193     pdb.executeSimpleSQL("BEGIN TRANSACTION;");
195     // build package directory, and create it if necessary
196     let pkdir = getUserPkgDir();
197     if (!pi) {
198       // we want new directory
199       pi = {};
200       pi.version = ""+dnpkg.version;
201       pi.dirname = normDiskStr(pkgname);
202       pkdir.append(pi.dirname);
203       pkdir.createUnique(pkdir.DIRECTORY_TYPE, 0700);
204       pi.dirname = pkdir.leafName;
205       // create database record, so we can get package id from it
206       {
207         let stmt = pdb.createStatement("INSERT OR REPLACE INTO packages (name, dirname, version, downurl, state) VALUES (:name, :dirname, :version, :downurl, 0)");
208         try {
209           stmt.params.name = pkgname;
210           stmt.params.dirname = pi.dirname;
211           stmt.params.version = pi.version;
212           stmt.params.downurl = dnpkg.downurl;
213           stmt.execute();
214         } finally {
215           stmt.reset();
216         }
217       }
218       pi = getPkgInfo();
219       if (!pi) throw new Error("something is wrong with package database");
220     } else {
221       // we want existing directory
222       pkdir.append(pi.dirname);
223       if (pkdir.exists() && !pkdir.isDirectory()) throw new Error("something is wrong with package storage");
224     }
226     // clear package directory
227     try { pkdir.remove(true); } catch (e) {}
228     try { if (!pkdir.exists()) pkdir.create(pkdir.DIRECTORY_TYPE, 0700); } catch (e) {}
230     function copyFile (srcpath) {
231       let fl = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
232       fl.initWithPath(srcpath);
233       //conlog("copying ["+srcpath+"] to ["+pkdir.path+"/]");
234       fl.copyTo(pkdir, fl.leafName);
235       let nn = pkdir.clone();
236       nn.append(fl.leafName);
237       return nn.path;
238     }
240     //TODO: make this async?
241     // copy package files
242     dnpkg.diskname = copyFile(dnpkg.diskname);
243     for (let res of dnpkg.resources) res.diskname = copyFile(res.diskname);
245     // update resources
246     // ah, id is always integer here
247     pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
248     pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
250     // fill requires
251     {
252       let stmt = pdb.createStatement("INSERT OR REPLACE INTO requires (pkgid, url, diskname, sha512) VALUES (:pkgid, :url, :diskname, :sha512)");
253       // insert main js
254       stmt.params.pkgid = pi.id;
255       stmt.params.url = dnpkg.downurl;
256       stmt.params.diskname = dnpkg.diskname;
257       stmt.params.sha512 = dnpkg.sha512;
258       stmt.execute();
259       stmt.reset();
260       // insert other js files
261       for (let res of dnpkg.resources) {
262         if (res.resname !== null) continue;
263         stmt.reset();
264         stmt.params.pkgid = pi.id;
265         stmt.params.url = res.url;
266         stmt.params.diskname = res.diskname;
267         stmt.params.sha512 = res.sha512;
268         stmt.execute();
269         stmt.reset();
270       }
271     }
273     // fill resources
274     {
275       let stmt = pdb.createStatement("INSERT OR REPLACE INTO resources (pkgid, url, diskname, sha512, resname) VALUES (:pkgid, :url, :diskname, :sha512, :resname)");
276       for (let res of dnpkg.resources) {
277         if (res.resname === null) continue;
278         stmt.reset();
279         stmt.params.pkgid = pi.id;
280         stmt.params.url = res.url;
281         stmt.params.diskname = res.diskname;
282         stmt.params.sha512 = res.sha512;
283         stmt.params.resname = res.resname;
284         stmt.execute();
285         stmt.reset();
286       }
287     }
289     pdb.executeSimpleSQL("COMMIT;");
290     return true;
291   } catch (e) {
292     pdb.executeSimpleSQL("ROLLBACK;");
293     throw e;
294   }
298 ////////////////////////////////////////////////////////////////////////////////
299 // this returns array of xnfos:
300 //  string path: path to main script
301 //  string[] reqs: list of required files
302 //  {string name} rsrc: resources dict, keyed by resource name, value is string path
303 PackageDB.prototype.getActivePackagesForCache = function () {
304   let res = new Array();
306   let pdb = this.db;
308   function getPackageIds () {
309     let stmt = pdb.createStatement("SELECT id FROM packages WHERE state=0 ORDER BY name ASC");
310     try {
311       while (stmt.executeStep()) res.push({id:stmt.row.id});
312     } catch (e) {
313     } finally {
314       stmt.reset();
315     }
316   }
318   getPackageIds();
319   //conlog("packages: "+res.length);
321   let reqmt = pdb.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
322   let resmt = pdb.createStatement("SELECT diskname,resname FROM resources WHERE pkgid=:pkgid");
323   for (let pi of res) {
324     //conlog("package: "+pi.id);
325     // first JS files
326     reqmt.reset();
327     reqmt.params.pkgid = pi.id;
328     while (reqmt.executeStep()) {
329       //conlog("req: ", reqmt.row.diskname);
330       if (!pi.reqs) {
331         pi.path = reqmt.row.diskname;
332         pi.reqs = new Array();
333       } else {
334         pi.reqs.push(reqmt.row.diskname);
335       }
336     }
338     // then resources
339     resmt.reset();
340     resmt.params.pkgid = pi.id;
341     pi.rsrc = {};
342     while (resmt.executeStep()) {
343       //conlog("res: ", resmt.row.resname, " : ", resmt.row.diskname);
344       pi.rsrc[resmt.row.resname] = resmt.row.diskname;
345     }
346   }
347   reqmt.reset();
348   resmt.reset();
350   return res;
354 ////////////////////////////////////////////////////////////////////////////////
355 PackageDB.prototype.removePackage = function (name) {
356   if (typeof(name) !== "string" || !name) throw new Error("invalid package name");
357   let pi = this.getActivePackageByName(name);
358   if (!pi) throw new Error("unknown package '"+name+"'");
359   let pdb = this.db;
360   try {
361     pdb.executeSimpleSQL("BEGIN TRANSACTION;");
362     pdb.executeSimpleSQL("DELETE FROM packages WHERE id="+pi.id);
363     pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
364     pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
365     pdb.executeSimpleSQL("COMMIT;");
366     // clear package directory
367   } catch (e) {
368     pdb.executeSimpleSQL("ROLLBACK;");
369     throw e;
370   }
371   let pkdir = getUserPkgDir();
372   pkdir.append(pi.dirname);
373   if (pkdir.exists() && pkdir.isDirectory()) {
374     try { pkdir.remove(true); } catch (e) {}
375   }
379 ////////////////////////////////////////////////////////////////////////////////
380 exports.PackageDB = PackageDB;