chiark / gitweb /
Consistency check for finished tracks.
[disorder] / server / dump.c
CommitLineData
460b9539 1/*
2 * This file is part of DisOrder.
5aff007d 3 * Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
460b9539 4 *
e7eb3a27 5 * This program is free software: you can redistribute it and/or modify
460b9539 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
460b9539 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 *
460b9539 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/>.
460b9539 17 */
132a5a4a
RK
18/** @file server/dump.c
19 * @brief Dump and restore database contents
20 */
05b75f8d 21#include "disorder-server.h"
460b9539 22
23static const struct option options[] = {
24 { "help", no_argument, 0, 'h' },
25 { "version", no_argument, 0, 'V' },
26 { "config", required_argument, 0, 'c' },
27 { "dump", no_argument, 0, 'd' },
28 { "undump", no_argument, 0, 'u' },
29 { "debug", no_argument, 0, 'D' },
30 { "recover", no_argument, 0, 'r' },
31 { "recover-fatal", no_argument, 0, 'R' },
460b9539 32 { "recompute-aliases", no_argument, 0, 'a' },
33 { "remove-pathless", no_argument, 0, 'P' },
34 { 0, 0, 0, 0 }
35};
36
37/* display usage message and terminate */
38static void help(void) {
39 xprintf("Usage:\n"
40 " disorder-dump [OPTIONS] --dump|--undump PATH\n"
41 " disorder-dump [OPTIONS] --recompute-aliases\n"
42 "Options:\n"
43 " --help, -h Display usage message\n"
44 " --version, -V Display version number\n"
45 " --config PATH, -c PATH Set configuration file\n"
46 " --dump, -d Dump state to PATH\n"
47 " --undump, -u Restore state from PATH\n"
48 " --recover, -r Run database recovery\n"
49 " --recompute-aliases, -a Recompute aliases\n"
50 " --remove-pathless, -P Remove pathless tracks\n"
51 " --debug Debug mode\n");
52 xfclose(stdout);
53 exit(0);
54}
55
b3074bd1
RK
56/** @brief Dump one record
57 * @param s Output stream
58 * @param tag Tag for error messages
59 * @param letter Prefix leter for dumped record
60 * @param dbname Database name
61 * @param db Database handle
62 * @param tid Transaction handle
63 * @return 0 or @c DB_LOCK_DEADLOCK
64 */
65static int dump_one(struct sink *s,
66 const char *tag,
67 int letter,
68 const char *dbname,
69 DB *db,
70 DB_TXN *tid) {
71 int err;
72 DBC *cursor;
73 DBT k, d;
74
75 /* dump the preferences */
76 cursor = trackdb_opencursor(db, tid);
77 err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
78 DB_FIRST);
79 while(err == 0) {
80 if(sink_writec(s, letter) < 0
81 || urlencode(s, k.data, k.size)
82 || sink_writec(s, '\n') < 0
83 || urlencode(s, d.data, d.size)
84 || sink_writec(s, '\n') < 0)
2e9ba080 85 disorder_fatal(errno, "error writing to %s", tag);
b3074bd1
RK
86 err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
87 DB_NEXT);
88 }
89 switch(err) {
90 case DB_LOCK_DEADLOCK:
91 trackdb_closecursor(cursor);
92 return err;
93 case DB_NOTFOUND:
94 return trackdb_closecursor(cursor);
95 case 0:
96 assert(!"cannot happen");
97 default:
2e9ba080 98 disorder_fatal(0, "error reading %s: %s", dbname, db_strerror(err));
b3074bd1
RK
99 }
100}
101
102static struct {
103 int letter;
104 const char *dbname;
105 DB **db;
106} dbtable[] = {
107 { 'P', "prefs.db", &trackdb_prefsdb },
108 { 'G', "global.db", &trackdb_globaldb },
109 { 'U', "users.db", &trackdb_usersdb },
110 { 'W', "schedule.db", &trackdb_scheduledb },
111 { 'L', "playlists.db", &trackdb_playlistsdb },
112 /* avoid 'T' and 'S' for now */
113};
114#define NDBTABLE (sizeof dbtable / sizeof *dbtable)
115
460b9539 116/* dump prefs to FP, return nonzero on error */
b3074bd1 117static void do_dump(FILE *fp, const char *tag) {
460b9539 118 DB_TXN *tid;
119 struct sink *s = sink_stdio(tag, fp);
460b9539 120
121 for(;;) {
122 tid = trackdb_begin_transaction();
123 if(fseek(fp, 0, SEEK_SET) < 0)
2e9ba080 124 disorder_fatal(errno, "error calling fseek");
460b9539 125 if(fflush(fp) < 0)
2e9ba080 126 disorder_fatal(errno, "error calling fflush");
460b9539 127 if(ftruncate(fileno(fp), 0) < 0)
2e9ba080 128 disorder_fatal(errno, "error calling ftruncate");
b3074bd1 129 if(fprintf(fp, "V0") < 0)
2e9ba080 130 disorder_fatal(errno, "error writing to %s", tag);
b3074bd1
RK
131 for(size_t n = 0; n < NDBTABLE; ++n)
132 if(dump_one(s, tag,
133 dbtable[n].letter, dbtable[n].dbname, *dbtable[n].db,
134 tid))
135 goto fail;
f35e5800 136
b3074bd1 137 if(fputs("E\n", fp) < 0)
2e9ba080 138 disorder_fatal(errno, "error writing to %s", tag);
460b9539 139 break;
140fail:
2e9ba080 141 disorder_info("aborting transaction and retrying dump");
460b9539 142 trackdb_abort_transaction(tid);
143 }
144 trackdb_commit_transaction(tid);
2e9ba080 145 if(fflush(fp) < 0) disorder_fatal(errno, "error writing to %s", tag);
460b9539 146 /* caller might not be paranoid so we are paranoid on their behalf */
2e9ba080 147 if(fsync(fileno(fp)) < 0) disorder_fatal(errno, "error syncing %s", tag);
460b9539 148}
149
150/* delete all aliases prefs, return 0 or DB_LOCK_DEADLOCK */
151static int remove_aliases(DB_TXN *tid, int remove_pathless) {
152 DBC *cursor;
153 int err;
154 DBT k, d;
155 struct kvp *data;
156 int alias, pathless;
157
2e9ba080 158 disorder_info("removing aliases");
460b9539 159 cursor = trackdb_opencursor(trackdb_tracksdb, tid);
160 if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
161 DB_FIRST)) == DB_LOCK_DEADLOCK) {
2e9ba080 162 disorder_error(0, "cursor->c_get: %s", db_strerror(err));
460b9539 163 goto done;
164 }
165 while(err == 0) {
166 data = kvp_urldecode(d.data, d.size);
167 alias = !!kvp_get(data, "_alias_for");
168 pathless = !kvp_get(data, "_path");
169 if(pathless && !remove_pathless)
2e9ba080 170 disorder_info("no _path for %s", utf82mb(xstrndup(k.data, k.size)));
460b9539 171 if(alias || (remove_pathless && pathless)) {
172 switch(err = cursor->c_del(cursor, 0)) {
173 case 0: break;
174 case DB_LOCK_DEADLOCK:
2e9ba080 175 disorder_error(0, "cursor->c_get: %s", db_strerror(err));
460b9539 176 goto done;
177 default:
2e9ba080 178 disorder_fatal(0, "cursor->c_del: %s", db_strerror(err));
460b9539 179 }
180 }
181 err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), DB_NEXT);
182 }
183 if(err == DB_LOCK_DEADLOCK) {
2e9ba080 184 disorder_error(0, "cursor operation: %s", db_strerror(err));
460b9539 185 goto done;
186 }
2e9ba080
RK
187 if(err != DB_NOTFOUND)
188 disorder_fatal(0, "cursor->c_get: %s", db_strerror(err));
460b9539 189 err = 0;
190done:
191 if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
192 return err;
193}
194
195/* truncate (i.e. empty) a database, return 0 or DB_LOCK_DEADLOCK */
196static int truncdb(DB_TXN *tid, DB *db) {
197 int err;
198 u_int32_t count;
199
200 switch(err = db->truncate(db, tid, &count, 0)) {
201 case 0: break;
202 case DB_LOCK_DEADLOCK:
2e9ba080 203 disorder_error(0, "db->truncate: %s", db_strerror(err));
460b9539 204 break;
205 default:
2e9ba080 206 disorder_fatal(0, "db->truncate: %s", db_strerror(err));
460b9539 207 }
208 return err;
209}
210
211/* read a DBT from FP, return 0 on success or -1 on input error */
212static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
213 char *s;
214 struct dynstr d;
215
216 if(inputline(tag, fp, &s, '\n')) return -1;
217 dynstr_init(&d);
218 if(urldecode(sink_dynstr(&d), s, strlen(s)))
2e9ba080 219 disorder_fatal(0, "invalid URL-encoded data in %s", tag);
460b9539 220 dbt->data = d.vec;
221 dbt->size = d.nvec;
222 return 0;
223}
224
225/* undump from FP, return 0 or DB_LOCK_DEADLOCK */
226static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
227 int err, c;
460b9539 228
2e9ba080 229 disorder_info("undumping");
460b9539 230 if(fseek(fp, 0, SEEK_SET) < 0)
2e9ba080 231 disorder_fatal(errno, "error calling fseek on %s", tag);
460b9539 232 if((err = truncdb(tid, trackdb_prefsdb))) return err;
fb1bc1f5 233 if((err = truncdb(tid, trackdb_globaldb))) return err;
460b9539 234 if((err = truncdb(tid, trackdb_searchdb))) return err;
fb1bc1f5 235 if((err = truncdb(tid, trackdb_tagsdb))) return err;
a745dd43 236 if((err = truncdb(tid, trackdb_usersdb))) return err;
0e464e60 237 if((err = truncdb(tid, trackdb_scheduledb))) return err;
460b9539 238 c = getc(fp);
239 while(!ferror(fp) && !feof(fp)) {
b3074bd1
RK
240 for(size_t n = 0; n < NDBTABLE; ++n) {
241 if(dbtable[n].letter == c) {
242 DB *db = *dbtable[n].db;
243 const char *dbname = dbtable[n].dbname;
244 DBT k, d;
245
246 if(undump_dbt(fp, tag, prepare_data(&k))
247 || undump_dbt(fp, tag, prepare_data(&d)))
248 break;
249 switch(err = db->put(db, tid, &k, &d, 0)) {
250 case 0:
251 break;
252 case DB_LOCK_DEADLOCK:
2e9ba080 253 disorder_error(0, "error updating %s: %s", dbname, db_strerror(err));
b3074bd1
RK
254 return err;
255 default:
2e9ba080 256 disorder_fatal(0, "error updating %s: %s", dbname, db_strerror(err));
b3074bd1
RK
257 }
258 goto next;
259 }
260 }
261
460b9539 262 switch(c) {
263 case 'V':
264 c = getc(fp);
265 if(c != '0')
2e9ba080 266 disorder_fatal(0, "unknown version '%c'", c);
460b9539 267 break;
268 case 'E':
269 return 0;
460b9539 270 case '\n':
271 break;
b3074bd1
RK
272 default:
273 if(c >= 32 && c <= 126)
2e9ba080 274 disorder_fatal(0, "unexpected character '%c'", c);
b3074bd1 275 else
2e9ba080 276 disorder_fatal(0, "unexpected character 0x%02X", c);
460b9539 277 }
b3074bd1 278 next:
460b9539 279 c = getc(fp);
280 }
281 if(ferror(fp))
2e9ba080 282 disorder_fatal(errno, "error reading %s", tag);
460b9539 283 else
2e9ba080 284 disorder_fatal(0, "unexpected EOF reading %s", tag);
460b9539 285 return 0;
286}
287
288/* recompute aliases and search database from prefs, return 0 or
289 * DB_LOCK_DEADLOCK */
290static int recompute_aliases(DB_TXN *tid) {
291 DBC *cursor;
292 DBT k, d;
293 int err;
294 struct kvp *data;
295 const char *path, *track;
296
2e9ba080 297 disorder_info("recomputing aliases");
460b9539 298 cursor = trackdb_opencursor(trackdb_tracksdb, tid);
299 if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
300 DB_FIRST)) == DB_LOCK_DEADLOCK) goto done;
301 while(err == 0) {
302 data = kvp_urldecode(d.data, d.size);
303 track = xstrndup(k.data, k.size);
304 if(!kvp_get(data, "_alias_for")) {
305 if(!(path = kvp_get(data, "_path")))
2e9ba080 306 disorder_error(0, "%s is not an alias but has no path", utf82mb(track));
460b9539 307 else
308 if((err = trackdb_notice_tid(track, path, tid)) == DB_LOCK_DEADLOCK)
309 goto done;
310 }
311 err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
312 DB_NEXT);
313 }
314 switch(err) {
315 case 0:
316 break;
317 case DB_NOTFOUND:
318 err = 0;
319 break;
320 case DB_LOCK_DEADLOCK:
321 break;
322 default:
2e9ba080 323 disorder_fatal(0, "cursor->c_get: %s", db_strerror(err));
460b9539 324 }
325done:
326 if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
327 return err;
328}
329
330/* restore prefs from FP */
331static void do_undump(FILE *fp, const char *tag, int remove_pathless) {
332 DB_TXN *tid;
333
334 for(;;) {
335 tid = trackdb_begin_transaction();
336 if(remove_aliases(tid, remove_pathless)
337 || undump_from_fp(tid, fp, tag)
338 || recompute_aliases(tid)) goto fail;
339 break;
340fail:
2e9ba080 341 disorder_info("aborting transaction and retrying undump");
460b9539 342 trackdb_abort_transaction(tid);
343 }
2e9ba080 344 disorder_info("committing undump");
460b9539 345 trackdb_commit_transaction(tid);
346}
347
348/* just recompute alisaes */
349static void do_recompute(int remove_pathless) {
350 DB_TXN *tid;
351
352 for(;;) {
353 tid = trackdb_begin_transaction();
354 if(remove_aliases(tid, remove_pathless)
355 || recompute_aliases(tid)) goto fail;
356 break;
357fail:
2e9ba080 358 disorder_info("aborting transaction and retrying recomputation");
460b9539 359 trackdb_abort_transaction(tid);
360 }
2e9ba080 361 disorder_info("committing recomputed aliases");
460b9539 362 trackdb_commit_transaction(tid);
363}
364
365int main(int argc, char **argv) {
366 int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
b3074bd1 367 int remove_pathless = 0, fd;
460b9539 368 const char *path;
369 char *tmp;
370 FILE *fp;
371
320598d4 372 mem_init();
b3074bd1 373 while((n = getopt_long(argc, argv, "hVc:dDurRaP", options, 0)) >= 0) {
460b9539 374 switch(n) {
375 case 'h': help();
3fbdc96d 376 case 'V': version("disorder-dump");
460b9539 377 case 'c': configfile = optarg; break;
378 case 'd': dump = 1; break;
379 case 'u': undump = 1; break;
380 case 'D': debugging = 1; break;
375d9478
RK
381 case 'r': recover = TRACKDB_NORMAL_RECOVER; break;
382 case 'R': recover = TRACKDB_FATAL_RECOVER; break;
460b9539 383 case 'a': recompute = 1; break;
384 case 'P': remove_pathless = 1; break;
2e9ba080 385 default: disorder_fatal(0, "invalid option");
460b9539 386 }
387 }
388 if(dump + undump + recompute != 1)
2e9ba080 389 disorder_fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
460b9539 390 if(recompute) {
391 if(optind != argc)
2e9ba080 392 disorder_fatal(0, "--recompute-aliases does not take a filename");
460b9539 393 path = 0;
394 } else {
395 if(optind >= argc)
2e9ba080 396 disorder_fatal(0, "missing dump file name");
460b9539 397 if(optind + 1 < argc)
2e9ba080 398 disorder_fatal(0, "specify only a dump file name");
460b9539 399 path = argv[optind];
400 }
2e9ba080
RK
401 if(config_read(0, NULL))
402 disorder_fatal(0, "cannot read configuration");
a745dd43 403 trackdb_init(recover|TRACKDB_MAY_CREATE);
d25c4615 404 trackdb_open(TRACKDB_NO_UPGRADE);
460b9539 405 if(dump) {
a745dd43
RK
406 /* We write to a temporary file and rename into place. We make
407 * sure the permissions are tight from the start. */
460b9539 408 byte_xasprintf(&tmp, "%s.%lx.tmp", path, (unsigned long)getpid());
a745dd43 409 if((fd = open(tmp, O_CREAT|O_TRUNC|O_WRONLY, 0600)) < 0)
2e9ba080 410 disorder_fatal(errno, "error opening %s", tmp);
a745dd43 411 if(!(fp = fdopen(fd, "w")))
2e9ba080 412 disorder_fatal(errno, "fdopen on %s", tmp);
b3074bd1 413 do_dump(fp, tmp);
2e9ba080 414 if(fclose(fp) < 0) disorder_fatal(errno, "error closing %s", tmp);
460b9539 415 if(rename(tmp, path) < 0)
2e9ba080 416 disorder_fatal(errno, "error renaming %s to %s", tmp, path);
460b9539 417 } else if(undump) {
418 /* the databases or logfiles might end up with wrong permissions
419 * if new ones are created */
2e9ba080
RK
420 if(getuid() == 0)
421 disorder_info("you might need to chown database files");
422 if(!(fp = fopen(path, "r")))
423 disorder_fatal(errno, "error opening %s", path);
460b9539 424 do_undump(fp, path, remove_pathless);
425 xfclose(fp);
426 } else if(recompute) {
427 do_recompute(remove_pathless);
428 }
429 trackdb_close();
8cd7d4bc 430 trackdb_deinit(NULL);
460b9539 431 return 0;
432}
433
434/*
435Local Variables:
436c-basic-offset:2
437comment-column:40
438End:
439*/