better directory layout; unification with non-bootstrapping addons
[guerillascript.git] / main / modules / pkg / packagedb.js
blob66c043fb9065c95dc8fa364bbf28b12e3b6e19c9
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.
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 //conlog("** package db dir: [", this.dbFile.path, "]");
27 //this.dbFile.append("database");
28 if (!this.dbFile.exists()) this.dbFile.create(this.dbFile.DIRECTORY_TYPE, 0700);
29 this.dbFile.append("packages.db");
30 //conlog("package db: [", this.dbFile.path, "]");
32 this.dbObj = Services.storage.openDatabase(this.dbFile);
33 // the auto_vacuum pragma has to be set before the table is created
34 this.dbObj.executeSimpleSQL("PRAGMA auto_vacuum = NONE;");
35 this.dbObj.executeSimpleSQL("PRAGMA journal_mode = DELETE;");
37 this.dbObj.executeSimpleSQL(
38 "CREATE TABLE IF NOT EXISTS packages (\n"+
39 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique package id */
40 " name TEXT NOT NULL UNIQUE,\n"+ /* package name */
41 " dirname TEXT NOT NULL UNIQUE,\n"+ /* package dir name, valid disk file name inside package dir */
42 " version TEXT NOT NULL,\n"+ /* package version */
43 " downurl TEXT NOT NULL,\n"+ /* package update url */
44 " state INTEGER NOT NULL DEFAULT 0\n"+ /* see PackageDB.STATE_* */
45 ");"
47 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name ON packages (name);");
48 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS packages_by_name_and_state ON packages (name, state);");
50 this.dbObj.executeSimpleSQL(
51 "CREATE TABLE IF NOT EXISTS resources (\n"+
52 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id */
53 " pkgid INTEGER NOT NULL,\n"+ /* unique package id */
54 " url TEXT NOT NULL UNIQUE,\n"+ /* file url */
55 " diskname TEXT NOT NULL,\n"+ /* disk file name; ""' means "main package file" */
56 " sha512 TEXT NOT NULL,\n"+ /* sha-512 of the disk file */
57 " resname TEXT NOT NULL DEFAULT '',\n"+ /* resource name for named resources */
58 " contenttype TEXT NOT NULL DEFAULT 'application/octet-stream'\n"+ /* resource content type */
59 ");"
61 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid ON resources (pkgid);");
62 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS rsrcs_by_pkgid_and_name ON resources (pkgid, resname);");
64 this.dbObj.executeSimpleSQL(
65 "CREATE TABLE IF NOT EXISTS requires (\n"+
66 " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"+ /* unique file id; first one is main js file */
67 " pkgid INTEGER NOT NULL,\n"+ /* unique package id */
68 " url TEXT NOT NULL UNIQUE,\n"+ /* file url */
69 " diskname TEXT NOT NULL,\n"+ /* disk file name */
70 " sha512 TEXT NOT NULL\n"+ /* sha-512 of the disk file */
71 ");"
73 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid ON requires (pkgid);");
74 this.dbObj.executeSimpleSQL("CREATE INDEX IF NOT EXISTS reqs_by_pkgid_and_id ON requires (pkgid, id);");
75 // run vacuum once manually to switch to the correct auto_vacuum mode for
76 // databases that were created with incorrect auto_vacuum
77 this.dbObj.executeSimpleSQL("VACUUM;");
79 return this.dbObj;
80 });
83 PackageDB.prototype.__defineGetter__("opened", function () {
84 return (this.dbObj != null);
85 });
88 PackageDB.prototype.close = function () {
89 if (this.dbObj) {
90 this.dbObj.close();
91 this.dbObj = null;
92 this.dbFile = null;
97 PackageDB.prototype.getActivePackageByName = function (name) {
98 if (typeof(name) !== "string" || !name) return null;
99 let stmt = this.db.createStatement("SELECT id,name,dirname,version,downurl FROM packages WHERE state=0 AND name=:name LIMIT 1");
100 try {
101 stmt.params.name = name;
102 if (stmt.executeStep()) {
103 let res = {};
104 res.id = stmt.row.id;
105 res.name = stmt.row.name;
106 res.dirname = stmt.row.dirname;
107 res.version = stmt.row.version;
108 res.url = stmt.row.downurl;
109 stmt.reset();
110 return res;
112 } catch (e) {}
113 stmt.reset();
114 return null;
118 PackageDB.prototype.getActivePackages = function () {
119 let res = new Array();
120 let stmt = this.db.createStatement("SELECT id,name,dirname,version,downurl FROM packages WHERE state=0 ORDER BY name ASC");
121 try {
122 while (stmt.executeStep()) {
123 let rr = {};
124 rr.id = stmt.row.id;
125 rr.name = stmt.row.name;
126 rr.dirname = stmt.row.dirname;
127 rr.version = stmt.row.version;
128 rr.url = stmt.row.downurl;
129 res.push(rr);
131 } catch (e) {
132 } finally {
133 stmt.reset();
135 return res;
139 PackageDB.prototype.getPackageJSFiles = function (pkgid) {
140 let res = new Array();
141 let stmt = this.db.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
142 try {
143 stmt.params.pkgid = pkgid;
144 while (stmt.executeStep()) {
145 res.push(stmt.row.diskname);
147 } catch (e) {
148 } finally {
149 stmt.reset();
151 return res;
155 ////////////////////////////////////////////////////////////////////////////////
156 // normalize string, so it can be used as disk file name
157 function normDiskStr (s) {
158 const validRE = /^[-A-Za-z_0-9.]$/;
159 let res = "";
160 for (let ch of s) {
161 if (!validRE.test(ch)) ch = "_";
162 res += ch;
164 if (res.length && res[0] == "_") res = "pkg"+res;
165 return res;
169 // options:
170 // bool checkOnly
171 // bool ignoreVersion
172 // bool forceUpdate
173 // returns true if package should be updated
174 PackageDB.prototype.checkAndUpdate = function (pkgname, dnpkg, options) {
175 if (typeof(pkgname) !== "string" || !pkgname) throw new Error("invalid package name");
176 if (typeof(dnpkg) !== "object") throw new Error("package info object expected");
177 if (typeof(options) !== "object") options = {};
179 let pdb = this.db;
181 function getPkgInfo () {
182 let stmt = pdb.createStatement("SELECT id,version,dirname FROM packages WHERE name=:name LIMIT 1");
183 stmt.params.name = pkgname;
184 if (stmt.step()) return {id:stmt.row.id, version:stmt.row.version, dirname:stmt.row.dirname};
185 return null;
188 let pi = getPkgInfo();
189 if (!options.forceUpdate && !options.ignoreVersion && pi) {
190 if (pi.version == dnpkg.version) return false; // no need to do anything
193 // need to update
194 if (options.checkOnly) return true;
196 try {
197 pdb.executeSimpleSQL("BEGIN TRANSACTION;");
199 // build package directory, and create it if necessary
200 let pkdir = getUserPkgDir();
201 if (!pi) {
202 // we want new directory
203 pi = {};
204 pi.version = ""+dnpkg.version;
205 pi.dirname = normDiskStr(pkgname);
206 pkdir.append(pi.dirname);
207 pkdir.createUnique(pkdir.DIRECTORY_TYPE, 0700);
208 pi.dirname = pkdir.leafName;
209 // create database record, so we can get package id from it
211 let stmt = pdb.createStatement("INSERT OR REPLACE INTO packages (name, dirname, version, downurl, state) VALUES (:name, :dirname, :version, :downurl, 0)");
212 try {
213 stmt.params.name = pkgname;
214 stmt.params.dirname = pi.dirname;
215 stmt.params.version = pi.version;
216 stmt.params.downurl = dnpkg.downurl;
217 stmt.execute();
218 } finally {
219 stmt.reset();
222 pi = getPkgInfo();
223 if (!pi) throw new Error("something is wrong with package database");
224 } else {
225 // we want existing directory
226 pkdir.append(pi.dirname);
227 if (pkdir.exists() && !pkdir.isDirectory()) throw new Error("something is wrong with package storage");
230 // clear package directory
231 try { pkdir.remove(true); } catch (e) {}
232 try { if (!pkdir.exists()) pkdir.create(pkdir.DIRECTORY_TYPE, 0700); } catch (e) {}
234 function copyFile (srcpath) {
235 let fl = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
236 fl.initWithPath(srcpath);
237 //conlog("copying ["+srcpath+"] to ["+pkdir.path+"/]");
238 fl.copyTo(pkdir, fl.leafName);
239 let nn = pkdir.clone();
240 nn.append(fl.leafName);
241 return nn.path;
244 //TODO: make this async?
245 // copy package files
246 dnpkg.diskname = copyFile(dnpkg.diskname);
247 for (let res of dnpkg.resources) res.diskname = copyFile(res.diskname);
249 // update resources
250 // ah, id is always integer here
251 pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
252 pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
254 // fill requires
256 let stmt = pdb.createStatement("INSERT OR REPLACE INTO requires (pkgid, url, diskname, sha512) VALUES (:pkgid, :url, :diskname, :sha512)");
257 // insert main js
258 stmt.params.pkgid = pi.id;
259 stmt.params.url = dnpkg.downurl;
260 stmt.params.diskname = dnpkg.diskname;
261 stmt.params.sha512 = dnpkg.sha512;
262 stmt.execute();
263 stmt.reset();
264 // insert other js files
265 for (let res of dnpkg.resources) {
266 if (res.resname !== null) continue;
267 stmt.reset();
268 stmt.params.pkgid = pi.id;
269 stmt.params.url = res.url;
270 stmt.params.diskname = res.diskname;
271 stmt.params.sha512 = res.sha512;
272 stmt.execute();
273 stmt.reset();
277 // fill resources
279 let stmt = pdb.createStatement("INSERT OR REPLACE INTO resources (pkgid, url, diskname, sha512, resname, contenttype) VALUES (:pkgid, :url, :diskname, :sha512, :resname, :ct)");
280 for (let res of dnpkg.resources) {
281 if (res.resname === null) continue;
282 stmt.reset();
283 stmt.params.pkgid = pi.id;
284 stmt.params.url = res.url;
285 stmt.params.diskname = res.diskname;
286 stmt.params.sha512 = res.sha512;
287 stmt.params.resname = res.resname;
288 stmt.params.ct = res.contenttype||"application/octet-stream";
289 stmt.execute();
290 stmt.reset();
294 pdb.executeSimpleSQL("COMMIT;");
295 return true;
296 } catch (e) {
297 pdb.executeSimpleSQL("ROLLBACK;");
298 throw e;
303 ////////////////////////////////////////////////////////////////////////////////
304 // this returns array of xnfos:
305 // string path: path to main script
306 // string[] reqs: list of required files
307 // {string name} rsrc: resources dict, keyed by resource name, value is string path
308 PackageDB.prototype.getActivePackagesForCache = function () {
309 let res = new Array();
311 let pdb = this.db;
313 function getPackageIds () {
314 let stmt = pdb.createStatement("SELECT id FROM packages WHERE state=0 ORDER BY name ASC");
315 try {
316 while (stmt.executeStep()) res.push({id:stmt.row.id});
317 } catch (e) {
318 } finally {
319 stmt.reset();
323 getPackageIds();
324 //conlog("packages: "+res.length);
326 let reqmt = pdb.createStatement("SELECT diskname FROM requires WHERE pkgid=:pkgid ORDER BY id ASC");
327 let resmt = pdb.createStatement("SELECT diskname,resname,contenttype FROM resources WHERE pkgid=:pkgid");
328 for (let pi of res) {
329 //conlog("package: "+pi.id);
330 // first JS files
331 reqmt.reset();
332 reqmt.params.pkgid = pi.id;
333 while (reqmt.executeStep()) {
334 //conlog("req: ", reqmt.row.diskname);
335 if (!pi.reqs) {
336 pi.path = reqmt.row.diskname;
337 pi.reqs = new Array();
338 } else {
339 pi.reqs.push(reqmt.row.diskname);
343 // then resources
344 resmt.reset();
345 resmt.params.pkgid = pi.id;
346 pi.rsrc = {};
347 while (resmt.executeStep()) {
348 //conlog("res: ", resmt.row.resname, " : ", resmt.row.diskname);
349 pi.rsrc[resmt.row.resname] = {path:resmt.row.diskname, contenttype:resmt.row.contenttype};
352 reqmt.reset();
353 resmt.reset();
355 return res;
359 ////////////////////////////////////////////////////////////////////////////////
360 PackageDB.prototype.removePackage = function (name) {
361 if (typeof(name) !== "string" || !name) throw new Error("invalid package name");
362 let pi = this.getActivePackageByName(name);
363 if (!pi) throw new Error("unknown package '"+name+"'");
364 let pdb = this.db;
365 try {
366 pdb.executeSimpleSQL("BEGIN TRANSACTION;");
367 pdb.executeSimpleSQL("DELETE FROM packages WHERE id="+pi.id);
368 pdb.executeSimpleSQL("DELETE FROM resources WHERE pkgid="+pi.id);
369 pdb.executeSimpleSQL("DELETE FROM requires WHERE pkgid="+pi.id);
370 pdb.executeSimpleSQL("COMMIT;");
371 // clear package directory
372 } catch (e) {
373 pdb.executeSimpleSQL("ROLLBACK;");
374 throw e;
376 let pkdir = getUserPkgDir();
377 pkdir.append(pi.dirname);
378 if (pkdir.exists() && pkdir.isDirectory()) {
379 try { pkdir.remove(true); } catch (e) {}
384 ////////////////////////////////////////////////////////////////////////////////
385 exports.PackageDB = PackageDB;