chiark / gitweb /
Switch to GPL v3
[disorder] / server / dbupgrade.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2007, 2008 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 #include "disorder-server.h"
19
20 static DB_TXN *global_tid;
21
22 #define BADKEY_WARN 0
23 #define BADKEY_FAIL 1
24 #define BADKEY_DELETE 2
25
26 /** @brief Bad key behavior */
27 static int badkey = BADKEY_WARN;
28
29 static long aliases_removed, keys_normalized, values_normalized, renoticed;
30 static long keys_already_ok, values_already_ok;
31
32 static const struct option options[] = {
33   { "help", no_argument, 0, 'h' },
34   { "version", no_argument, 0, 'V' },
35   { "config", required_argument, 0, 'c' },
36   { "debug", no_argument, 0, 'd' },
37   { "no-debug", no_argument, 0, 'D' },
38   { "delete-bad-keys", no_argument, 0, 'x' },
39   { "fail-bad-keys", no_argument, 0, 'X' },
40   { "syslog", no_argument, 0, 's' },
41   { "no-syslog", no_argument, 0, 'S' },
42   { 0, 0, 0, 0 }
43 };
44
45 /* display usage message and terminate */
46 static void help(void) {
47   xprintf("Usage:\n"
48           "  disorder-dbupgrade [OPTIONS]\n"
49           "Options:\n"
50           "  --help, -h              Display usage message\n"
51           "  --version, -V           Display version number\n"
52           "  --config PATH, -c PATH  Set configuration file\n"
53           "  --debug, -d             Turn on debugging\n"
54           "  --[no-]syslog           Force logging\n"
55           "  --delete-bad-keys, -x   Delete unconvertible keys\n"
56           "  --fail-bad-keys, -X     Fail if bad keys are found\n"
57           "\n"
58           "Database upgrader for DisOrder.  Not intended to be run\n"
59           "directly.\n");
60   xfclose(stdout);
61   exit(0);
62 }
63
64 /** @brief Visit each key in a database and call @p callback
65  * @return 0 or DB_LOCK_DEADLOCK
66  *
67  * @p global_tid must be set.  @p callback should return 0 or DB_LOCK_DEADLOCK.
68  */
69 static int scan_core(const char *name, DB *db,
70                      int (*callback)(const char *name, DB *db, DBC *c,
71                                      DBT *k, DBT *d)) {
72   long count = 0;
73   DBC *c = trackdb_opencursor(db, global_tid);
74   int err, r = 0;
75   DBT k[1], d[1];
76
77   values_normalized = 0;
78   keys_normalized = 0;
79   aliases_removed = 0;
80   renoticed = 0;
81   keys_already_ok = 0;
82   values_already_ok = 0;
83   memset(k, 0, sizeof k);
84   memset(d, 0, sizeof d);
85   while((err = c->c_get(c, k, d, DB_NEXT)) == 0) {
86     if((err = callback(name, db, c, k, d)))
87       break;
88     ++count;
89     if(count % 1000 == 0)
90       info("scanning %s, %ld so far", name, count);
91   }
92   if(err && err != DB_NOTFOUND && err != DB_LOCK_DEADLOCK)
93     fatal(0, "%s: error scanning database: %s", name, db_strerror(err));
94   r = (err == DB_LOCK_DEADLOCK ? err : 0);
95   if((err = c->c_close(c)))
96     fatal(0, "%s: error closing cursor: %s", name, db_strerror(err));
97   info("%s: %ld entries scanned", name, count);
98   if(values_normalized || values_already_ok)
99     info("%s: %ld values converted, %ld already ok", name,
100          values_normalized, values_already_ok);
101   if(keys_normalized || keys_already_ok)
102     info("%s: %ld keys converted, %ld already OK", name,
103          keys_normalized, keys_already_ok);
104   if(aliases_removed)
105     info("%s: %ld aliases removed", name, aliases_removed);
106   if(renoticed)
107     info("%s: %ld tracks re-noticed", name, renoticed);
108   return r;
109 }
110
111 /** @brief Visit each key in a database and call @p callback
112  *
113  * Everything happens inside the @p global_tid tranasction.  @p callback
114  * should return 0 or DB_LOCK_DEADLOCK.
115  */
116 static void scandb(const char *name, DB *db,
117                    int (*callback)(const char *name, DB *db, DBC *c,
118                                    DBT *k, DBT *d)) {
119   info("scanning %s", name);
120   for(;;) {
121     global_tid = trackdb_begin_transaction();
122     if(scan_core(name, db, callback)) {
123       trackdb_abort_transaction(global_tid);
124       global_tid = 0;
125       error(0, "detected deadlock, restarting scan");
126       continue;
127     } else {
128       trackdb_commit_transaction(global_tid);
129       global_tid = 0;
130       break;
131     }
132   }
133 }
134
135 /** @brief Truncate database @p db */
136 static void truncate_database(const char *name, DB *db) {
137   u_int32_t count;
138   int err;
139   
140   do {
141     err = db->truncate(db, 0, &count, DB_AUTO_COMMIT);
142   } while(err == DB_LOCK_DEADLOCK);
143   if(err)
144     fatal(0, "error truncating %s: %s", name, db_strerror(err));
145 }
146
147 /* scan callbacks */
148
149 static int normalize_keys(const char *name, DB *db, DBC *c,
150                           DBT *k, DBT *d) {
151   char *knfc;
152   size_t nknfc;
153   int err;
154
155   /* Find the normalized form of the key */
156   knfc = utf8_compose_canon(k->data, k->size, &nknfc);
157   if(!knfc) {
158     switch(badkey) {
159     case BADKEY_WARN:
160       error(0, "%s: invalid key: %.*s", name,
161             (int)k->size, (const char *)k->data);
162       break;
163     case BADKEY_DELETE:
164       error(0, "%s: deleting invalid key: %.*s", name,
165             (int)k->size, (const char *)k->data);
166       if((err = c->c_del(c, 0))) {
167         if(err != DB_LOCK_DEADLOCK)
168           fatal(0, "%s: error removing denormalized key: %s",
169                 name, db_strerror(err));
170         return err;
171       }
172       break;
173     case BADKEY_FAIL:
174       fatal(0, "%s: invalid key: %.*s", name,
175             (int)k->size, (const char *)k->data);
176     }
177     return 0;
178   }
179   /* If the key is already in NFC then do nothing */
180   if(nknfc == k->size && !memcmp(k->data, knfc, nknfc)) {
181     ++keys_already_ok;
182     return 0;
183   }
184   /* To rename the key we must delete the old one and insert a new one */
185   if((err = c->c_del(c, 0))) {
186     if(err != DB_LOCK_DEADLOCK)
187       fatal(0, "%s: error removing denormalized key: %s",
188             name, db_strerror(err));
189     return err;
190   }
191   k->size = nknfc;
192   k->data = knfc;
193   if((err = db->put(db, global_tid, k, d, DB_NOOVERWRITE))) {
194     if(err != DB_LOCK_DEADLOCK)
195       fatal(0, "%s: error storing normalized key: %s", name, db_strerror(err));
196     return err;
197   }
198   ++keys_normalized;
199   return 0;
200 }
201
202 static int normalize_values(const char *name, DB *db,
203                             DBC attribute((unused)) *c,
204                             DBT *k, DBT *d) {
205   char *dnfc;
206   size_t ndnfc;
207   int err;
208
209   /* Find the normalized form of the value */
210   dnfc = utf8_compose_canon(d->data, d->size, &ndnfc);
211   if(!dnfc)
212     fatal(0, "%s: cannot convert data to NFC: %.*s", name,
213           (int)d->size, (const char *)d->data);
214   /* If the key is already in NFC then do nothing */
215   if(ndnfc == d->size && !memcmp(d->data, dnfc, ndnfc)) {
216     ++values_already_ok;
217     return 0;
218   }
219   d->size = ndnfc;
220   d->data = dnfc;
221   if((err = db->put(db, global_tid, k, d, 0))) {
222     if(err != DB_LOCK_DEADLOCK)
223       fatal(0, "%s: error storing normalized data: %s", name, db_strerror(err));
224     return err;
225   }
226   ++values_normalized;
227   return 0;
228 }
229
230 static int renotice(const char *name, DB attribute((unused)) *db,
231                     DBC attribute((unused)) *c,
232                     DBT *k, DBT *d) {
233   const struct kvp *const t = kvp_urldecode(d->data, d->size);
234   const char *const track = xstrndup(k->data, k->size);
235   const char *const path = kvp_get(t, "_path");
236   int err;
237
238   if(!path) {
239     /* If an alias sorts later than the actual filename then it'll appear
240      * in the scan. */
241     if(kvp_get(t, "_alias_for"))
242       return 0;
243     fatal(0, "%s: no '_path' for %.*s", name,
244           (int)k->size, (const char *)k->data);
245   }
246   switch(err = trackdb_notice_tid(track, path, global_tid)) {
247   case 0:
248     ++renoticed;
249     return 0;
250   case DB_LOCK_DEADLOCK:
251     return err;
252   default:
253     fatal(0, "%s: unexpected return from trackdb_notice_tid: %s",
254           name, db_strerror(err));
255   }
256 }
257  
258 static int remove_aliases_normalize_keys(const char *name, DB *db, DBC *c,
259                                          DBT *k, DBT *d) {
260   const struct kvp *const t = kvp_urldecode(d->data, d->size);
261   int err;
262
263   if(kvp_get(t, "_alias_for")) {
264     /* This is an alias.  We remove all the alias entries. */
265     if((err = c->c_del(c, 0))) {
266       if(err != DB_LOCK_DEADLOCK)
267         fatal(0, "%s: error removing alias: %s", name, db_strerror(err));
268       return err;
269     }
270     ++aliases_removed;
271     return 0;
272   } else if(!kvp_get(t, "_path"))
273     error(0, "%s: %.*s has neither _alias_for nor _path", name,
274           (int)k->size, (const char *)k->data);
275   return normalize_keys(name, db, c, k, d);
276 }
277
278 /** @brief Upgrade the database to the current version
279  *
280  * This function is supposed to be idempotent, so if it is interrupted
281  * half way through it is safe to restart.
282  */
283 static void upgrade(void) {
284   char buf[32];
285
286   info("upgrading database to dbversion %ld", config->dbversion);
287   /* Normalize keys and values as required.  We will also remove aliases as
288    * they will be regenerated when we re-noticed the tracks. */
289   info("renormalizing keys");
290   scandb("tracks.db", trackdb_tracksdb, remove_aliases_normalize_keys);
291   scandb("prefs.db", trackdb_prefsdb, normalize_keys);
292   scandb("global.db", trackdb_globaldb, normalize_keys);
293   scandb("noticed.db", trackdb_noticeddb, normalize_values);
294   /* search.db and tags.db we will rebuild */
295   info("regenerating search database and aliases");
296   truncate_database("search.db", trackdb_searchdb);
297   truncate_database("tags.db", trackdb_tagsdb);
298   /* Regenerate the search database and aliases */
299   scandb("tracks.db", trackdb_tracksdb, renotice);
300   /* Finally update the database version */
301   snprintf(buf, sizeof buf, "%ld", config->dbversion);
302   trackdb_set_global("_dbversion", buf, 0);
303   info("completed database upgrade");
304 }
305
306 int main(int argc, char **argv) {
307   int n, logsyslog = !isatty(2);
308   
309   set_progname(argv);
310   mem_init();
311   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
312   while((n = getopt_long(argc, argv, "hVc:dDSsxX", options, 0)) >= 0) {
313     switch(n) {
314     case 'h': help();
315     case 'V': version("disorder-dbupgrade");
316     case 'c': configfile = optarg; break;
317     case 'd': debugging = 1; break;
318     case 'D': debugging = 0; break;
319     case 'S': logsyslog = 0; break;
320     case 's': logsyslog = 1; break;
321     case 'x': badkey = BADKEY_DELETE; break;
322     case 'X': badkey = BADKEY_FAIL; break;
323     default: fatal(0, "invalid option");
324     }
325   }
326   /* If stderr is a TTY then log there, otherwise to syslog. */
327   if(logsyslog) {
328     openlog(progname, LOG_PID, LOG_DAEMON);
329     log_default = &log_syslog;
330   }
331   if(config_read(0)) fatal(0, "cannot read configuration");
332   /* Open the database */
333   trackdb_init(TRACKDB_NO_RECOVER);
334   trackdb_open(TRACKDB_OPEN_FOR_UPGRADE);
335   upgrade();
336   return 0;
337 }
338
339 /*
340 Local Variables:
341 c-basic-offset:2
342 comment-column:40
343 fill-column:79
344 indent-tabs-mode:nil
345 End:
346 */