chiark / gitweb /
automatically upgrade on startup if necessary
[disorder] / server / dbupgrade.c
CommitLineData
3dc3d7db
RK
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
32#include "syscalls.h"
33#include "log.h"
34#include "defs.h"
35#include "kvp.h"
36#include "trackdb.h"
37#include "trackdb-int.h"
38#include "mem.h"
39#include "configuration.h"
40#include "unicode.h"
41
42static DB_TXN *global_tid;
43
44static const struct option options[] = {
45 { "help", no_argument, 0, 'h' },
46 { "version", no_argument, 0, 'V' },
47 { "config", required_argument, 0, 'c' },
48 { "debug", no_argument, 0, 'd' },
49 { "no-debug", no_argument, 0, 'D' },
50 { "syslog", no_argument, 0, 's' },
51 { "no-syslog", no_argument, 0, 'S' },
52 { 0, 0, 0, 0 }
53};
54
55/* display usage message and terminate */
56static void help(void) {
57 xprintf("Usage:\n"
58 " disorder-dbupgrade [OPTIONS]\n"
59 "Options:\n"
60 " --help, -h Display usage message\n"
61 " --version, -V Display version number\n"
62 " --config PATH, -c PATH Set configuration file\n"
63 " --debug, -d Turn on debugging\n"
64 " --[no-]syslog Force logging\n"
65 "\n"
66 "Database upgrader for DisOrder. Not intended to be run\n"
67 "directly.\n");
68 xfclose(stdout);
69 exit(0);
70}
71
72/* display version number and terminate */
73static void version(void) {
74 xprintf("disorder-dbupgrade version %s\n", disorder_version_string);
75 xfclose(stdout);
76 exit(0);
77}
78
79/** @brief Visit each key in a database and call @p callback
80 * @return 0 or DB_LOCK_DEADLOCK
81 *
82 * @p global_tid must be set. @p callback should return 0 or DB_LOCK_DEADLOCK.
83 */
84static int scan_core(const char *name, DB *db,
85 int (*callback)(const char *name, DB *db, DBC *c,
86 DBT *k, DBT *d)) {
87 long count = 0;
88 DBC *c = trackdb_opencursor(db, global_tid);
89 int err, r = 0;
90 DBT k[1], d[1];
91
92 memset(k, 0, sizeof k);
93 memset(d, 0, sizeof d);
94 while((err = c->c_get(c, k, d, DB_NEXT)) == 0) {
95 if((err = callback(name, db, c, k, d)))
96 break;
97 ++count;
98 if(count % 1000 == 0)
99 info("scanning %s, %ld so far", name, count);
100 }
101 if(err && err != DB_NOTFOUND && err != DB_LOCK_DEADLOCK)
102 fatal(0, "%s: error scanning database: %s", name, db_strerror(err));
103 r = (err == DB_LOCK_DEADLOCK ? err : 0);
104 if((err = c->c_close(c)))
105 fatal(0, "%s: error closing cursor: %s", name, db_strerror(err));
106 return r;
107}
108
109/** @brief Visit each key in a database and call @p callback
110 *
111 * Everything happens inside the @p global_tid tranasction. @p callback
112 * should return 0 or DB_LOCK_DEADLOCK.
113 */
114static void scan(const char *name, DB *db,
115 int (*callback)(const char *name, DB *db, DBC *c,
116 DBT *k, DBT *d)) {
117 info("scanning %s", name);
118 for(;;) {
119 global_tid = trackdb_begin_transaction();
120 if(scan_core(name, db, callback)) {
121 trackdb_abort_transaction(global_tid);
122 global_tid = 0;
123 error(0, "detected deadlock, restarting scan");
124 continue;
125 } else {
126 trackdb_commit_transaction(global_tid);
127 global_tid = 0;
128 break;
129 }
130 }
131}
132
133/** @brief Truncate database @p db */
134static void truncate_database(const char *name, DB *db) {
135 u_int32_t count;
136 int err;
137
138 do {
139 err = db->truncate(db, 0, &count, DB_AUTO_COMMIT);
140 } while(err == DB_LOCK_DEADLOCK);
141 if(err)
142 fatal(0, "error truncating %s: %s", name, db_strerror(err));
143}
144
145/* scan callbacks */
146
147static int normalize_keys(const char *name, DB *db, DBC *c,
148 DBT *k, DBT *d) {
149 char *knfc;
150 size_t nknfc;
151 int err;
152
153 /* Find the normalized form of the key */
154 knfc = utf8_compose_canon(k->data, k->size, &nknfc);
155 if(!knfc)
156 fatal(0, "%s: cannot convert key to NFC: %.*s", name,
157 (int)k->size, (const char *)k->data);
158 /* If the key is already in NFC then do nothing */
159 if(nknfc == k->size && !memcmp(k->data, knfc, nknfc))
160 return 0;
161 /* To rename the key we must delete the old one and insert a new one */
162 if((err = c->c_del(c, 0))) {
163 if(err != DB_LOCK_DEADLOCK)
164 fatal(0, "%s: error removing denormalized key: %s",
165 name, db_strerror(err));
166 return err;
167 }
168 k->size = nknfc;
169 k->data = knfc;
170 if((err = db->put(db, global_tid, k, d, DB_NOOVERWRITE))) {
171 if(err != DB_LOCK_DEADLOCK)
172 fatal(0, "%s: error storing normalized key: %s", name, db_strerror(err));
173 return err;
174 }
175 return 0;
176}
177
178static int normalize_values(const char *name, DB *db,
179 DBC attribute((unused)) *c,
180 DBT *k, DBT *d) {
181 char *dnfc;
182 size_t ndnfc;
183 int err;
184
185 /* Find the normalized form of the value */
186 dnfc = utf8_compose_canon(d->data, d->size, &ndnfc);
187 if(!dnfc)
188 fatal(0, "%s: cannot convert data to NFC: %.*s", name,
189 (int)d->size, (const char *)d->data);
190 /* If the key is already in NFC then do nothing */
191 if(ndnfc == d->size && !memcmp(d->data, dnfc, ndnfc))
192 return 0;
193 d->size = ndnfc;
194 d->data = dnfc;
195 if((err = db->put(db, global_tid, k, d, 0))) {
196 if(err != DB_LOCK_DEADLOCK)
197 fatal(0, "%s: error storing normalized data: %s", name, db_strerror(err));
198 return err;
199 }
200 return 0;
201}
202
203static int renotice(const char *name, DB attribute((unused)) *db,
204 DBC attribute((unused)) *c,
205 DBT *k, DBT *d) {
206 const struct kvp *const t = kvp_urldecode(d->data, d->size);
207 const char *const track = xstrndup(k->data, k->size);
208 const char *const path = kvp_get(t, "_path");
209 int err;
210
211 if(!path)
212 fatal(0, "%s: no '_path' for %.*s", name,
213 (int)k->size, (const char *)k->data);
214 switch(err = trackdb_notice_tid(track, path, global_tid)) {
215 case 0:
216 return 0;
217 case DB_LOCK_DEADLOCK:
218 return err;
219 default:
220 fatal(0, "%s: unexpected return from trackdb_notice_tid: %s",
221 name, db_strerror(err));
222 }
223}
224
225static int remove_aliases_normalize_keys(const char *name, DB *db, DBC *c,
226 DBT *k, DBT *d) {
227 const struct kvp *const t = kvp_urldecode(d->data, d->size);
228 int err;
229
230 if(kvp_get(t, "_alias_for")) {
231 /* This is an alias. We remove all the alias entries. */
232 if((err = c->c_del(c, 0))) {
233 if(err != DB_LOCK_DEADLOCK)
234 fatal(0, "%s: error removing alias: %s", name, db_strerror(err));
235 return err;
236 }
237 return 0;
238 }
239 return normalize_keys(name, db, c, k, d);
240}
241
242/** @brief Upgrade the database to the current version
243 *
244 * This function is supposed to be idempotent, so if it is interrupted
245 * half way through it is safe to restart.
246 */
247static void upgrade(void) {
248 char buf[32];
249
250 info("upgrading database to dbversion %ld", config->dbversion);
251 /* Normalize keys and values as required. We will also remove aliases as
252 * they will be regenerated when we re-noticed the tracks. */
253 info("renormalizing keys");
254 scan("tracks.db", trackdb_tracksdb, remove_aliases_normalize_keys);
255 scan("prefs.db", trackdb_prefsdb, normalize_keys);
256 scan("global.db", trackdb_globaldb, normalize_keys);
257 scan("noticed.db", trackdb_noticeddb, normalize_values);
258 /* search.db and tags.db we will rebuild */
259 info("regenerating search database and aliases");
260 truncate_database("search.db", trackdb_searchdb);
261 truncate_database("tags.db", trackdb_tagsdb);
262 /* Regenerate the search database and aliases */
263 scan("tracks.db", trackdb_tracksdb, renotice);
264 /* Finally update the database version */
265 snprintf(buf, sizeof buf, "%ld", config->dbversion);
266 trackdb_set_global("_dbversion", buf, 0);
267 info("completed database upgrade");
268}
269
270int main(int argc, char **argv) {
271 int n, logsyslog = !isatty(2);
272
273 set_progname(argv);
274 mem_init();
275 if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
276 while((n = getopt_long(argc, argv, "hVc:dDSs", options, 0)) >= 0) {
277 switch(n) {
278 case 'h': help();
279 case 'V': version();
280 case 'c': configfile = optarg; break;
281 case 'd': debugging = 1; break;
282 case 'D': debugging = 0; break;
283 case 'S': logsyslog = 0; break;
284 case 's': logsyslog = 1; break;
285 default: fatal(0, "invalid option");
286 }
287 }
288 /* If stderr is a TTY then log there, otherwise to syslog. */
289 if(logsyslog) {
290 openlog(progname, LOG_PID, LOG_DAEMON);
291 log_default = &log_syslog;
292 }
293 if(config_read(0)) fatal(0, "cannot read configuration");
294 /* Open the database */
d25c4615
RK
295 trackdb_init(TRACKDB_NO_RECOVER);
296 trackdb_open(TRACKDB_OPEN_FOR_UPGRADE);
3dc3d7db
RK
297 upgrade();
298 return 0;
299}
300
301/*
302Local Variables:
303c-basic-offset:2
304comment-column:40
305fill-column:79
306indent-tabs-mode:nil
307End:
308*/