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