chiark / gitweb /
Doxygen-clean
[disorder] / server / dbupgrade.c
CommitLineData
3dc3d7db
RK
1/*
2 * This file is part of DisOrder
5aff007d 3 * Copyright (C) 2007, 2008 Richard Kettlewell
3dc3d7db 4 *
e7eb3a27 5 * This program is free software: you can redistribute it and/or modify
3dc3d7db 6 * it under the terms of the GNU General Public License as published by
e7eb3a27 7 * the Free Software Foundation, either version 3 of the License, or
3dc3d7db
RK
8 * (at your option) any later version.
9 *
e7eb3a27
RK
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 *
3dc3d7db 15 * You should have received a copy of the GNU General Public License
e7eb3a27 16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
3dc3d7db 17 */
05b75f8d 18#include "disorder-server.h"
3dc3d7db
RK
19
20static DB_TXN *global_tid;
21
de3bd7ad 22#define BADKEY_WARN 0
23#define BADKEY_FAIL 1
24#define BADKEY_DELETE 2
25
26/** @brief Bad key behavior */
27static int badkey = BADKEY_WARN;
28
29static long aliases_removed, keys_normalized, values_normalized, renoticed;
30static long keys_already_ok, values_already_ok;
31
3dc3d7db
RK
32static 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' },
de3bd7ad 38 { "delete-bad-keys", no_argument, 0, 'x' },
39 { "fail-bad-keys", no_argument, 0, 'X' },
3dc3d7db
RK
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 */
46static 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"
de3bd7ad 55 " --delete-bad-keys, -x Delete unconvertible keys\n"
56 " --fail-bad-keys, -X Fail if bad keys are found\n"
3dc3d7db
RK
57 "\n"
58 "Database upgrader for DisOrder. Not intended to be run\n"
59 "directly.\n");
60 xfclose(stdout);
61 exit(0);
62}
63
3dc3d7db
RK
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 */
69static 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
de3bd7ad 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;
3dc3d7db
RK
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));
de3bd7ad 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);
3dc3d7db
RK
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 */
05b75f8d
RK
116static void scandb(const char *name, DB *db,
117 int (*callback)(const char *name, DB *db, DBC *c,
118 DBT *k, DBT *d)) {
3dc3d7db
RK
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 */
136static 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
149static 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);
de3bd7ad 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 }
3dc3d7db 179 /* If the key is already in NFC then do nothing */
de3bd7ad 180 if(nknfc == k->size && !memcmp(k->data, knfc, nknfc)) {
181 ++keys_already_ok;
3dc3d7db 182 return 0;
de3bd7ad 183 }
3dc3d7db
RK
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 }
de3bd7ad 198 ++keys_normalized;
3dc3d7db
RK
199 return 0;
200}
201
202static 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 */
de3bd7ad 215 if(ndnfc == d->size && !memcmp(d->data, dnfc, ndnfc)) {
216 ++values_already_ok;
3dc3d7db 217 return 0;
de3bd7ad 218 }
3dc3d7db
RK
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 }
de3bd7ad 226 ++values_normalized;
3dc3d7db
RK
227 return 0;
228}
229
230static 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
de3bd7ad 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;
3dc3d7db
RK
243 fatal(0, "%s: no '_path' for %.*s", name,
244 (int)k->size, (const char *)k->data);
de3bd7ad 245 }
3dc3d7db
RK
246 switch(err = trackdb_notice_tid(track, path, global_tid)) {
247 case 0:
de3bd7ad 248 ++renoticed;
3dc3d7db
RK
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
258static 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 }
de3bd7ad 270 ++aliases_removed;
3dc3d7db 271 return 0;
de3bd7ad 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);
3dc3d7db
RK
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 */
283static 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");
05b75f8d
RK
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);
3dc3d7db
RK
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 */
05b75f8d 299 scandb("tracks.db", trackdb_tracksdb, renotice);
3dc3d7db
RK
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
306int 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");
de3bd7ad 312 while((n = getopt_long(argc, argv, "hVc:dDSsxX", options, 0)) >= 0) {
3dc3d7db
RK
313 switch(n) {
314 case 'h': help();
3fbdc96d 315 case 'V': version("disorder-dbupgrade");
3dc3d7db
RK
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;
de3bd7ad 321 case 'x': badkey = BADKEY_DELETE; break;
322 case 'X': badkey = BADKEY_FAIL; break;
3dc3d7db
RK
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 */
d25c4615
RK
333 trackdb_init(TRACKDB_NO_RECOVER);
334 trackdb_open(TRACKDB_OPEN_FOR_UPGRADE);
3dc3d7db
RK
335 upgrade();
336 return 0;
337}
338
339/*
340Local Variables:
341c-basic-offset:2
342comment-column:40
343fill-column:79
344indent-tabs-mode:nil
345End:
346*/