chiark / gitweb /
af1d43d13294cd502f15cc8d23441f7c836f18af
[disorder] / server / dump.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2004, 2005, 2007, 2008 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 3 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,
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  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 #include "disorder-server.h"
20
21 static const struct option options[] = {
22   { "help", no_argument, 0, 'h' },
23   { "version", no_argument, 0, 'V' },
24   { "config", required_argument, 0, 'c' },
25   { "dump", no_argument, 0, 'd' },
26   { "undump", no_argument, 0, 'u' },
27   { "debug", no_argument, 0, 'D' },
28   { "recover", no_argument, 0, 'r' },
29   { "recover-fatal", no_argument, 0, 'R' },
30   { "trackdb", no_argument, 0, 't' },
31   { "searchdb", no_argument, 0, 's' },
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 */
38 static 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
56 /* dump prefs to FP, return nonzero on error */
57 static void do_dump(FILE *fp, const char *tag,
58                     int tracksdb, int searchdb) {
59   DBC *cursor = 0;
60   DB_TXN *tid;
61   struct sink *s = sink_stdio(tag, fp);
62   int err;
63   DBT k, d;
64
65   for(;;) {
66     tid = trackdb_begin_transaction();
67     if(fseek(fp, 0, SEEK_SET) < 0)
68       fatal(errno, "error calling fseek");
69     if(fflush(fp) < 0)
70       fatal(errno, "error calling fflush");
71     if(ftruncate(fileno(fp), 0) < 0)
72       fatal(errno, "error calling ftruncate");
73     if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
74       fatal(errno, "error writing to %s", tag);
75     /* dump the preferences */
76     cursor = trackdb_opencursor(trackdb_prefsdb, tid);
77     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
78                         DB_FIRST);
79     while(err == 0) {
80       if(fputc('P', fp) < 0
81          || urlencode(s, k.data, k.size)
82          || fputc('\n', fp) < 0
83          || urlencode(s, d.data, d.size)
84          || fputc('\n', fp) < 0)
85         fatal(errno, "error writing to %s", tag);
86       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
87                           DB_NEXT);
88     }
89     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
90     cursor = 0;
91
92     /* dump the global preferences */
93     cursor = trackdb_opencursor(trackdb_globaldb, tid);
94     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
95                         DB_FIRST);
96     while(err == 0) {
97       if(fputc('G', fp) < 0
98          || urlencode(s, k.data, k.size)
99          || fputc('\n', fp) < 0
100          || urlencode(s, d.data, d.size)
101          || fputc('\n', fp) < 0)
102         fatal(errno, "error writing to %s", tag);
103       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
104                           DB_NEXT);
105     }
106     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
107     cursor = 0;
108     
109     /* dump the users */
110     cursor = trackdb_opencursor(trackdb_usersdb, tid);
111     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
112                         DB_FIRST);
113     while(err == 0) {
114       if(fputc('U', fp) < 0
115          || urlencode(s, k.data, k.size)
116          || fputc('\n', fp) < 0
117          || urlencode(s, d.data, d.size)
118          || fputc('\n', fp) < 0)
119         fatal(errno, "error writing to %s", tag);
120       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
121                           DB_NEXT);
122     }
123     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
124     cursor = 0;
125
126     /* dump the schedule */
127     cursor = trackdb_opencursor(trackdb_scheduledb, tid);
128     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
129                         DB_FIRST);
130     while(err == 0) {
131       if(fputc('W', fp) < 0
132          || urlencode(s, k.data, k.size)
133          || fputc('\n', fp) < 0
134          || urlencode(s, d.data, d.size)
135          || fputc('\n', fp) < 0)
136         fatal(errno, "error writing to %s", tag);
137       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
138                           DB_NEXT);
139     }
140     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
141     cursor = 0;
142     
143     
144     if(tracksdb) {
145       cursor = trackdb_opencursor(trackdb_tracksdb, tid);
146       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
147                           DB_FIRST);
148       while(err == 0) {
149         if(fputc('T', fp) < 0
150            || urlencode(s, k.data, k.size)
151            || fputc('\n', fp) < 0
152            || urlencode(s, d.data, d.size)
153            || fputc('\n', fp) < 0)
154           fatal(errno, "error writing to %s", tag);
155         err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
156                             DB_NEXT);
157       }
158       if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
159       cursor = 0;
160     }
161
162     if(searchdb) {
163       cursor = trackdb_opencursor(trackdb_searchdb, tid);
164       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
165                           DB_FIRST);
166       while(err == 0) {
167         if(fputc('S', fp) < 0
168            || urlencode(s, k.data, k.size)
169            || fputc('\n', fp) < 0
170            || urlencode(s, d.data, d.size)
171            || fputc('\n', fp) < 0)
172           fatal(errno, "error writing to %s", tag);
173         err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
174                             DB_NEXT);
175       }
176       if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }      cursor = 0;
177     }
178
179     if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag);
180     if(err == DB_LOCK_DEADLOCK) {
181       error(0, "c->c_get: %s", db_strerror(err));
182       goto fail;
183     }
184     if(err && err != DB_NOTFOUND)
185       fatal(0, "cursor->c_get: %s", db_strerror(err));
186     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
187     break;
188 fail:
189     trackdb_closecursor(cursor);
190     cursor = 0;
191     info("aborting transaction and retrying dump");
192     trackdb_abort_transaction(tid);
193   }
194   trackdb_commit_transaction(tid);
195   if(fflush(fp) < 0) fatal(errno, "error writing to %s", tag);
196   /* caller might not be paranoid so we are paranoid on their behalf */
197   if(fsync(fileno(fp)) < 0) fatal(errno, "error syncing %s", tag);
198 }
199
200 /* delete all aliases prefs, return 0 or DB_LOCK_DEADLOCK */
201 static int remove_aliases(DB_TXN *tid, int remove_pathless) {
202   DBC *cursor;
203   int err;
204   DBT k, d;
205   struct kvp *data;
206   int alias, pathless;
207
208   info("removing aliases");
209   cursor = trackdb_opencursor(trackdb_tracksdb, tid);
210   if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
211                           DB_FIRST)) == DB_LOCK_DEADLOCK) {
212     error(0, "cursor->c_get: %s", db_strerror(err));
213     goto done;
214   }
215   while(err == 0) {
216     data = kvp_urldecode(d.data, d.size);
217     alias = !!kvp_get(data, "_alias_for");
218     pathless = !kvp_get(data, "_path");
219     if(pathless && !remove_pathless)
220       info("no _path for %s", utf82mb(xstrndup(k.data, k.size)));
221     if(alias || (remove_pathless && pathless)) {
222       switch(err = cursor->c_del(cursor, 0)) {
223       case 0: break;
224       case DB_LOCK_DEADLOCK:
225         error(0, "cursor->c_get: %s", db_strerror(err));
226         goto done;
227       default:
228         fatal(0, "cursor->c_del: %s", db_strerror(err));
229       }
230     }
231     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), DB_NEXT);
232   }
233   if(err == DB_LOCK_DEADLOCK) {
234     error(0, "cursor operation: %s", db_strerror(err));
235     goto done;
236   }
237   if(err != DB_NOTFOUND) fatal(0, "cursor->c_get: %s", db_strerror(err));
238   err = 0;
239 done:
240   if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
241   return err;
242 }
243
244 /* truncate (i.e. empty) a database, return 0 or DB_LOCK_DEADLOCK */
245 static int truncdb(DB_TXN *tid, DB *db) {
246   int err;
247   u_int32_t count;
248
249   switch(err = db->truncate(db, tid, &count, 0)) {
250   case 0: break;
251   case DB_LOCK_DEADLOCK:
252     error(0, "db->truncate: %s", db_strerror(err));
253     break;
254   default:
255     fatal(0, "db->truncate: %s", db_strerror(err));
256   }
257   return err;
258 }
259
260 /* read a DBT from FP, return 0 on success or -1 on input error */
261 static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
262   char *s;
263   struct dynstr d;
264
265   if(inputline(tag, fp, &s, '\n')) return -1;
266   dynstr_init(&d);
267   if(urldecode(sink_dynstr(&d), s, strlen(s)))
268     fatal(0, "invalid URL-encoded data in %s", tag);
269   dbt->data = d.vec;
270   dbt->size = d.nvec;
271   return 0;
272 }
273
274 /* undump from FP, return 0 or DB_LOCK_DEADLOCK */
275 static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
276   int err, c;
277   DBT k, d;
278   const char *which_name;
279   DB *which_db;
280
281   info("undumping");
282   if(fseek(fp, 0, SEEK_SET) < 0)
283     fatal(errno, "error calling fseek on %s", tag);
284   if((err = truncdb(tid, trackdb_prefsdb))) return err;
285   if((err = truncdb(tid, trackdb_globaldb))) return err;
286   if((err = truncdb(tid, trackdb_searchdb))) return err;
287   if((err = truncdb(tid, trackdb_tagsdb))) return err;
288   if((err = truncdb(tid, trackdb_usersdb))) return err;
289   if((err = truncdb(tid, trackdb_scheduledb))) return err;
290   c = getc(fp);
291   while(!ferror(fp) && !feof(fp)) {
292     switch(c) {
293     case 'V':
294       c = getc(fp);
295       if(c != '0')
296         fatal(0, "unknown version '%c'", c);
297       break;
298     case 'E':
299       return 0;
300     case 'P':
301     case 'G':
302     case 'U':
303     case 'W':
304       switch(c) {
305       case 'P':
306         which_db = trackdb_prefsdb;
307         which_name = "prefs.db";
308         break;
309       case 'G':
310         which_db = trackdb_globaldb;
311         which_name = "global.db";
312         break;
313       case 'U':
314         which_db = trackdb_usersdb;
315         which_name = "users.db";
316         break;
317       case 'W':                         /* for 'when' */
318         which_db = trackdb_scheduledb;
319         which_name = "scheduledb.db";
320         break;
321       default:
322         abort();
323       }
324       if(undump_dbt(fp, tag, prepare_data(&k))
325          || undump_dbt(fp, tag, prepare_data(&d)))
326         break;
327       switch(err = which_db->put(which_db, tid, &k, &d, 0)) {
328       case 0:
329         break;
330       case DB_LOCK_DEADLOCK:
331         error(0, "error updating %s: %s", which_name, db_strerror(err));
332         return err;
333       default:
334         fatal(0, "error updating %s: %s", which_name, db_strerror(err));
335       }
336       break;
337     case 'T':
338     case 'S':
339       if(undump_dbt(fp, tag, prepare_data(&k))
340          || undump_dbt(fp, tag, prepare_data(&d)))
341         break;
342       /* We don't restore the tracks.db or search.db entries, instead
343        * we recompute them */
344       break;
345     case '\n':
346       break;
347     }
348     c = getc(fp);
349   }
350   if(ferror(fp))
351     fatal(errno, "error reading %s", tag);
352   else
353     fatal(0, "unexpected EOF reading %s", tag);
354   return 0;
355 }
356
357 /* recompute aliases and search database from prefs, return 0 or
358  * DB_LOCK_DEADLOCK */
359 static int recompute_aliases(DB_TXN *tid) {
360   DBC *cursor;
361   DBT k, d;
362   int err;
363   struct kvp *data;
364   const char *path, *track;
365
366   info("recomputing aliases");
367   cursor = trackdb_opencursor(trackdb_tracksdb, tid);
368   if((err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
369                           DB_FIRST)) == DB_LOCK_DEADLOCK) goto done;
370   while(err == 0) {
371     data = kvp_urldecode(d.data, d.size);
372     track = xstrndup(k.data, k.size);
373     if(!kvp_get(data, "_alias_for")) {
374       if(!(path = kvp_get(data, "_path")))
375         error(0, "%s is not an alias but has no path", utf82mb(track));
376       else
377         if((err = trackdb_notice_tid(track, path, tid)) == DB_LOCK_DEADLOCK)
378           goto done;
379     }
380     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
381                         DB_NEXT);
382   }
383   switch(err) {
384   case 0:
385     break;
386   case DB_NOTFOUND:
387     err = 0;
388     break;
389   case DB_LOCK_DEADLOCK:
390     break;
391   default:
392     fatal(0, "cursor->c_get: %s", db_strerror(err));
393   }
394 done:
395   if(trackdb_closecursor(cursor) && !err) err = DB_LOCK_DEADLOCK;
396   return err;
397 }
398
399 /* restore prefs from FP */
400 static void do_undump(FILE *fp, const char *tag, int remove_pathless) {
401   DB_TXN *tid;
402
403   for(;;) {
404     tid = trackdb_begin_transaction();
405     if(remove_aliases(tid, remove_pathless)
406        || undump_from_fp(tid, fp, tag)
407        || recompute_aliases(tid)) goto fail;
408     break;
409 fail:
410     info("aborting transaction and retrying undump");
411     trackdb_abort_transaction(tid);
412   }
413   info("committing undump");
414   trackdb_commit_transaction(tid);
415 }
416
417 /* just recompute alisaes */
418 static void do_recompute(int remove_pathless) {
419   DB_TXN *tid;
420
421   for(;;) {
422     tid = trackdb_begin_transaction();
423     if(remove_aliases(tid, remove_pathless)
424        || recompute_aliases(tid)) goto fail;
425     break;
426 fail:
427     info("aborting transaction and retrying recomputation");
428     trackdb_abort_transaction(tid);
429   }
430   info("committing recomputed aliases");
431   trackdb_commit_transaction(tid);
432 }
433
434 int main(int argc, char **argv) {
435   int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
436   int tracksdb = 0, searchdb = 0, remove_pathless = 0, fd;
437   const char *path;
438   char *tmp;
439   FILE *fp;
440
441   mem_init();
442   while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
443     switch(n) {
444     case 'h': help();
445     case 'V': version("disorder-dump");
446     case 'c': configfile = optarg; break;
447     case 'd': dump = 1; break;
448     case 'u': undump = 1; break;
449     case 'D': debugging = 1; break;
450     case 't': tracksdb = 1; break;
451     case 's': searchdb = 1; break;
452     case 'r': recover = TRACKDB_NORMAL_RECOVER;
453     case 'R': recover = TRACKDB_FATAL_RECOVER;
454     case 'a': recompute = 1; break;
455     case 'P': remove_pathless = 1; break;
456     default: fatal(0, "invalid option");
457     }
458   }
459   if(dump + undump + recompute != 1)
460     fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
461   if((undump || recompute) && (tracksdb || searchdb))
462     fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases");
463   if(recompute) {
464     if(optind != argc)
465       fatal(0, "--recompute-aliases does not take a filename");
466     path = 0;
467   } else {
468     if(optind >= argc)
469       fatal(0, "missing dump file name");
470     if(optind + 1 < argc)
471       fatal(0, "specify only a dump file name");
472     path = argv[optind];
473   }
474   if(config_read(0)) fatal(0, "cannot read configuration");
475   trackdb_init(recover|TRACKDB_MAY_CREATE);
476   trackdb_open(TRACKDB_NO_UPGRADE);
477   if(dump) {
478     /* We write to a temporary file and rename into place.  We make
479      * sure the permissions are tight from the start. */
480     byte_xasprintf(&tmp, "%s.%lx.tmp", path, (unsigned long)getpid());
481     if((fd = open(tmp, O_CREAT|O_TRUNC|O_WRONLY, 0600)) < 0)
482       fatal(errno, "error opening %s", tmp);
483     if(!(fp = fdopen(fd, "w")))
484       fatal(errno, "fdopen on %s", tmp);
485     do_dump(fp, tmp, tracksdb, searchdb);
486     if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
487     if(rename(tmp, path) < 0)
488       fatal(errno, "error renaming %s to %s", tmp, path);
489   } else if(undump) {
490     /* the databases or logfiles might end up with wrong permissions
491      * if new ones are created */
492     if(getuid() == 0) info("you might need to chown database files");
493     if(!(fp = fopen(path, "r"))) fatal(errno, "error opening %s", path);
494     do_undump(fp, path, remove_pathless);
495     xfclose(fp);
496   } else if(recompute) {
497     do_recompute(remove_pathless);
498   }
499   trackdb_close();
500   trackdb_deinit();
501   return 0;
502 }
503
504 /*
505 Local Variables:
506 c-basic-offset:2
507 comment-column:40
508 End:
509 */