xref: /dragonfly/usr.bin/undo/undo.c (revision 4290546f)
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 
542 	/*
543 	 * If fp != NULL dump the history to stdout.
544 	 */
545 	for (i = 0; fp && i < tid_num; ++i) {
546 		if (i && hist_ary[i].tid == hist_ary[i-1].tid)
547 			continue;
548 		t = (time_t)hist_ary[i].time32;
549 		tp = localtime(&t);
550 		strftime(datestr, sizeof(datestr), "%d-%b-%Y %H:%M:%S", tp);
551 		printf("\t0x%016llx %s\n", hist_ary[i].tid, datestr);
552 	}
553 
554 	/*
555 	 * Locate the next-to-last entry, ignoring any duplicates.
556 	 */
557 	if (tid_num > 1) {
558 		for (i = tid_num - 2; i > 0; --i) {
559 			if (hist_ary[i].tid != hist_ary[tid_num-1].tid)
560 				break;
561 		}
562 		hen = hist_ary[i];
563 	}
564 done:
565 	if (hist_aryp) {
566 		*hist_aryp = hist_ary;
567 		*tid_nump = tid_num;
568 	} else {
569 		free(hist_ary);
570 	}
571 	return(hen);
572 }
573 
574 static
575 hammer_tid_t
576 parse_delta_time(const char *timeStr)
577 {
578 	hammer_tid_t tid;
579 
580 	tid = strtoull(timeStr, NULL, 0);
581 	return(tid);
582 }
583 
584 static void
585 runcmd(int fd, const char *cmd, ...)
586 {
587 	va_list va;
588 	pid_t pid;
589 	char **av;
590 	int ac;
591 	int i;
592 
593 	va_start(va, cmd);
594 	for (ac = 0; va_arg(va, void *) != NULL; ++ac)
595 		;
596 	va_end(va);
597 
598 	av = malloc((ac + 1) * sizeof(char *));
599 	va_start(va, cmd);
600 	for (i = 0; i < ac; ++i)
601 		av[i] = va_arg(va, char *);
602 	va_end(va);
603 	av[i] = NULL;
604 
605 	if ((pid = fork()) < 0) {
606 		perror("fork");
607 		exit(1);
608 	} else if (pid == 0) {
609 		if (fd != 1) {
610 			dup2(fd, 1);
611 			close(fd);
612 		}
613 		execv(cmd, av);
614 		_exit(1);
615 	} else {
616 		while (waitpid(pid, NULL, 0) != pid)
617 			;
618 	}
619 	free(av);
620 }
621 
622 /*
623  * Convert tid to timestamp.
624  */
625 static char *
626 timestamp(hammer_ioc_hist_entry_t hen)
627 {
628 	static char timebuf[64];
629 	time_t t = (time_t)hen->time32;
630 	struct tm *tp;
631 
632 	tp = localtime(&t);
633 	strftime(timebuf, sizeof(timebuf), "%d-%b-%Y %H:%M:%S", tp);
634 	return(timebuf);
635 }
636 
637 static void
638 usage(void)
639 {
640 	fprintf(stderr, "undo [-adDiuv] [-o outfile] "
641 			"[-t transaction-id] [-t transaction-id] file...\n"
642 			"    -a       Iterate all historical segments\n"
643 			"    -d       Forward diff\n"
644 			"    -D       Reverse diff\n"
645 			"    -i       Dump history transaction ids\n"
646 			"    -u       Generate .undo files\n"
647 			"    -v       Verbose\n"
648 			"    -o file  Output to the specified file\n"
649 			"    -t TID   Retrieve as of transaction-id, TID\n"
650 			"             (a second `-t TID' to diff two versions)\n");
651 	exit(1);
652 }
653 
654