e297c1d8c03f44c944253e98823e2ed60c34e88a
[dragonfly.git] / usr.bin / undo / undo.c
1 /*
2  * Copyright (c) 2008 The DragonFly Project.  All rights reserved.
3  * 
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@backplane.com>
6  * 
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in
15  *    the documentation and/or other materials provided with the
16  *    distribution.
17  * 3. Neither the name of The DragonFly Project nor the names of its
18  *    contributors may be used to endorse or promote products derived
19  *    from this software without specific, prior written permission.
20  * 
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
25  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32  * SUCH DAMAGE.
33  * 
34  * $DragonFly: src/usr.bin/undo/undo.c,v 1.6 2008/07/17 21:34:47 thomas Exp $
35  */
36 /*
37  * UNDO - retrieve an older version of a file.
38  */
39
40 #include <sys/types.h>
41 #include <sys/stat.h>
42 #include <sys/wait.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <stdarg.h>
46 #include <string.h>
47 #include <unistd.h>
48 #include <fcntl.h>
49 #include <errno.h>
50 #include <vfs/hammer/hammer_disk.h>
51 #include <vfs/hammer/hammer_ioctl.h>
52
53 enum undo_type { TYPE_FILE, TYPE_DIFF, TYPE_RDIFF, TYPE_HISTORY };
54
55 static void doiterate(const char *orig_filename, const char *outFileName,
56                    const char *outFilePostfix, int mult, enum undo_type type);
57 static void dogenerate(const char *filename, const char *outFileName,
58                    const char *outFilePostfix,
59                    int mult, int idx, enum undo_type type,
60                    struct hammer_ioc_hist_entry ts1,
61                    struct hammer_ioc_hist_entry ts2,
62                    int force);
63 static struct hammer_ioc_hist_entry
64             find_recent(const char *filename);
65 static struct hammer_ioc_hist_entry
66             output_history(const char *filename, int fd, FILE *fp,
67                    struct hammer_ioc_hist_entry **hist_ary, int *tid_num);
68 static hammer_tid_t parse_delta_time(const char *timeStr);
69 static void runcmd(int fd, const char *cmd, ...);
70 static char *timestamp(hammer_ioc_hist_entry_t hen);
71 static void usage(void);
72
73 static int VerboseOpt;
74
75 int
76 main(int ac, char **av)
77 {
78         const char *outFileName = NULL;
79         const char *outFilePostfix = NULL;
80         enum { CMD_DUMP, CMD_ITERATEALL } cmd;
81         enum undo_type type;
82         struct hammer_ioc_hist_entry ts1;
83         struct hammer_ioc_hist_entry ts2;
84         int c;
85         int mult;
86
87         bzero(&ts1, sizeof(ts1));
88         bzero(&ts2, sizeof(ts2));
89
90         cmd = CMD_DUMP;
91         type = TYPE_FILE;
92
93         while ((c = getopt(ac, av, "adDiuvo:t:")) != -1) {
94                 switch(c) {
95                 case 'd':
96                         if (type != TYPE_FILE)
97                                 usage();
98                         type = TYPE_DIFF;
99                         break;
100                 case 'D':
101                         if (type != TYPE_FILE)
102                                 usage();
103                         type = TYPE_RDIFF;
104                         break;
105                 case 'i':
106                         if (type != TYPE_FILE)
107                                 usage();
108                         type = TYPE_HISTORY;
109                         break;
110                 case 'a':
111                         cmd = CMD_ITERATEALL;
112                         break;
113                 case 'u':
114                         outFilePostfix = ".undo";
115                         break;
116                 case 'v':
117                         ++VerboseOpt;
118                         break;
119                 case 'o':
120                         outFileName = optarg;
121                         break;
122                 case 't':
123                         if (ts1.tid && ts2.tid)
124                                 usage();
125                         else if (ts1.tid == 0)
126                                 ts1.tid = parse_delta_time(optarg);
127                         else
128                                 ts2.tid = parse_delta_time(optarg);
129                         break;
130                 default:
131                         usage();
132                         /* NOT REACHED */
133                         break;
134                 }
135         }
136
137         /*
138          * Option validation
139          */
140         if (outFileName && outFilePostfix) {
141                 fprintf(stderr, "The -o option may not be combined with -u\n");
142                 usage();
143         }
144
145         ac -= optind;
146         av += optind;
147         mult = (ac > 1);
148
149         if (ac == 0)
150                 usage();
151
152         /*
153          * Validate the output template, if specified.
154          */
155         if (outFileName && mult) {
156                 const char *ptr = outFileName;
157                 int didStr = 0;
158
159                 while ((ptr = strchr(ptr, '%')) != NULL) {
160                         if (ptr[1] == 's') {
161                                 if (didStr) {
162                                         fprintf(stderr, "Malformed output "
163                                                         "template\n");
164                                         usage();
165                                 }
166                                 didStr = 1;
167                                 ++ptr;
168                         } else if (ptr[1] != '%') {
169                                 fprintf(stderr, "Malformed output template\n");
170                                 usage();
171                         } else {
172                                 ptr += 2;
173                         }
174                 }
175         }
176
177         while (ac) {
178                 switch(cmd) {
179                 case CMD_DUMP:
180                         dogenerate(*av, outFileName, outFilePostfix,
181                                    mult, -1, type, ts1, ts2, 1);
182                         break;
183                 case CMD_ITERATEALL:
184                         doiterate(*av, outFileName, outFilePostfix,
185                                   mult, type);
186                         break;
187                 }
188                 ++av;
189                 --ac;
190         }
191         return(0);
192 }
193
194 /*
195  * Iterate through a file's history
196  */
197 static
198 void
199 doiterate(const char *orig_filename, const char *outFileName,
200            const char *outFilePostfix, int mult, enum undo_type type)
201 {
202         hammer_ioc_hist_entry_t tid_ary = NULL;
203         struct hammer_ioc_hist_entry tid_max;
204         struct hammer_ioc_hist_entry ts1;
205         const char *use_filename;
206         char *path = NULL;
207         int tid_num = 0;
208         int i;
209         int fd;
210
211         tid_max.tid = HAMMER_MAX_TID;
212         tid_max.time32 = 0;
213
214         use_filename = orig_filename;
215         if ((fd = open(orig_filename, O_RDONLY)) < 0) {
216                 ts1 = find_recent(orig_filename);
217                 if (ts1.tid) {
218                         asprintf(&path, "%s@@0x%016llx",
219                                  orig_filename, ts1.tid);
220                         use_filename = path;
221                 }
222         }
223
224         if ((fd = open(use_filename, O_RDONLY)) >= 0) {
225                 printf("%s: ITERATE ENTIRE HISTORY\n", orig_filename);
226                 output_history(NULL, fd, NULL, &tid_ary, &tid_num);
227                 close(fd);
228
229                 for (i = 0; i < tid_num; ++i) {
230                         if (i && tid_ary[i].tid == tid_ary[i-1].tid)
231                                 continue;
232
233                         if (i == tid_num - 1) {
234                                 dogenerate(orig_filename,
235                                            outFileName, outFilePostfix,
236                                            mult, i, type,
237                                            tid_ary[i], tid_max, 0);
238                         } else {
239                                 dogenerate(orig_filename,
240                                            outFileName, outFilePostfix,
241                                            mult, i, type,
242                                            tid_ary[i], tid_ary[i+1], 0);
243                         }
244                 }
245
246         } else {
247                 printf("%s: ITERATE ENTIRE HISTORY: %s\n",
248                         orig_filename, strerror(errno));
249         }
250         if (path)
251                 free(path);
252 }
253
254 /*
255  * Generate output for a file as-of ts1 (ts1 may be 0!), if diffing then
256  * through ts2.
257  */
258 static
259 void
260 dogenerate(const char *filename, const char *outFileName,
261            const char *outFilePostfix,
262            int mult, int idx, enum undo_type type,
263            struct hammer_ioc_hist_entry ts1,
264            struct hammer_ioc_hist_entry ts2,
265            int force)
266 {
267         struct stat st;
268         const char *elm;
269         char *ipath1 = NULL;
270         char *ipath2 = NULL;
271         FILE *fi;
272         FILE *fp; 
273         char *buf;
274         char *path;
275         int n;
276
277         buf = malloc(8192);
278
279         /*
280          * Open the input file.  If ts1 is 0 try to locate the most recent
281          * version of the file prior to the current version.
282          */
283         if (ts1.tid == 0)
284                 ts1 = find_recent(filename);
285         asprintf(&ipath1, "%s@@0x%016llx", filename, ts1.tid);
286         if (lstat(ipath1, &st) < 0) {
287                 free(ipath1);
288                 asprintf(&ipath1, "%s", filename);
289                 if (force == 0 || lstat(ipath1, &st) < 0) {
290                         fprintf(stderr, "Cannot locate src/historical "
291                                         "idx=%d %s@@0x%016llx,\n"
292                                         "the file may have been renamed "
293                                         "in the past.\n",
294                                 idx, filename, ts1.tid);
295                         goto done;
296                 }
297                 fprintf(stderr, 
298                         "WARNING: %s was renamed at some point in the past,\n"
299                         "attempting to continue with current version\n",
300                         filename);
301         }
302
303         if (ts2.tid == 0) {
304                 asprintf(&ipath2, "%s", filename);
305         } else {
306                 asprintf(&ipath2, "%s@@0x%015llx", filename, ts2.tid);
307         }
308         if (lstat(ipath2, &st) < 0) {
309                 if (VerboseOpt) {
310                         if (ts2.tid) {
311                                 fprintf(stderr, "Cannot locate tgt/historical "
312                                                 "idx=%d %s\n",
313                                         idx, ipath2);
314                         } else if (VerboseOpt > 1) {
315                                 fprintf(stderr, "Cannot locate %s\n", filename);
316                         }
317                 }
318                 ipath2 = strdup("/dev/null");
319         }
320
321         /*
322          * elm is the last component of the input file name
323          */
324         if ((elm = strrchr(filename, '/')) != NULL)
325                 ++elm;
326         else
327                 elm = filename;
328
329         /*
330          * Where do we stuff our output?
331          */
332         if (outFileName) {
333                 if (mult) {
334                         asprintf(&path, outFileName, elm);
335                         fp = fopen(path, "w");
336                         if (fp == NULL) {
337                                 perror(path);
338                                 exit(1);
339                         }
340                         free(path);
341                 } else {
342                         fp = fopen(outFileName, "w");
343                         if (fp == NULL) {
344                                 perror(outFileName);
345                                 exit(1);
346                         }
347                 }
348         } else if (outFilePostfix) {
349                 if (idx >= 0) {
350                         asprintf(&path, "%s%s.%04d", filename,
351                                  outFilePostfix, idx);
352                 } else {
353                         asprintf(&path, "%s%s", filename, outFilePostfix);
354                 }
355                 fp = fopen(path, "w");
356                 if (fp == NULL) {
357                         perror(path);
358                         exit(1);
359                 }
360                 free(path);
361         } else {
362                 if (mult && type == TYPE_FILE) {
363                         if (idx >= 0) {
364                                 printf("\n>>> %s %04d 0x%016llx %s\n\n",
365                                        filename, idx, ts1.tid, timestamp(&ts1));
366                         } else {
367                                 printf("\n>>> %s ---- 0x%016llx %s\n\n",
368                                        filename, ts1.tid, timestamp(&ts1));
369                         }
370                 } else if (idx >= 0 && type == TYPE_FILE) {
371                         printf("\n>>> %s %04d 0x%016llx %s\n\n", 
372                                filename, idx, ts1.tid, timestamp(&ts1));
373                 }
374                 fp = stdout;
375         }
376
377         switch(type) {
378         case TYPE_FILE:
379                 if ((fi = fopen(ipath1, "r")) != NULL) {
380                         while ((n = fread(buf, 1, 8192, fi)) > 0)
381                                 fwrite(buf, 1, n, fp);
382                         fclose(fi);
383                 }
384                 break;
385         case TYPE_DIFF:
386                 printf("diff -u %s %s (to %s)\n",
387                        ipath1, ipath2, timestamp(&ts2));
388                 fflush(stdout);
389                 runcmd(fileno(fp), "/usr/bin/diff", "diff", "-u", ipath1, ipath2, NULL);
390                 break;
391         case TYPE_RDIFF:
392                 printf("diff -u %s %s\n", ipath2, ipath1);
393                 fflush(stdout);
394                 runcmd(fileno(fp), "/usr/bin/diff", "diff", "-u", ipath2, ipath1, NULL);
395                 break;
396         case TYPE_HISTORY:
397                 if ((fi = fopen(ipath1, "r")) != NULL) {
398                         output_history(filename, fileno(fi), fp, NULL, NULL);
399                         fclose(fi);
400                 }
401                 break;
402         }
403
404         if (fp != stdout)
405                 fclose(fp);
406 done:
407         free(buf);
408 }
409
410 /*
411  * Try to find a recent version of the file.
412  *
413  * XXX if file cannot be found
414  */
415 static
416 struct hammer_ioc_hist_entry
417 find_recent(const char *filename)
418 {
419         hammer_ioc_hist_entry_t tid_ary = NULL;
420         int tid_num = 0;
421         struct hammer_ioc_hist_entry hen;
422         char *dirname;
423         char *path;
424         int fd;
425         int i;
426
427         if ((fd = open(filename, O_RDONLY)) >= 0) {
428                 hen = output_history(NULL, fd, NULL, NULL, NULL);
429                 close(fd);
430                 return(hen);
431         }
432
433         /*
434          * If the object does not exist acquire the history of its
435          * directory and then try accessing the object at each TID.
436          */
437         if (strrchr(filename, '/')) {
438                 dirname = strdup(filename);
439                 *strrchr(dirname, '/') = 0;
440         } else {
441                 dirname = strdup(".");
442         }
443
444         hen.tid = 0;
445         hen.time32 = 0;
446         if ((fd = open(dirname, O_RDONLY)) >= 0) {
447                 output_history(NULL, fd, NULL, &tid_ary, &tid_num);
448                 close(fd);
449                 free(dirname);
450
451                 for (i = tid_num - 1; i >= 0; --i) {
452                         asprintf(&path, "%s@@0x%016llx", filename, tid_ary[i].tid);
453                         if ((fd = open(path, O_RDONLY)) >= 0) {
454                                 hen = output_history(NULL, fd, NULL, NULL, NULL);
455                                 close(fd);
456                                 free(path);
457                                 break;
458                         }
459                         free(path);
460                 }
461         }
462         return(hen);
463 }
464
465 /*
466  * Collect all the transaction ids representing changes made to the
467  * file, sort, and output (weeding out duplicates).  If fp is NULL
468  * we do not output anything and simply return the most recent TID we
469  * can find.
470  */
471 static int
472 tid_cmp(const void *arg1, const void *arg2)
473 {
474         const struct hammer_ioc_hist_entry *tid1 = arg1;
475         const struct hammer_ioc_hist_entry *tid2 = arg2;
476
477         if (tid1->tid < tid2->tid)
478                 return(-1);
479         if (tid1->tid > tid2->tid)
480                 return(1);
481         return(0);
482 }
483
484 static
485 struct hammer_ioc_hist_entry
486 output_history(const char *filename, int fd, FILE *fp,
487                struct hammer_ioc_hist_entry **hist_aryp, int *tid_nump)
488 {
489         struct hammer_ioc_hist_entry hen;
490         struct hammer_ioc_history hist;
491         char datestr[64];
492         struct tm *tp;
493         time_t t;
494         int tid_max = 32;
495         int tid_num = 0;
496         int i;
497         hammer_ioc_hist_entry_t hist_ary = malloc(tid_max * sizeof(*hist_ary));
498
499         bzero(&hist, sizeof(hist));
500         hist.beg_tid = HAMMER_MIN_TID;
501         hist.end_tid = HAMMER_MAX_TID;
502         hist.head.flags |= HAMMER_IOC_HISTORY_ATKEY;
503         hist.key = 0;
504         hist.nxt_key = HAMMER_MAX_KEY;
505
506         hen.tid = 0;
507         hen.time32 = 0;
508
509         if (ioctl(fd, HAMMERIOC_GETHISTORY, &hist) < 0) {
510                 if (filename)
511                         printf("%s: %s\n", filename, strerror(errno));
512                 goto done;
513         }
514         if (filename)
515                 printf("%s: objid=0x%016llx\n", filename, hist.obj_id);
516         for (;;) {
517                 if (tid_num + hist.count >= tid_max) {
518                         tid_max = (tid_max * 3 / 2) + hist.count;
519                         hist_ary = realloc(hist_ary, tid_max * sizeof(*hist_ary));
520                 }
521                 for (i = 0; i < hist.count; ++i) {
522                         hist_ary[tid_num++] = hist.hist_ary[i];
523                 }
524                 if (hist.head.flags & HAMMER_IOC_HISTORY_EOF)
525                         break;
526                 if (hist.head.flags & HAMMER_IOC_HISTORY_NEXT_KEY) {
527                         hist.key = hist.nxt_key;
528                         hist.nxt_key = HAMMER_MAX_KEY;
529                 }
530                 if (hist.head.flags & HAMMER_IOC_HISTORY_NEXT_TID) 
531                         hist.beg_tid = hist.nxt_tid;
532                 if (ioctl(fd, HAMMERIOC_GETHISTORY, &hist) < 0) {
533                         if (filename)
534                                 printf("%s: %s\n", filename, strerror(errno));
535                         break;
536                 }
537         }
538         qsort(hist_ary, tid_num, sizeof(*hist_ary), tid_cmp);
539         if (tid_num == 0)
540                 goto done;
541         for (i = 0; fp && i < tid_num; ++i) {
542                 if (i && hist_ary[i].tid == hist_ary[i-1].tid)
543                         continue;
544                 t = (time_t)hist_ary[i].time32;
545                 tp = localtime(&t);
546                 strftime(datestr, sizeof(datestr), "%d-%b-%Y %H:%M:%S", tp);
547                 printf("\t0x%016llx %s\n", hist_ary[i].tid, datestr);
548         }
549         if (tid_num > 1)
550                 hen = hist_ary[tid_num-2];
551 done:
552         if (hist_aryp) {
553                 *hist_aryp = hist_ary;
554                 *tid_nump = tid_num;
555         } else {
556                 free(hist_ary);
557         }
558         return(hen);
559 }
560
561 static
562 hammer_tid_t
563 parse_delta_time(const char *timeStr)
564 {
565         hammer_tid_t tid;
566
567         tid = strtoull(timeStr, NULL, 0);
568         return(tid);
569 }
570
571 static void
572 runcmd(int fd, const char *cmd, ...)
573 {
574         va_list va;
575         pid_t pid;
576         char **av;
577         int ac;
578         int i;
579
580         va_start(va, cmd);
581         for (ac = 0; va_arg(va, void *) != NULL; ++ac)
582                 ;
583         va_end(va);
584
585         av = malloc((ac + 1) * sizeof(char *));
586         va_start(va, cmd);
587         for (i = 0; i < ac; ++i)
588                 av[i] = va_arg(va, char *);
589         va_end(va);
590         av[i] = NULL;
591
592         if ((pid = fork()) < 0) {
593                 perror("fork");
594                 exit(1);
595         } else if (pid == 0) {
596                 if (fd != 1) {
597                         dup2(fd, 1);
598                         close(fd);
599                 }
600                 execv(cmd, av);
601                 _exit(1);
602         } else {
603                 while (waitpid(pid, NULL, 0) != pid)
604                         ;
605         }
606         free(av);
607 }
608
609 /*
610  * Convert tid to timestamp.
611  */
612 static char *
613 timestamp(hammer_ioc_hist_entry_t hen)
614 {
615         static char timebuf[64];
616         time_t t = (time_t)hen->time32;
617         struct tm *tp;
618
619         tp = localtime(&t);
620         strftime(timebuf, sizeof(timebuf), "%d-%b-%Y %H:%M:%S", tp);
621         return(timebuf);
622 }
623
624 static void
625 usage(void)
626 {
627         fprintf(stderr, "undo [-adDiuv] [-o outfile] "
628                         "[-t transaction-id] [-t transaction-id] file...\n"
629                         "    -a       Iterate all historical segments\n"
630                         "    -d       Forward diff\n"
631                         "    -D       Reverse diff\n"
632                         "    -i       Dump history transaction ids\n"
633                         "    -u       Generate .undo files\n"
634                         "    -v       Verbose\n"
635                         "    -o file  Output to the specified file\n"
636                         "    -t TID   Retrieve as of transaction-id, TID\n"
637                         "             (a second `-t TID' to diff two versions)\n");
638         exit(1);
639 }
640