chiark / gitweb /
New scripts/setup which interactively sets up a DisOrder configuration
[disorder] / server / dbupgrade.c
... / ...
CommitLineData
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
44static 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 */
51static int badkey = BADKEY_WARN;
52
53static long aliases_removed, keys_normalized, values_normalized, renoticed;
54static long keys_already_ok, values_already_ok;
55
56static 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 */
70static 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 */
89static 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 */
100static 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 */
147static 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 */
167static 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
180static 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
233static 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
261static 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
289static 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 */
314static 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
337int 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/*
371Local Variables:
372c-basic-offset:2
373comment-column:40
374fill-column:79
375indent-tabs-mode:nil
376End:
377*/