1 /* ctl_backups.c -- tool for managing replication-based backup files
2  *
3  * Copyright (c) 1994-2015 Carnegie Mellon University.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in
14  *    the documentation and/or other materials provided with the
15  *    distribution.
16  *
17  * 3. The name "Carnegie Mellon University" must not be used to
18  *    endorse or promote products derived from this software without
19  *    prior written permission. For permission or any legal
20  *    details, please contact
21  *      Carnegie Mellon University
22  *      Center for Technology Transfer and Enterprise Creation
23  *      4615 Forbes Avenue
24  *      Suite 302
25  *      Pittsburgh, PA  15213
26  *      (412) 268-7393, fax: (412) 268-7395
27  *      innovation@andrew.cmu.edu
28  *
29  * 4. Redistributions of any form whatsoever must retain the following
30  *    acknowledgment:
31  *    "This product includes software developed by Computing Services
32  *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33  *
34  * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35  * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36  * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37  * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41  *
42  */
43 
44 #include <config.h>
45 
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 #include <sys/wait.h>
49 
50 #include <assert.h>
51 #include <errno.h>
52 #include <jansson.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <syslog.h>
56 #include <sysexits.h>
57 #include <unistd.h>
58 
59 #include "lib/cyrusdb.h"
60 #include "lib/util.h"
61 #include "lib/xmalloc.h"
62 
63 #include "imap/global.h"
64 #include "imap/imap_err.h"
65 
66 #include "backup/backup.h"
67 
68 static struct namespace ctl_backups_namespace;
69 
fatal(const char * error,int code)70 EXPORTED void fatal(const char *error, int code)
71 {
72     fprintf(stderr, "fatal error: %s\n", error);
73     cyrus_done();
74     exit(code);
75 }
76 
77 static const char *argv0 = NULL;
usage(void)78 static void usage(void)
79 {
80     fprintf(stderr, "Usage:\n");
81     fprintf(stderr, "    %s [options] compact [mode] backup...\n", argv0);
82     fprintf(stderr, "    %s [options] list [list_opts] [[mode] backup...]\n", argv0);
83     fprintf(stderr, "    %s [options] lock [lock_opts] [mode] backup\n", argv0);
84     fprintf(stderr, "    %s [options] reindex [mode] backup...\n", argv0);
85     fprintf(stderr, "    %s [options] stat [mode] backup...\n", argv0);
86     fprintf(stderr, "    %s [options] verify [mode] backup...\n", argv0);
87 
88     fprintf(stderr, "\n%s\n",
89             "Commands:\n"
90             "    compact             # compact specified backups\n"
91             "    list [list_opts]    # list backups (all if none specified)\n"
92             "    lock [lock_opts]    # lock specified backup\n"
93             "    reindex             # reindex specified backups\n"
94             "    stat                # show stats of specified backups\n"
95             "    verify              # verify specified backups\n"
96     );
97 
98     fprintf(stderr, "%s\n",
99             "Options:\n"
100             "    -C alt_config       # alternate config file\n"
101             "    -F                  # force (run command even if not needed)\n"
102             "    -S                  # stop on error\n"
103             "    -V                  # don't verify checksums (faster read-only ops)\n"
104             "    -j                  # output in JSON format\n"
105             "    -v                  # verbose (repeat for more verbosity)\n"
106             "    -w                  # wait for locks (don't skip locked backups)\n"
107     );
108 
109     fprintf(stderr, "%s\n",
110             "List options:\n"
111             "    -t [hours]          # stale (no update in hours) backups only (default: 24)\n"
112     );
113 
114     fprintf(stderr, "%s\n",
115             "Lock options:\n"
116             "    -c                  # exclusively create backup\n"
117             "    -p                  # lock backup and wait for eof on stdin (default)\n"
118             "    -s                  # lock backup and open index in sqlite3\n"
119             "    -x command          # lock backup and execute command\n"
120     );
121 
122     fprintf(stderr, "%s\n",
123             "Modes:\n"
124             "    -A                  # all known backups\n"
125             "    -D                  # specified backups interpreted as domains\n"
126             "    -P                  # specified backups interpreted as userid prefixes\n"
127             "    -f                  # specified backups interpreted as filenames\n"
128             "    -m                  # specified backups interpreted as mboxnames\n"
129             "    -u                  # specified backups interpreted as userids (default)\n"
130             "\n"
131             "    Modes -A, -D, -P not available for all commands\n" /* FIXME which */
132     );
133 
134     exit(EX_USAGE);
135 }
136 
137 enum ctlbu_mode {
138     CTLBU_MODE_UNSPECIFIED = 0,
139     CTLBU_MODE_FILENAME,
140     CTLBU_MODE_MBOXNAME,
141     CTLBU_MODE_USERNAME,
142     CTLBU_MODE_DOMAIN,
143     CTLBU_MODE_PREFIX,
144     CTLBU_MODE_ALL,
145 };
146 
147 enum ctlbu_lock_mode {
148     CTLBU_LOCK_MODE_UNSPECIFIED = 0,
149     CTLBU_LOCK_MODE_PIPE,
150     CTLBU_LOCK_MODE_SQL,
151     CTLBU_LOCK_MODE_EXEC,
152 };
153 
154 struct ctlbu_cmd_options {
155     enum ctlbu_mode mode;
156     enum ctlbu_lock_mode lock_mode;
157     enum backup_open_nonblock wait;
158     enum backup_open_create create;
159     int verbose;
160     int stop_on_error;
161     int list_stale;
162     int force;
163     int noverify;
164     int jsonout;
165     const char *lock_exec_cmd;
166     const char *domain;
167 };
168 
169 enum ctlbu_cmd {
170     CTLBU_CMD_UNSPECIFIED = 0,
171     CTLBU_CMD_COMPACT,
172     CTLBU_CMD_DELETE,
173     CTLBU_CMD_LIST,
174     CTLBU_CMD_LOCK,
175     CTLBU_CMD_MOVE,
176     CTLBU_CMD_RECONSTRUCT,
177     CTLBU_CMD_REINDEX,
178     CTLBU_CMD_STAT,
179     CTLBU_CMD_VERIFY,
180 };
181 
182 static int ctlbu_skips_fails = 0;
183 
184 /* same signature as foreach_cb */
185 static int cmd_compact_one(void *rock,
186                            const char *userid, size_t userid_len,
187                            const char *fname, size_t fname_len);
188 static int cmd_delete_one(void *rock,
189                           const char *userid, size_t userid_len,
190                           const char *fname, size_t fname_len);
191 static int cmd_list_one(void *rock,
192                         const char *userid, size_t userid_len,
193                         const char *fname, size_t fname_len);
194 static int cmd_lock_one(void *rock,
195                         const char *userid, size_t userid_len,
196                         const char *fname, size_t fname_len);
197 static int cmd_move_one(void *rock,
198                         const char *userid, size_t userid_len,
199                         const char *fname, size_t fname_len);
200 static int cmd_reindex_one(void *rock,
201                            const char *userid, size_t userid_len,
202                            const char *fname, size_t fname_len);
203 static int cmd_stat_one(void *rock,
204                         const char *userid, size_t userid_len,
205                         const char *fname, size_t fname_len);
206 static int cmd_verify_one(void *rock,
207                           const char *userid, size_t userid_len,
208                           const char *fname, size_t fname_len);
209 
210 static foreach_cb *const cmd_func[] = {
211     NULL,
212     cmd_compact_one,
213     cmd_delete_one,
214     cmd_list_one,
215     cmd_lock_one,
216     cmd_move_one,
217     NULL, /* reconstruct one doesn't make sense */
218     cmd_reindex_one,
219     cmd_stat_one,
220     cmd_verify_one,
221 };
222 
223 static int lock_run_pipe(const char *userid, const char *fname,
224                          enum backup_open_nonblock nonblock,
225                          enum backup_open_create create);
226 static int lock_run_sqlite(const char *userid, const char *fname,
227                            enum backup_open_nonblock nonblock,
228                            enum backup_open_create create);
229 static int lock_run_exec(const char *userid, const char *fname,
230                          const char *cmd,
231                          enum backup_open_nonblock nonblock,
232                          enum backup_open_create create);
233 
parse_cmd_string(const char * cmd)234 static enum ctlbu_cmd parse_cmd_string(const char *cmd)
235 {
236     assert(cmd != NULL);
237 
238     switch(cmd[0]) {
239     case 'c':
240         if (strcmp(cmd, "compact") == 0) return CTLBU_CMD_COMPACT;
241         break;
242     case 'd':
243         if (strcmp(cmd, "delete") == 0) return CTLBU_CMD_DELETE;
244         break;
245     case 'l':
246         if (strcmp(cmd, "list") == 0) return CTLBU_CMD_LIST;
247         if (strcmp(cmd, "lock") == 0) return CTLBU_CMD_LOCK;
248         break;
249     case 'm':
250         if (strcmp(cmd, "move") == 0) return CTLBU_CMD_MOVE;
251         break;
252     case 'r':
253         if (strcmp(cmd, "reconstruct") == 0) return CTLBU_CMD_RECONSTRUCT;
254         if (strcmp(cmd, "reindex") == 0) return CTLBU_CMD_REINDEX;
255         break;
256     case 's':
257         if (strcmp(cmd, "stat") == 0) return CTLBU_CMD_STAT;
258         break;
259     case 'v':
260         if (strcmp(cmd, "verify") == 0) return CTLBU_CMD_VERIFY;
261         break;
262     };
263 
264     return CTLBU_CMD_UNSPECIFIED;
265 }
266 
print_status(const char * cmd,const char * userid,const char * fname,const struct ctlbu_cmd_options * options,int r)267 static void print_status(const char *cmd,
268                          const char *userid, const char *fname,
269                          const struct ctlbu_cmd_options *options,
270                          int r)
271 {
272     char *status = NULL;
273     switch (r) {
274         case 1:
275             status = xstrdup("skipped");
276             break;
277         case 0:
278             status = xstrdup("ok");
279             break;
280         case IMAP_MAILBOX_LOCKED:
281             status = xstrdup("locked");
282             break;
283         default:
284             status = strconcat("failed (", error_message(r), ")", NULL);
285             break;
286     }
287 
288     if (options->jsonout) {
289         json_t *out = json_object();
290         json_object_set_new(out, "command", json_string(cmd));
291         json_object_set_new(out, "userid", json_string(userid));
292         json_object_set_new(out, "fname", json_string(fname));
293         json_object_set_new(out, "status", json_string(status));
294 
295         const size_t flags = JSON_INDENT(2) | JSON_PRESERVE_ORDER;
296         json_dumpf(out, stdout, flags);
297         puts("");
298         json_decref(out);
299     }
300     else {
301         printf("%s %s: %s\n", cmd, userid ? userid : fname, status);
302     }
303 
304     free(status);
305 }
306 
domain_filter(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)307 static int domain_filter(void *rock,
308                          const char *key, size_t key_len,
309                          const char *data __attribute__((unused)),
310                          size_t data_len __attribute__((unused)))
311 {
312     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
313     char *userid = NULL;
314     mbname_t *mbname = NULL;
315     const char *domain = NULL;
316     int doit = 0;
317 
318     /* input args might not be 0-terminated, so make a safe copy */
319     if (!key_len) return 0;
320     userid = xstrndup(key, key_len);
321 
322     mbname = mbname_from_userid(userid);
323     domain = mbname_domain(mbname);
324     if (!domain)
325         domain = config_defdomain;
326 
327     if (domain)
328         doit = !strcmp(domain, options->domain);
329 
330     mbname_free(&mbname);
331     free(userid);
332 
333     return doit;
334 }
335 
save_argv0(const char * s)336 static void save_argv0(const char *s)
337 {
338     const char *slash = strrchr(s, '/');
339     if (slash)
340         argv0 = slash + 1;
341     else
342         argv0 = s;
343 }
344 
main(int argc,char ** argv)345 int main(int argc, char **argv)
346 {
347     save_argv0(argv[0]);
348 
349     int opt, r = 0;
350     const char *alt_config = NULL;
351     enum ctlbu_cmd cmd = CTLBU_CMD_UNSPECIFIED;
352     struct ctlbu_cmd_options options = {0};
353     options.wait = BACKUP_OPEN_NONBLOCK;
354 
355     while ((opt = getopt(argc, argv, ":AC:DFPSVcfjmpst:x:uvw")) != EOF) {
356         switch (opt) {
357         case 'A':
358             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
359             options.mode = CTLBU_MODE_ALL;
360             break;
361         case 'C':
362             alt_config = optarg;
363             break;
364         case 'D':
365             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
366             options.mode = CTLBU_MODE_DOMAIN;
367             break;
368         case 'F':
369             options.force = 1;
370             break;
371         case 'P':
372             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
373             options.mode = CTLBU_MODE_PREFIX;
374             break;
375         case 'S':
376             options.stop_on_error = 1;
377             break;
378         case 'V':
379             options.noverify = 1;
380             break;
381         case 'c':
382             options.create = BACKUP_OPEN_CREATE_EXCL;
383             break;
384         case 'j':
385             if (options.verbose) usage();
386             options.jsonout = 1;
387             break;
388         case 'f':
389             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
390             options.mode = CTLBU_MODE_FILENAME;
391             break;
392         case 'm':
393             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
394             options.mode = CTLBU_MODE_MBOXNAME;
395             break;
396         case 'p':
397             if (options.lock_mode != CTLBU_LOCK_MODE_UNSPECIFIED) usage();
398             options.lock_mode = CTLBU_LOCK_MODE_PIPE;
399             break;
400         case 's':
401             if (options.lock_mode != CTLBU_LOCK_MODE_UNSPECIFIED) usage();
402             options.lock_mode = CTLBU_LOCK_MODE_SQL;
403             break;
404         case 't':
405             options.list_stale = atoi(optarg);
406             if (!options.list_stale) usage();
407             break;
408         case 'u':
409             if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
410             options.mode = CTLBU_MODE_USERNAME;
411             break;
412         case 'v':
413             if (options.jsonout) usage();
414             options.verbose ++;
415             break;
416         case 'x':
417             if (options.lock_mode != CTLBU_LOCK_MODE_UNSPECIFIED) usage();
418             options.lock_mode = CTLBU_LOCK_MODE_EXEC;
419             options.lock_exec_cmd = optarg;
420             break;
421         case 'w':
422             options.wait = BACKUP_OPEN_BLOCK;
423             break;
424         case ':':
425             if (optopt == 't') options.list_stale = 24;
426             else usage();
427             break;
428         default:
429             usage();
430             break;
431         }
432     }
433 
434     /* get the command */
435     if (optind == argc) usage();
436     cmd = parse_cmd_string(argv[optind++]);
437     if (cmd == CTLBU_CMD_UNSPECIFIED) usage();
438 
439     if (options.lock_mode != CTLBU_LOCK_MODE_UNSPECIFIED
440         && cmd != CTLBU_CMD_LOCK)
441         usage();
442 
443     switch (cmd) {
444     /* list defaults to all */
445     case CTLBU_CMD_LIST:
446         if (options.mode == CTLBU_MODE_UNSPECIFIED && argc - optind == 0)
447             options.mode = CTLBU_MODE_ALL;
448         break;
449 
450     /* some commands only accept one backup at a time */
451     case CTLBU_CMD_LOCK:
452     case CTLBU_CMD_MOVE:
453     case CTLBU_CMD_DELETE:
454         if (options.mode == CTLBU_MODE_ALL) usage();
455         if (options.mode == CTLBU_MODE_DOMAIN) usage();
456         if (options.mode == CTLBU_MODE_PREFIX) usage();
457         if (argc - optind > 1) usage();
458         break;
459 
460     /* reconstruct doesn't accept named backups */
461     case CTLBU_CMD_RECONSTRUCT:
462         if (options.mode != CTLBU_MODE_UNSPECIFIED) usage();
463         if (optind != argc) usage();
464         break;
465 
466     default:
467         break;
468     }
469 
470     /* default mode is username */
471     if (options.mode == CTLBU_MODE_UNSPECIFIED)
472         options.mode = CTLBU_MODE_USERNAME;
473 
474     /* mode all doesn't want any named backups */
475     if (options.mode == CTLBU_MODE_ALL && optind != argc) usage();
476 
477     cyrus_init(alt_config, "ctl_backups", 0, 0);
478 
479     if ((r = mboxname_init_namespace(&ctl_backups_namespace, 1)) != 0) {
480         fatal(error_message(r), EX_CONFIG);
481     }
482     mboxevent_setnamespace(&ctl_backups_namespace);
483 
484     if (cmd == CTLBU_CMD_RECONSTRUCT) {
485         /* special handling for reconstruct */
486         // FIXME
487     }
488     else if (options.mode == CTLBU_MODE_ALL) {
489         /* loop over entire backups.db */
490         struct db *backups_db = NULL;
491 
492         r = backupdb_open(&backups_db, NULL);
493 
494         if (!r)
495             r = cyrusdb_foreach(backups_db, NULL, 0, NULL,
496                                 cmd_func[cmd], &options,
497                                 NULL);
498 
499         if (backups_db)
500             cyrusdb_close(backups_db);
501     }
502     else if (options.mode == CTLBU_MODE_DOMAIN) {
503         /* loop over domains named on command line */
504         struct db *backups_db = NULL;
505         int i;
506 
507         r = backupdb_open(&backups_db, NULL);
508 
509         for (i = optind; i < argc && !r; i++) {
510             options.domain = argv[i];
511 
512             r = cyrusdb_foreach(backups_db, NULL, 0,
513                                 domain_filter,
514                                 cmd_func[cmd], &options,
515                                 NULL);
516         }
517 
518         if (backups_db)
519             cyrusdb_close(backups_db);
520     }
521     else if (options.mode == CTLBU_MODE_PREFIX) {
522         /* loop over prefixes named on command line */
523         struct db *backups_db = NULL;
524         int i;
525 
526         r = backupdb_open(&backups_db, NULL);
527 
528         for (i = optind; i < argc && !r; i++) {
529             r = cyrusdb_foreach(backups_db,
530                                 argv[i], strlen(argv[i]),
531                                 NULL,
532                                 cmd_func[cmd], &options,
533                                 NULL);
534         }
535 
536         if (backups_db)
537             cyrusdb_close(backups_db);
538     }
539     else {
540         /* loop over backups named on command line */
541         struct buf userid = BUF_INITIALIZER;
542         struct buf fname = BUF_INITIALIZER;
543         int i;
544 
545         for (i = optind; i < argc; i++) {
546             buf_reset(&userid);
547             buf_reset(&fname);
548             mbname_t *mbname = NULL;
549 
550             if (options.mode == CTLBU_MODE_USERNAME)
551                 mbname = mbname_from_userid(argv[i]);
552             else if (options.mode == CTLBU_MODE_MBOXNAME)
553                 mbname = mbname_from_extname(argv[i], &ctl_backups_namespace, NULL);
554 
555             if (mbname) {
556                 r = backup_get_paths(mbname, &fname, NULL, BACKUP_OPEN_NOCREATE);
557                 if (r) {
558                     /* XXX this should print cyrusdb_strerror(r), except that
559                      * just returns "cyrusdb error", unhelpfully */
560                     fprintf(stderr, "%s: not found in backups.db\n", argv[i]);
561                     syslog(LOG_DEBUG, "ctl_backups '%s': %s (%d)", argv[i], cyrusdb_strerror(r), r);
562                     mbname_free(&mbname);
563                     continue;
564                 }
565                 if (mbname_userid(mbname)) {
566                     buf_setcstr(&userid, mbname_userid(mbname));
567                 }
568             }
569             else
570                 buf_setcstr(&fname, argv[i]);
571 
572             if (!r && cmd_func[cmd])
573                 r = cmd_func[cmd](&options,
574                                   buf_cstring(&userid),
575                                   buf_len(&userid),
576                                   buf_cstring(&fname),
577                                   buf_len(&fname));
578 
579             if (mbname) mbname_free(&mbname);
580 
581             if (r) break;
582         }
583 
584         buf_free(&userid);
585         buf_free(&fname);
586     }
587 
588     backup_cleanup_staging_path();
589     cyrus_done();
590     exit(r || ctlbu_skips_fails ? EX_TEMPFAIL : EX_OK);
591 }
592 
cmd_compact_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)593 static int cmd_compact_one(void *rock,
594                            const char *key, size_t key_len,
595                            const char *data, size_t data_len)
596 {
597     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
598     char *userid = NULL;
599     char *fname = NULL;
600     int r = 0;
601 
602     /* input args might not be 0-terminated, so make a safe copy */
603     if (key_len)
604         userid = xstrndup(key, key_len);
605     if (data_len)
606         fname = xstrndup(data, data_len);
607 
608     r = backup_compact(fname, options->wait, options->force,
609                        options->verbose, stdout);
610 
611     print_status("compact", userid, fname, options, r);
612 
613     if (userid) free(userid);
614     if (fname) free(fname);
615 
616     if (r) ++ctlbu_skips_fails;
617     if (r == IMAP_MAILBOX_LOCKED) r = 0;
618     return options->stop_on_error ? r : 0;
619 }
620 
cmd_delete_one(void * rock,const char * userid,size_t userid_len,const char * fname,size_t fname_len)621 static int cmd_delete_one(void *rock,
622                           const char *userid, size_t userid_len,
623                           const char *fname, size_t fname_len)
624 {
625     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
626     (void) options;
627     fprintf(stderr, "unimplemented: %s %s[%zu] %s[%zu]\n", __func__,
628             userid, userid_len, fname, fname_len);
629     return -1;
630 }
631 
cmd_list_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)632 static int cmd_list_one(void *rock,
633                         const char *key, size_t key_len,
634                         const char *data, size_t data_len)
635 {
636     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
637     struct backup *backup = NULL;
638     struct backup_chunk *latest_chunk = NULL;
639     struct stat data_stat_buf = {0};
640     char *userid = NULL;
641     char *fname = NULL;
642     char timestamp[32] = "[unknown]";
643     int r = 0;
644 
645     /* input args might not be 0-terminated, so make a safe copy */
646     if (key_len)
647         userid = xstrndup(key, key_len);
648     if (data_len)
649         fname = xstrndup(data, data_len);
650 
651     r = backup_open_paths(&backup, fname, NULL,
652                           options->wait, BACKUP_OPEN_NOCREATE);
653     if (!r && !options->noverify)
654         r = backup_verify(backup, BACKUP_VERIFY_QUICK, 0, NULL);
655 
656     if (r) {
657         fprintf(stderr, "%s: %s\n", userid ? userid : fname, error_message(r));
658         goto done;
659     }
660 
661     latest_chunk = backup_get_latest_chunk(backup);
662     if (latest_chunk) {
663         if (options->list_stale) {
664             /* skip out early if it's not stale */
665             if (time(NULL) - latest_chunk->ts_end < 3600 * options->list_stale)
666                 goto done;
667         }
668 
669         strftime(timestamp, sizeof(timestamp), "%F %T",
670                  localtime(&latest_chunk->ts_end));
671     }
672 
673     r = backup_stat(backup, &data_stat_buf, NULL);
674     if (r) {
675         fprintf(stderr, "fstat %s: %s\n", userid ? userid : fname, strerror(errno));
676         data_stat_buf.st_size = -1;
677     }
678 
679     if (options->jsonout) {
680         json_t *out = json_object();
681         json_object_set_new(out, "timestamp", json_string(timestamp));
682         json_object_set_new(out, "compressed", json_integer(data_stat_buf.st_size));
683         json_object_set_new(out, "userid", json_string(userid));
684         json_object_set_new(out, "fname", json_string(fname));
685 
686         const size_t flags = JSON_INDENT(2) | JSON_PRESERVE_ORDER;
687         json_dumpf(out, stdout, flags);
688         puts("");
689         json_decref(out);
690     }
691     else {
692         printf("%s\t" OFF_T_FMT "\t%s\t%s\n",
693                timestamp,
694                data_stat_buf.st_size,
695                userid ? userid : "[unknown]",
696                fname);
697     }
698 
699 done:
700     if (latest_chunk) backup_chunk_free(&latest_chunk);
701     if (backup) backup_close(&backup);
702     if (userid) free(userid);
703     if (fname) free(fname);
704 
705     if (r) ++ctlbu_skips_fails;
706     if (r == IMAP_MAILBOX_LOCKED) r = 0;
707     return options->stop_on_error ? r : 0;
708 }
709 
cmd_lock_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)710 static int cmd_lock_one(void *rock,
711                         const char *key, size_t key_len,
712                         const char *data, size_t data_len)
713 {
714     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
715     char *userid = NULL;
716     char *fname = NULL;
717     int r = 0;
718 
719     /* input args might not be 0-terminated, so make a safe copy */
720     if (key_len)
721         userid = xstrndup(key, key_len);
722     if (data_len)
723         fname = xstrndup(data, data_len);
724 
725     switch (options->lock_mode) {
726     case CTLBU_LOCK_MODE_UNSPECIFIED:
727     case CTLBU_LOCK_MODE_PIPE:
728         r = lock_run_pipe(userid, fname, options->wait, options->create);
729         break;
730     case CTLBU_LOCK_MODE_SQL:
731         r = lock_run_sqlite(userid, fname, options->wait, options->create);
732         break;
733     case CTLBU_LOCK_MODE_EXEC:
734         r = lock_run_exec(userid, fname, options->lock_exec_cmd,
735                           options->wait, options->create);
736         break;
737     }
738 
739     if (userid) free(userid);
740     if (fname) free(fname);
741 
742     /* don't care about stop_on_error: lock command only accepts one backup */
743     if (r) ++ctlbu_skips_fails;
744     return r;
745 }
746 
cmd_move_one(void * rock,const char * userid,size_t userid_len,const char * fname,size_t fname_len)747 static int cmd_move_one(void *rock,
748                         const char *userid, size_t userid_len,
749                         const char *fname, size_t fname_len)
750 {
751     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
752     (void) options;
753     fprintf(stderr, "unimplemented: %s %s[%zu] %s[%zu]\n", __func__,
754             userid, userid_len, fname, fname_len);
755     return -1;
756 }
757 
cmd_reindex_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)758 static int cmd_reindex_one(void *rock,
759                            const char *key, size_t key_len,
760                            const char *data, size_t data_len)
761 {
762     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
763     char *userid = NULL;
764     char *fname = NULL;
765     int r;
766 
767     /* input args might not be 0-terminated, so make a safe copy */
768     if (key_len)
769         userid = xstrndup(key, key_len);
770     if (data_len)
771         fname = xstrndup(data, data_len);
772 
773     r = backup_reindex(fname, options->wait, options->verbose, stdout);
774 
775     print_status("reindex", userid, fname, options, r);
776 
777     if (userid) free(userid);
778     if (fname) free(fname);
779 
780     if (r) ++ctlbu_skips_fails;
781     if (r == IMAP_MAILBOX_LOCKED) r = 0;
782     return options->stop_on_error ? r : 0;
783 }
784 
_sum_message_lengths_cb(const struct backup_message * message,void * rock)785 static int _sum_message_lengths_cb(const struct backup_message *message,
786                                    void *rock)
787 {
788     size_t *sum = (size_t *) rock;
789 
790     *sum += message->length;
791 
792     return 0;
793 }
794 
cmd_stat_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)795 static int cmd_stat_one(void *rock,
796                         const char *key, size_t key_len,
797                         const char *data, size_t data_len)
798 {
799     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
800     struct backup *backup = NULL;
801     char *userid = NULL;
802     char *fname = NULL;
803     struct backup_chunk_list *all_chunks = NULL, *keep_chunks = NULL;
804     struct backup_chunk *latest_chunk = NULL;
805     struct backup_chunk *chunk;
806     time_t since;
807     size_t compactable = 0, uncompressed = 0;
808     struct stat data_stat;
809     double cmp_ratio, utl_ratio;
810     char start_time[32] = "[unknown]";
811     char end_time[32] = "[unknown]";
812     int r;
813 
814     /* input args might not be 0-terminated, so make a safe copy */
815     if (key_len)
816         userid = xstrndup(key, key_len);
817     if (data_len)
818         fname = xstrndup(data, data_len);
819 
820     r = backup_open_paths(&backup, fname, NULL,
821                           options->wait, BACKUP_OPEN_NOCREATE);
822     if (r) goto done;
823 
824     if (!options->noverify) {
825         r = backup_verify(backup, BACKUP_VERIFY_QUICK, 0, NULL);
826         if (r) goto done;
827     }
828 
829     const int retention = config_getduration(IMAPOPT_BACKUP_RETENTION, 'd');
830     if (retention > 0) {
831         since = time(0) - retention;
832     }
833     else {
834         /* zero or negative retention means "keep forever" */
835         since = -1;
836     }
837 
838     all_chunks = backup_get_chunks(backup);
839     keep_chunks = backup_get_live_chunks(backup, since);
840 
841     if (!all_chunks || !keep_chunks) {
842         r = IMAP_INTERNAL;
843         goto done;
844     }
845 
846     /* uncompressed length is sum of lengths of all chunks */
847     for (chunk = all_chunks->head; chunk; chunk = chunk->next) {
848         uncompressed += chunk->length;
849     }
850 
851     /* compactable length is sum of live message lengths in keep chunks */
852     for (chunk = keep_chunks->head; chunk; chunk = chunk->next) {
853         backup_message_foreach(backup, chunk->id, &since,
854                                _sum_message_lengths_cb, &compactable);
855     }
856 
857     /* compression ratio is compressed length / uncompressed length */
858     r = backup_stat(backup, &data_stat, NULL);
859     if (r) goto done;
860     cmp_ratio = 100.0 * data_stat.st_size / uncompressed;
861 
862     /* utilisation ratio is compactable length / uncompressed length */
863     utl_ratio = 100.0 * compactable / uncompressed;
864 
865     /* start/end time are from latest chunk */
866     latest_chunk = backup_get_latest_chunk(backup);
867     if (latest_chunk) {
868         strftime(start_time, sizeof(start_time), "%F %T",
869                  localtime(&latest_chunk->ts_start));
870         strftime(end_time, sizeof(end_time), "%F %T",
871                  localtime(&latest_chunk->ts_end));
872     }
873 
874     if (options->jsonout) {
875         json_t *out = json_object();
876         json_object_set_new(out, "userid", json_string(userid));
877         json_object_set_new(out, "fname", json_string(fname));
878         json_object_set_new(out, "compressed", json_integer(data_stat.st_size));
879         json_object_set_new(out, "uncompressed", json_integer(uncompressed));
880         json_object_set_new(out, "compactable", json_integer(compactable));
881         json_object_set_new(out, "compression_ratio", json_real(cmp_ratio));
882         json_object_set_new(out, "utilisation_ratio", json_real(utl_ratio));
883         json_object_set_new(out, "last_start_time", json_string(start_time));
884         json_object_set_new(out, "last_end_time", json_string(end_time));
885 
886         const size_t flags = JSON_INDENT(2) | JSON_PRESERVE_ORDER;
887         json_dumpf(out, stdout, flags);
888         puts("");
889         json_decref(out);
890     }
891     else {
892         printf("%s\t" OFF_T_FMT "\t" SIZE_T_FMT "\t" SIZE_T_FMT "\t%6.1f%%\t%6.1f%%\t%21s\t%s\n",
893                userid ? userid : fname,
894                data_stat.st_size,
895                uncompressed,
896                compactable,
897                cmp_ratio,
898                utl_ratio,
899                start_time,
900                end_time);
901     }
902 
903 done:
904     if (r) {
905         fprintf(stderr, "%s: %s\n", userid ? userid : fname, error_message(r));
906     }
907 
908     if (latest_chunk) backup_chunk_free(&latest_chunk);
909     if (all_chunks) backup_chunk_list_free(&all_chunks);
910     if (keep_chunks) backup_chunk_list_free(&keep_chunks);
911     if (backup) backup_close(&backup);
912     if (fname) free(fname);
913     if (userid) free(userid);
914 
915     return options->stop_on_error ? r : 0;
916 }
917 
cmd_verify_one(void * rock,const char * key,size_t key_len,const char * data,size_t data_len)918 static int cmd_verify_one(void *rock,
919                           const char *key, size_t key_len,
920                           const char *data, size_t data_len)
921 {
922     struct ctlbu_cmd_options *options = (struct ctlbu_cmd_options *) rock;
923     struct backup *backup = NULL;
924     char *userid = NULL;
925     char *fname = NULL;
926     int r;
927 
928     /* input args might not be 0-terminated, so make a safe copy */
929     if (key_len)
930         userid = xstrndup(key, key_len);
931     if (data_len)
932         fname = xstrndup(data, data_len);
933 
934     r = backup_open_paths(&backup, fname, NULL,
935                           options->wait, BACKUP_OPEN_NOCREATE);
936 
937     /* n.b. deliberately ignoring nonsensical noverify option here */
938     if (!r) r = backup_verify(backup, BACKUP_VERIFY_FULL, options->verbose, stdout);
939 
940     print_status("verify", userid, fname, options, r);
941 
942     if (backup) backup_close(&backup);
943     if (userid) free(userid);
944     if (fname) free(fname);
945 
946     if (r) ++ctlbu_skips_fails;
947     if (r == IMAP_MAILBOX_LOCKED) r = 0;
948     return options->stop_on_error ? r : 0;
949 }
950 
lock_run_pipe(const char * userid,const char * fname,enum backup_open_nonblock nonblock,enum backup_open_create create)951 static int lock_run_pipe(const char *userid, const char *fname,
952                          enum backup_open_nonblock nonblock,
953                          enum backup_open_create create)
954 {
955     printf("* Trying to obtain lock on %s...\n", userid ? userid : fname);
956 
957     struct backup *backup = NULL;
958     int r;
959 
960     r = backup_open_paths(&backup, fname, NULL, nonblock, create);
961 
962     if (!r)
963         r = backup_verify(backup, BACKUP_VERIFY_QUICK, 0, NULL);
964 
965     if (r) {
966         printf("NO failed (%s)\n", error_message(r));
967         r = backup_close(&backup);
968         return EX_SOFTWARE; // FIXME would something else be more appropriate?
969     }
970 
971     printf("OK locked\n");
972 
973     /* wait until stdin closes */
974     char buf[PROT_BUFSIZE] = {0};
975     while (!feof(stdin)) {
976         if (!fgets(buf, sizeof(buf), stdin))
977             break;
978     }
979 
980     r = backup_close(&backup);
981     if (r) fprintf(stderr, "warning: backup_close() returned %i\n", r);
982 
983     return 0;
984 }
985 
lock_run_sqlite(const char * userid,const char * fname,enum backup_open_nonblock nonblock,enum backup_open_create create)986 static int lock_run_sqlite(const char *userid, const char *fname,
987                            enum backup_open_nonblock nonblock,
988                            enum backup_open_create create)
989 {
990     fprintf(stderr, "trying to obtain lock on %s...\n", userid ? userid : fname);
991 
992     struct backup *backup = NULL;
993     const char *index_fname = NULL;
994     int r, status;
995     pid_t pid;
996 
997     r = backup_open_paths(&backup, fname, NULL, nonblock, create);
998 
999     if (!r)
1000         r = backup_verify(backup, BACKUP_VERIFY_QUICK, 0, NULL);
1001 
1002     if (r) {
1003         fprintf(stderr, "unable to lock %s: %s\n",
1004                 userid ? userid : fname,
1005                 error_message(r));
1006         r = backup_close(&backup);
1007         return EX_SOFTWARE;
1008     }
1009 
1010     index_fname = backup_get_index_fname(backup);
1011 
1012     /* FIXME probably need to do something with signals here */
1013 
1014     pid = fork();
1015 
1016     switch (pid) {
1017     case -1:
1018         perror("fork");
1019         r = EX_SOFTWARE;
1020         break;
1021 
1022     case 0:
1023         /* child */
1024         fprintf(stderr, "execlp: %s %s\n", "sqlite3", index_fname);
1025         execlp("sqlite3", "sqlite3", index_fname, NULL);
1026         /* execlp never returns */
1027         perror("execlp sqlite3");
1028         _exit(EX_SOFTWARE);
1029         break;
1030 
1031     default:
1032         /* parent */
1033         waitpid(pid, &status, 0);
1034         if (WIFEXITED(status))
1035             r = WEXITSTATUS(status);
1036         else
1037             r = EX_SOFTWARE;
1038         break;
1039     }
1040 
1041     backup_close(&backup);
1042     return r;
1043 }
1044 
1045 static const char data_fname_env[] = "ctl_backups_lock_data_fname";
1046 static const char index_fname_env[] = "ctl_backups_lock_index_fname";
1047 
lock_run_exec(const char * userid,const char * fname,const char * cmd,enum backup_open_nonblock nonblock,enum backup_open_create create)1048 static int lock_run_exec(const char *userid, const char *fname,
1049                          const char *cmd,
1050                          enum backup_open_nonblock nonblock,
1051                          enum backup_open_create create)
1052 {
1053     fprintf(stderr, "trying to obtain lock on %s...\n", userid ? userid : fname);
1054 
1055     struct backup *backup = NULL;
1056     int r;
1057 
1058     r = backup_open_paths(&backup, fname, NULL, nonblock, create);
1059 
1060     if (!r)
1061         r = backup_verify(backup, BACKUP_VERIFY_QUICK, 0, NULL);
1062 
1063     if (r) {
1064         fprintf(stderr, "unable to lock %s: %s\n",
1065                 userid ? userid : fname,
1066                 error_message(r));
1067         r = backup_close(&backup);
1068         return EX_SOFTWARE;
1069     }
1070 
1071     setenv(data_fname_env, fname, 1);
1072     setenv(index_fname_env, backup_get_index_fname(backup), 1);
1073 
1074     r = system(cmd);
1075 
1076     unsetenv(data_fname_env);
1077     unsetenv(index_fname_env);
1078 
1079     if (r == -1)
1080         r = EX_SOFTWARE;
1081     else if (WIFEXITED(r))
1082         r = WEXITSTATUS(r);
1083     else
1084         r = EX_SOFTWARE;
1085 
1086     backup_close(&backup);
1087     return r;
1088 }
1089