5 recover.pl - a script to provide an interface for restore files similar
6 to Legatto Networker's recover program.
20 # Location of config file.
21 my $CONF_FILE = "$ENV{HOME}/.recoverrc";
22 my $HIST_FILE = "$ENV{HOME}/.recover.hist";
24 ########################################################################
25 ### Queries needed to gather files from directory.
26 ########################################################################
39 Job.jobtdate - ? as visible,
51 File.pathid = Path.pathid and
52 Filename.filenameid = File.filenameid and
53 Filename.name != '' and
54 File.jobid = Job.jobid
63 substring(Path.path from ? + 1) as name,
64 substring(Path.path from 1 for ?) as path,
68 Job.jobtdate - ? as visible,
79 File.jobid = Job.jobid and
80 Filename.name = '' and
81 Filename.filenameid = File.filenameid and
82 File.pathid = Path.pathid and
83 Path.path ~ ('^' || ? || '[^/]*/\$')
95 Path.path || Filename.name as name,
108 Job.jobtdate <= ? and
109 Job.jobtdate >= ? and
110 Path.path like ? || '%' and
111 File.pathid = Path.pathid and
112 Filename.filenameid = File.filenameid and
113 Filename.name != '' and
114 File.jobid = Job.jobid
135 Job.jobtdate <= ? and
136 Job.jobtdate >= ? and
137 File.jobid = Job.jobid and
138 Filename.name = '' and
139 Filename.filenameid = File.filenameid and
140 File.pathid = Path.pathid and
141 Path.path like ? || '%'
148 distinct on (path, name)
154 Job.jobtdate - ? as visible,
164 Job.jobtdate <= ? and
165 Job.jobtdate >= ? and
166 File.pathid = Path.pathid and
167 File.filenameid = Filename.filenameid and
168 File.jobid = Job.jobid
170 path, name, jobid desc
181 Job.jobtdate - ? as visible,
184 Job, Path, Filename, File, JobMedia, Media
186 File.pathid = Path.pathid and
187 File.filenameid = Filename.filenameid and
188 File.jobid = Job.jobid and
189 File.Jobid = JobMedia.jobid and
190 File.fileindex >= JobMedia.firstindex and
191 File.fileindex <= JobMedia.lastindex and
192 Job.jobtdate <= ? and
193 JobMedia.mediaid = Media.mediaid and
195 Filename.name = ? and
206 distinct(Filename.name),
211 Job.jobtdate - ? as visible,
221 Job.jobtdate <= ? and
223 File.pathid = Path.pathid and
224 Filename.filenameid = File.filenameid and
225 Filename.name != '' and
226 File.jobid = Job.jobid
236 distinct(substring(Path.path from ? + 1)) as name,
237 substring(Path.path from 1 for ?) as path,
241 Job.jobtdate - ? as visible,
251 Job.jobtdate <= ? and
252 File.jobid = Job.jobid and
253 Filename.name = '' and
254 Filename.filenameid = File.filenameid and
255 File.pathid = Path.pathid and
256 Path.path rlike concat('^', ?, '[^/]*/\$')
270 distinct(concat(Path.path, Filename.name)) as name,
283 Job.jobtdate <= ? and
284 Job.jobtdate >= ? and
285 Path.path like concat(?, '%') and
286 File.pathid = Path.pathid and
287 Filename.filenameid = File.filenameid and
288 Filename.name != '' and
289 File.jobid = Job.jobid
299 distinct(Path.path) as name,
312 Job.jobtdate <= ? and
313 Job.jobtdate >= ? and
314 File.jobid = Job.jobid and
315 Filename.name = '' and
316 Filename.filenameid = File.filenameid and
317 File.pathid = Path.pathid and
318 Path.path like concat(?, '%')
333 Job.jobtdate - ? as visible,
343 Job.jobtdate <= ? and
344 Job.jobtdate >= ? and
345 File.pathid = Path.pathid and
346 File.filenameid = Filename.filenameid and
347 File.jobid = Job.jobid
351 path, name, jobid desc
362 Job.jobtdate - ? as visible,
365 Job, Path, Filename, File, JobMedia, Media
367 File.pathid = Path.pathid and
368 File.filenameid = Filename.filenameid and
369 File.jobid = Job.jobid and
370 File.Jobid = JobMedia.jobid and
371 File.fileindex >= JobMedia.firstindex and
372 File.fileindex <= JobMedia.lastindex and
373 Job.jobtdate <= ? and
374 JobMedia.mediaid = Media.mediaid and
376 Filename.name = ? and
384 ############################################################################
385 ### Command lists for help and file completion
386 ############################################################################
389 'add' => '(add files) - Add files recursively to restore list',
390 'bootstrap' => 'print bootstrap file',
391 'cd' => '(cd dir) - Change working directory',
392 'changetime', '(changetime date/time) - Change database view to date',
393 'client' => '(client client-name) - change client to view',
394 'debug' => 'toggle debug flag',
395 'delete' => 'Remove files from restore list.',
396 'help' => 'Display this list',
397 'history', 'Print command history',
398 'info', '(info files) - Print stat and tape information about files',
399 'ls' => '(ls [opts] files) - List files in current directory',
400 'pwd' => 'Print current working directory',
401 'quit' => 'Exit program',
402 'recover', 'Create table for bconsole to use in recover',
403 'relocate', '(relocate dir) - specify new location for recovered files',
404 'show', '(show item) - Display information about item',
405 'verbose' => 'toggle verbose flag',
406 'versions', '(versions files) - Show all versions of file on tape',
407 'volumes', 'Show volumes needed for restore.'
411 'cache' => 'Display cached directories',
412 'catalog' => 'Display name of current catalog from config file',
413 'client' => 'Display current client',
414 'clients' => 'Display clients available in this catalog',
415 'restore' => 'Display information about pending restore',
416 'volumes' => 'Show volumes needed for restore.'
419 ##############################################################################
420 ### Read config and command line.
421 ##############################################################################
424 my $catalog; # Current catalog
437 my $restore_to = '/';
445 B<recover.pl> [B<-b> I<db connect string>] [B<-c> I<client> B<-j> I<jobname>]
446 [B<-i> I<initial diretory>] [B<-p>] [B<-t> I<timespec>]
448 B<recover.pl> [B<-h>]
450 Most of the command line arguments can be specified in the init file
451 B<$HOME/.recoverrc> (see CONFIG FILE FORMAT below). The command
452 line arguments will override the options in the init file. If no
453 I<catalogname> is specified, the first one found in the init file will
458 B<recover.pl> will read the specified catalog and provide a shell like
459 environment from which a time based view of the specified client/jobname
460 and be exampled and selected for restoration.
462 The command line option B<-b> specified the DBI compatible connect
463 script to use when connecting to the catalog database. The B<-c> and
464 B<-j> options specify the client and jobname respectively to view from
465 the catalog database. The B<-i> option will set the initial directory
466 you are viewing to the specified directory. if B<-i> is not specified,
467 it will default to /. You can set the initial time to view the catalog
468 from using the B<-t> option.
470 The B<-p> option will pre-load the entire catalog into memory. This
471 could take a lot of memory, so use it with caution.
473 The B<-d> option turns on debugging and the B<-v> option turns on
476 By specifying a I<catalogname>, the default options for connecting to
477 the catalog database will be taken from the section of the init file
478 specified by that name.
480 The B<-h> option will display this document.
482 In order for this program to have a chance of not being painfully slow,
483 the following indexs should be added to your database.
485 B<CREATE INDEX file_pathid_idx on file(pathid);>
487 B<CREATE INDEX file_filenameid_idx on file(filenameid);>
492 getopts("c:b:hi:j:pt:vd", $vars) || die "Usage: bad arguments\n";
495 system("perldoc $0");
499 $preload = $vars->{'p'} if ($vars->{'p'});
500 $debug = $vars->{'d'} if ($vars->{'d'});
501 $verbose = $vars->{'v'} if ($vars->{'v'});
503 # Set initial time to view the catalog
506 $rtime = parsedate($vars->{'t'}, FUZZY => 1, PREFER_PAST => 1);
523 # Read config file (if available).
525 &read_config($CONF_FILE);
529 $catalog = $ARGV[0] if (@ARGV);
532 $cstr = ${catalogs{$catalog}}->{'client'}
533 if (${catalogs{$catalog}}->{'client'});
535 $jobname = $catalogs{$catalog}->{'jobname'}
536 if ($catalogs{$catalog}->{'jobname'});
538 $dbconnect = $catalogs{$catalog}->{'dbconnect'}
539 if ($catalogs{$catalog}->{'dbconnect'});
541 $username = $catalogs{$catalog}->{'username'}
542 if ($catalogs{$catalog}->{'username'});
544 $password = $catalogs{$catalog}->{'password'}
545 if ($catalogs{$catalog}->{'password'});
547 $start_dir = $catalogs{$catalog}->{'cd'}
548 if ($catalogs{$catalog}->{'cd'});
550 $preload = $catalogs{$catalog}->{'preload'}
551 if ($catalogs{$catalog}->{'preload'} && !defined($vars->{'p'}));
553 $verbose = $catalogs{$catalog}->{'verbose'}
554 if ($catalogs{$catalog}->{'verbose'} && !defined($vars->{'v'}));
556 $debug = $catalogs{$catalog}->{'debug'}
557 if ($catalogs{$catalog}->{'debug'} && !defined($vars->{'d'}));
560 #### Command line overries config file
562 $start_dir = $vars->{'i'} if ($vars->{'i'});
563 $start_dir = '/' if (!$start_dir);
565 $start_dir .= '/' if (substr($start_dir, length($start_dir) - 1, 1) ne '/');
568 $dbconnect = $vars->{'b'};
571 die "You must supply a db connect string.\n" if (!defined($dbconnect));
573 if ($dbconnect =~ /^dbi:Pg/) {
576 elsif ($dbconnect =~ /^dbi:mysql/) {
580 die "Unknown database type specified in $dbconnect\n";
583 # Initialize database connection
585 print STDERR "DBG: Connect using: $dbconnect\n" if ($debug);
587 my $dbh = DBI->connect($dbconnect, $username, $password) ||
588 die "Can't open bacula database\nDatabase connect string '$dbconnect'";
590 die "Client id required.\n" if (!($cstr || $vars->{'c'}));
592 $cstr = $vars->{'c'} if ($vars->{'c'});
593 $client = &lookup_client($cstr);
595 # Set job information
596 $jobname = $vars->{'j'} if ($vars->{'j'});
598 die "You need to specify a job name.\n" if (!$jobname);
602 die "Failed to set client\n" if (!$client);
605 my $dir_sth = $dbh->prepare($queries{$db}->{'dir'})
606 || die "Can't prepare $queries{$db}->{'dir'}\n";
608 my $sel_sth = $dbh->prepare($queries{$db}->{'sel'})
609 || die "Can't prepare $queries{$db}->{'sel'}\n";
611 my $ver_sth = $dbh->prepare($queries{$db}->{'ver'})
612 || die "Can't prepare $queries{$db}->{'ver'}\n";
616 # Initialize readline.
617 my $term = new Term::ReadLine('Bacula Recover');
620 my $readline = $term->ReadLine;
621 my $tty_attribs = $term->Attribs;
623 # Needed for base64 decode
625 my @base64_digits = (
626 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
627 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
628 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
629 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
630 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
632 my @base64_map = (0) x 128;
634 for (my $i=0; $i<64; $i++) {
635 $base64_map[ord($base64_digits[$i])] = $i;
638 ##############################################################################
640 ##############################################################################
644 B<$HOME/.recoverrc> Configuration file for B<recover.pl>.
646 =head1 CONFIG FILE FORMAT
648 The config file will allow you to specify the defaults for your
649 catalog(s). Each catalog definition starts with B<[>I<catalogname>B<]>.
650 Blank lines and lines starting with # are ignored.
652 The first catalog specified will be used as the default catalog.
654 All values are specified in I<item> B<=> I<value> format. You can
655 specify the following I<item>s for each catalog.
660 my $conf_file = shift;
663 # No nothing if config file can't be read.
666 open(CONF, "<$conf_file") || die "$!: Can't open $conf_file\n";
670 # Skip comments and blank links
676 $catalog = $c if (!$catalog);
679 die "Duplicate catalog definition in $conf_file\n";
685 die "Conf file must start with catalog definition [catname]\n";
689 if (/^(\w+)\s*=\s*(.*)/) {
695 The name of the default client to view when connecting to this
696 catalog. This can be changed later with the B<client> command.
700 if ($item eq 'client') {
701 $catalogs{$c}->{'client'} = $value;
706 The DBI compatible database string to use to connect to this catalog.
712 dbi:Pg:dbname=bacula;host=backuphost
717 elsif ($item eq 'dbconnect') {
718 $catalogs{$c}->{'dbconnect'} = $value;
723 The name of the default job to view when connecting to the catalog. This
724 can be changed later with the B<client> command.
727 elsif ($item eq 'jobname') {
728 $catalogs{$c}->{'jobname'} = $value;
733 The password to use when connecing to the catalog database.
736 elsif ($item eq 'password') {
737 $catalogs{$c}->{'password'} = $value;
742 Set the preload flag. A preload flag of 1 or on will load the entire
743 catalog when recover.pl is start. This is a memory hog, so use with
747 elsif ($item eq 'preload') {
749 if ($value =~ /^(1|on)$/i) {
750 $catalogs{$c}->{'preload'} = 1;
752 elsif ($value =~ /^(0|off)$/i) {
753 $catalogs{$c}->{'preload'} = 0;
756 die "$value: Unknown value for preload.\n";
763 The username to use when connecing to the catalog database.
766 elsif ($item eq 'username') {
767 $catalogs{$c}->{'username'} = $value;
770 die "Unknown opton $item in $conf_file.\n";
775 die "Bad line $_ in $conf_file.\n";
787 sub create_file_entry {
790 my $fileindex = shift;
795 print STDERR "DBG: name = $name\n" if ($debug);
796 print STDERR "DBG: fileid = $fileid\n" if ($debug);
797 print STDERR "DBG: fileindex = $fileindex\n" if ($debug);
798 print STDERR "DBG: jobid = $jobid\n" if ($debug);
799 print STDERR "DBG: visible = $visible\n" if ($debug);
800 print STDERR "DBG: lstat = $lstat\n" if ($debug);
804 fileindex => $fileindex,
806 visible => ($visible >= 0) ? 1 : 0
812 foreach my $s (split(' ', $lstat)) {
813 print STDERR "DBG: Add $s to stat array.\n" if ($debug);
814 push(@stat, from_base64($s));
818 'st_dev' => $stat[0],
819 'st_ino' => $stat[1],
820 'st_mode' => $stat[2],
821 'st_nlink' => $stat[3],
822 'st_uid' => $stat[4],
823 'st_gid' => $stat[5],
824 'st_rdev' => $stat[6],
825 'st_size' => $stat[7],
826 'st_blksize' => $stat[8],
827 'st_blocks' => $stat[9],
828 'st_atime' => $stat[10],
829 'st_mtime' => $stat[11],
830 'st_ctime' => $stat[12],
831 'LinkFI' => $stat[13],
832 'st_flags' => $stat[14],
833 'data_stream' => $stat[15]
836 # Create mode string.
837 my $sstr = &mode2str($stat[2]);
838 $data->{'lstat'}->{'statstr'} = $sstr;
841 # Read directory data, return hash reference.
846 return $dircache->{$dir} if ($dircache->{$dir});
848 print "$dir not cached, fetching from database.\n" if ($verbose);
852 my $dl = length($dir);
854 print STDERR "? - 1: ftime = $ftime\n" if ($debug);
855 print STDERR "? - 2: client = $client\n" if ($debug);
856 print STDERR "? - 3: jobname = $jobname\n" if ($debug);
857 print STDERR "? - 4: rtime = $rtime\n" if ($debug);
858 print STDERR "? - 5: dir = $dir\n" if ($debug);
859 print STDERR "? - 6, 7: dl = $dl, $dl\n" if ($debug);
860 print STDERR "? - 8: ftime = $ftime\n" if ($debug);
861 print STDERR "? - 9: client = $client\n" if ($debug);
862 print STDERR "? - 10: jobname = $jobname\n" if ($debug);
863 print STDERR "? - 11: rtime = $rtime\n" if ($debug);
864 print STDERR "? - 12: dir = $dir\n" if ($debug);
866 print STDERR "DBG: Execute - $queries{$db}->{'dir'}\n" if ($debug);
879 ) || die "Can't execute $queries{$db}->{'dir'}\n";
881 while (my $ref = $dir_sth->fetchrow_hashref) {
882 my $file = $$ref{name};
883 print STDERR "DBG: File $file found in database.\n" if ($debug);
884 my $l = length($file);
885 $fmax = $l if ($l > $fmax);
887 $data->{$file} = &create_file_entry(
897 return undef if (!$fmax);
899 $dircache->{$dir} = $data if ($usecache);
904 print "Loading entire catalog, please wait...\n";
905 my $sth = $dbh->prepare($queries{$db}->{'cache'})
906 || die "Can't prepare $queries{$db}->{'cache'}\n";
907 print STDERR "DBG: Execute - $queries{$db}->{'cache'}\n" if ($debug);
908 $sth->execute($ftime, $client, $jobname, $rtime, $ftime)
909 || die "Can't execute $queries{$db}->{'cache'}\n";
911 print "Query complete, building catalog cache...\n" if ($verbose);
913 while (my $ref = $sth->fetchrow_hashref) {
914 my $dir = $ref->{path};
915 my $file = $ref->{name};
916 print STDERR "DBG: File $dir$file found in database.\n" if ($debug);
918 next if ($dir eq '/' and $file eq ''); # Skip data for /
920 # Rearrange directory
922 if ($file eq '' and $dir =~ m|(.*/)([^/]+/)$|) {
927 my $data = &create_file_entry(
936 $dircache->{$dir} = {} if (!$dircache->{$dir});
937 $dircache->{$dir}->{$file} = $data;
943 # Break a path up into dir and file.
951 if (substr($path, 0, 1) eq '/') {
954 if ($path =~ m|^(/.*/)([^/]*$)|) {
958 else { # Must be in /
960 $file = substr($path, 1);
963 print STDERR "DBG: / Dir - $dir; file = $file\n" if ($debug);
966 elsif ($path =~ m|^(.*/)([^/]*)$|) {
970 print STDERR "DBG: Dir - $dir; file = $file\n" if ($debug);
972 # File is in our current directory.
977 print STDERR "DBG: Set dir to $dir\n" if ($debug);
980 return ($fqdir, $dir, $file);
988 my $query = "select clientid, name from Client";
989 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
990 $sth->execute || die "Can't execute $query\n";
992 while (my $ref = $sth->fetchrow_hashref) {
993 $clients->{$ref->{'name'}} = $ref->{'clientid'};
1001 if ($clients->{$c}) {
1002 $c = $clients->{$c};
1005 warn "Could not find client $c\n";
1018 my $query = "select distinct name from Job order by name";
1019 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
1020 $sth->execute || die "Can't execute $query\n";
1022 while (my $ref = $sth->fetchrow_hashref) {
1023 $jobs->{$$ref{'name'}} = $$ref{'name'};
1034 jobtdate <= $rtime and
1035 name = '$jobname' and
1037 order by jobtdate desc
1041 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
1042 $sth->execute || die "Can't execute $query\n";
1044 if ($sth->rows == 1) {
1045 my $ref = $sth->fetchrow_hashref;
1046 $ftime = $$ref{jobtdate};
1049 warn "Could not find full backup. Setting full time to 0.\n";
1065 my $finfo = &fetch_dir('/');
1066 @flist = keys %$finfo;
1074 foreach my $f (@flist) {
1076 my $path = (substr($f, 0, 1) eq '/') ? $f : "$dir$f";
1077 my ($fqdir, $dir, $file) = &path_parts($path);
1078 my $finfo = &fetch_dir($fqdir);
1080 if (!$finfo->{$file}) {
1082 if (!$finfo->{"$file/"}) {
1083 warn "$f: File not found.\n";
1090 my $info = $finfo->{$file};
1092 my $fid = $info->{'fileid'};
1093 my $fidx = $info->{'fileindex'};
1094 my $jid = $info->{'jobid'};
1095 my $size = $info->{'lstat'}->{'st_size'};
1097 if ($opts->{'all'} || $info->{'visible'}) {
1098 print STDERR "DBG: $file - $size bytes\n"
1103 if (!$restore{$fid}) {
1104 print "Adding $fqdir$file\n" if (!$opts->{'quiet'});
1105 $restore{$fid} = [$jid, $fidx];
1113 if ($restore{$fid}) {
1114 print "Removing $fqdir$file\n" if (!$opts->{'quiet'});
1115 delete $restore{$fid};
1122 if ($file =~ m|/$|) {
1124 # Use preloaded files if we already retrieved them.
1126 my $newdir = "$dir$file";
1127 my $finfo = &fetch_dir($newdir);
1128 &select_files($mark, $opts, $newdir, keys %$finfo);
1132 my $newdir = "$fqdir$file";
1133 my $begin = ($opts->{'all'}) ? 0 : $ftime;
1135 print STDERR "DBG: Execute - $queries{$db}->{'sel'}\n"
1149 ) || die "Can't execute $queries{$db}->{'sel'}\n";
1151 while (my $ref = $sel_sth->fetchrow_hashref) {
1152 my $file = $$ref{'name'};
1153 my $fid = $$ref{'fileid'};
1154 my $fidx = $$ref{'fileindex'};
1155 my $jid = $$ref{'jobid'};
1156 my @stat_enc = split(' ', $$ref{'lstat'});
1157 my $size = &from_base64($stat_enc[7]);
1161 if (!$restore{$fid}) {
1162 print "Adding $file\n" if (!$opts->{'quiet'});
1163 $restore{$fid} = [$jid, $fidx];
1171 if ($restore{$fid}) {
1172 print "Removing $file\n" if (!$opts->{'quiet'});
1173 delete $restore{$fid};
1192 # Expand shell wildcards
1196 my ($fqdir, $dir, $file) = &path_parts($path);
1197 my $finfo = &fetch_dir($fqdir);
1198 return ($path) if (!$finfo);
1200 my $pat = "^$file\$";
1202 # Add / for dir match
1205 $dpat = "^$dpat/\$";
1210 $dpat =~ s/\./\\./g;
1216 foreach my $f (sort keys %$finfo) {
1219 push (@match, ($fqdir eq $cwd) ? $f : "$fqdir$f");
1221 elsif ($f =~ /$dpat/) {
1222 push (@match, ($fqdir eq $cwd) ? $f : "$fqdir$f");
1227 return ($path) if (!@match);
1233 my ($fqdir, $dir, $file) = &path_parts($path, 1);
1235 print STDERR "Expand $path\n" if ($debug);
1237 my $finfo = &fetch_dir($fqdir);
1238 return ($path) if (!$finfo);
1242 my $pat = "^$file/\$";
1249 foreach my $f (sort keys %$finfo) {
1250 print STDERR "Match $f to $pat\n" if ($debug);
1251 push (@match, ($fqdir eq $cwd) ? $f : "$fqdir$f") if ($f =~ /$pat/);
1254 return ($path) if (!@match);
1262 if (S_ISDIR($mode)) {
1265 elsif (S_ISCHR($mode)) {
1268 elsif (S_ISBLK($mode)) {
1271 elsif (S_ISREG($mode)) {
1274 elsif (S_ISFIFO($mode)) {
1277 elsif (S_ISLNK($mode)) {
1280 elsif (S_ISSOCK($mode)) {
1287 $sstr .= ($mode&S_IRUSR) ? 'r' : '-';
1288 $sstr .= ($mode&S_IWUSR) ? 'w' : '-';
1289 $sstr .= ($mode&S_IXUSR) ?
1290 (($mode&S_ISUID) ? 's' : 'x') :
1291 (($mode&S_ISUID) ? 'S' : '-');
1292 $sstr .= ($mode&S_IRGRP) ? 'r' : '-';
1293 $sstr .= ($mode&S_IWGRP) ? 'w' : '-';
1294 $sstr .= ($mode&S_IXGRP) ?
1295 (($mode&S_ISGID) ? 's' : 'x') :
1296 (($mode&S_ISGID) ? 'S' : '-');
1297 $sstr .= ($mode&S_IROTH) ? 'r' : '-';
1298 $sstr .= ($mode&S_IWOTH) ? 'w' : '-';
1299 $sstr .= ($mode&S_IXOTH) ?
1300 (($mode&S_ISVTX) ? 't' : 'x') :
1301 (($mode&S_ISVTX) ? 'T' : '-');
1307 # Algorithm copied from bacula source
1315 if (substr($where, 0, 1) eq '-') {
1317 $where = substr($where, 1);
1320 while ($where ne '') {
1322 my $d = substr($where, 0, 1);
1323 #print STDERR "\n$d - " . ord($d) . " - " . $base64_map[ord($d)] . "\n";
1324 $val += $base64_map[ord(substr($where, 0, 1))];
1325 $where = substr($where, 1);
1331 ### Command completion code
1337 for (my $i = 0, my $matched = 1; $i < length($m[0]) && $matched; $i++) {
1338 my $c = substr($m[0], $i, 1);
1340 for (my $j = 1; $j < @m; $j++) {
1342 if ($c ne substr($m[$j], $i, 1)) {
1349 $r .= $c if ($matched);
1361 $tty_attribs->{'completion_append_character'} = ' ';
1362 $tty_attribs->{completion_entry_function} = \&nocomplete;
1363 print STDERR "\nDBG: text - $text; line - $line; start - $start; end = $end\n"
1366 # Complete command if we are at start of line.
1368 if ($start == 0 || substr($line, 0, $start) =~ /^\s*$/) {
1369 my @list = grep (/^$text/, sort keys %COMMANDS);
1370 return () if (!@list);
1371 my $match = (@list > 1) ? &get_match(@list) : '';
1372 return $match, @list;
1377 $cstr =~ s/^\s+//; # Remove leading spaces
1379 my ($cmd, @args) = shellwords($cstr);
1380 return () if (!defined($cmd));
1382 # Complete dirs for cd
1384 return () if (@args > 1);
1385 return &complete_files($text, 1);
1387 # Complete files/dirs for info and ls
1388 elsif ($cmd =~ /^(add|delete|info|ls|mark|unmark|versions)$/) {
1389 return &complete_files($text, 0);
1391 # Complete clients for client
1392 elsif ($cmd eq 'client') {
1393 return () if (@args > 2);
1398 print STDERR "DBG: " . (@args) . " arguments found.\n" if ($debug);
1400 if (@args < 1 || (@args == 1 and $line =~ /[^\s]$/)) {
1401 @flist = grep (/^$pat/, sort keys %$clients);
1404 @flist = grep (/^$pat/, sort keys %$jobs);
1407 return () if (!@flist);
1408 my $match = (@flist > 1) ? &get_match(@flist) : '';
1410 #return $match, map {s/ /\\ /g; $_} @flist;
1411 return $match, @flist;
1413 # Complete show options for show
1414 elsif ($cmd eq 'show') {
1415 return () if (@args > 1);
1416 # attempt to suggest match.
1417 my @list = grep (/^$text/, sort keys %SHOW);
1418 return () if (!@list);
1419 my $match = (@list > 1) ? &get_match(@list) : '';
1420 return $match, @list;
1422 elsif ($cmd =~ /^(bsr|bootstrap|relocate)$/) {
1423 $tty_attribs->{completion_entry_function} =
1424 $tty_attribs->{filename_completion_function};
1432 sub complete_files {
1434 my $dironly = shift;
1438 my ($fqdir, $dir, $pat) = &path_parts($path, 1);
1440 $pat =~ s/([.\[\]\\])/\\$1/g;
1441 # First check for absolute name.
1443 $finfo = &fetch_dir($fqdir);
1444 print STDERR "DBG: " . join(', ', keys %$finfo) . "\n" if ($debug);
1445 return () if (!$finfo); # Nothing if dir not found.
1448 @flist = grep (m|^$pat.*/$|, sort keys %$finfo);
1451 @flist = grep (/^$pat/, sort keys %$finfo);
1454 return undef if (!@flist);
1456 print STDERR "DBG: Files found\n" if ($debug);
1458 if (@flist == 1 && $flist[0] =~ m|/$|) {
1459 $tty_attribs->{'completion_append_character'} = '';
1462 @flist = map {s/ /\\ /g; ($fqdir eq $cwd) ? $_ : "$dir$_"} @flist;
1463 my $match = (@flist > 1) ? &get_match(@flist) : '';
1465 print STDERR "DBG: Dir - $dir; cwd - $cwd\n" if ($debug);
1466 # Fill in dir if necessary.
1467 return $match, @flist;
1474 # subroutine to create printf format for long listing of ls
1484 foreach my $f (@$flist) {
1487 my $lstat = $info->{'lstat'};
1489 my $l = length($file);
1490 $fmax = $l if ($l > $fmax);
1492 $l = length($lstat->{'st_nlink'});
1493 $lmax = $l if ($l > $lmax);
1494 $l = length($lstat->{'st_uid'});
1495 $umax = $l if ($l > $umax);
1496 $l = length($lstat->{'st_gid'});
1497 $gmax = $l if ($l > $gmax);
1498 $l = length($lstat->{'st_size'});
1499 $smax = $l if ($l > $smax);
1502 return "%s %${lmax}d %${umax}d %${gmax}d %${smax}d %s %s\n";
1508 my $w = $term->get_screen_size;
1511 my $max_cols = ($m < @list) ? $w : @list;
1515 print STDERR "Need to print $l files\n" if ($debug);
1517 while($max_cols > 1) {
1520 # Initialize array of widths
1521 @wds = 0 x $max_cols;
1523 for ($cols = 0; $cols < $max_cols && $used < $w; $cols++) {
1526 for (my $j = $cols*$fpc; $j < ($cols + 1)*$fpc && $j < $l; $j++ ) {
1527 my $fl = length($list[$j]->[0]);
1528 $cw = $fl if ($fl > $cw);
1533 print STDERR "DBG: Total so far is $used\n" if ($debug);
1543 print STDERR "DBG: $cols of $max_cols columns uses $used space.\n"
1546 print STDERR "DBG: Print $fpc files per column\n"
1549 last if ($used <= $w && $cols == $max_cols);
1550 $fpc = int($l/$cols);
1551 $fpc++ if ($l % $cols);
1552 $max_cols = $cols - 1;
1555 if ($max_cols == 1) {
1560 print STDERR "Print out $fpc rows with $cols columns\n"
1563 for (my $i = 0; $i < $fpc; $i++) {
1565 for (my $j = $i; $j < $fpc*$cols; $j += $fpc) {
1566 my $cw = $wds[($j - $i)/$fpc];
1567 my $fmt = "%s%-${cw}s";
1572 $file = $list[$j]->[0];
1573 my $fdata = $list[$j]->[1];
1574 $r = ($restore{$fdata->{'fileid'}}) ? '+' : ' ';
1581 print ' ' if ($i != $j);
1582 printf $fmt, $r, $file;
1591 my $seconds = shift;
1594 if (abs(time() - $seconds) > 15724800) {
1595 $date = time2str('%b %e %Y', $seconds);
1598 $date = time2str('%b %e %R', $seconds);
1604 # subroutine to load entire bacula database.
1607 Once running, B<recover.pl> will present the user with a shell like
1608 environment where file can be exampled and selected for recover. The
1609 shell will provide command history and editing and if you have the
1610 Gnu readline module installed on your system, it will also provide
1611 command completion. When interacting with files, wildcards should work
1614 The following commands are understood.
1624 # Nop on blank or commented lines
1625 return ('nop') if ($cstr =~ /^\s*$/);
1626 return ('nop') if ($cstr =~ /^\s*#/);
1628 # Get rid of leading white space to make shellwords work better
1631 ($cmd, @args) = shellwords($cstr);
1633 if (!defined($cmd)) {
1634 warn "Could not warse $cstr\n";
1638 =head2 add [I<filelist>]
1640 Mark I<filelist> for recovery. If I<filelist> is not specified, mark all
1641 files in the current directory. B<mark> is an alias for this command.
1644 elsif ($cmd eq 'add' || $cmd eq 'mark') {
1650 getopts("aq", $vars) || return ('error', 'Add: Usage add [-q|-a] files');
1651 $options->{'all'} = $vars->{'a'};
1652 $options->{'quiet'} =$vars->{'q'};
1655 @command = ('add', $options);
1657 foreach my $a (@ARGV) {
1658 push(@command, &expand_files($a));
1663 =head2 bootstrap I<bootstrapfile>
1665 Create a bootstrap file suitable for use with the bacula B<bextract>
1666 command. B<bsr> is an alias for this command.
1669 elsif ($cmd eq 'bootstrap' || $cmd eq 'bsr') {
1670 return ('error', 'bootstrap takes single argument (file to write to)')
1672 @command = ('bootstrap', $args[0]);
1675 =head2 cd I<directory>
1677 Allows you to set your current directory. This command understands . for
1678 the current directory and .. for the parent. Also, cd - will change you
1679 back to the previous directory you were in.
1682 elsif ($cmd eq 'cd') {
1683 # Cd with no args goes to /
1684 @args = ('/') if (!@args);
1687 return ('error', 'Bad cd. cd requires 1 and only 1 argument.');
1690 my $todir = $args[0];
1692 # cd - should cd to previous directory. It is handled later.
1693 return ('cd', '-') if ($todir eq '-');
1696 my @e = expand_dirs($todir);
1699 return ('error', 'Bad cd. Wildcard expands to more than 1 dir.');
1704 print STDERR "Initial target is $todir\n" if ($debug);
1706 # remove prepended .
1708 while ($todir =~ m|^\./(.*)|) {
1710 $todir = '.' if (!$todir);
1713 # If only . is left, replace with current directory.
1714 $todir = $cwd if ($todir eq '.');
1715 print STDERR "target after . processing is $todir\n" if ($debug);
1720 while ($todir =~ m|^\.\./(.*)|) {
1722 print STDERR "DBG: ../ found, new todir - $todir\n" if ($debug);
1723 $prefix =~ s|/[^/]*/$|/|;
1726 if ($todir eq '..') {
1727 $prefix =~ s|/[^/]*/$|/|;
1731 print STDERR "target after .. processing is $todir\n" if ($debug);
1732 print STDERR "DBG: Final prefix - $prefix\n" if ($debug);
1734 $todir = "$prefix$todir" if ($prefix ne $cwd);
1736 print STDERR "DBG: todir after .. handling - $todir\n" if ($debug);
1738 # Turn relative directories into absolute directories.
1740 if (substr($todir, 0, 1) ne '/') {
1741 print STDERR "DBG: $todir has no leading /, prepend $cwd\n" if ($debug);
1742 $todir = "$cwd$todir";
1745 # Make sure we have a trailing /
1747 if (substr($todir, length($todir) - 1) ne '/') {
1748 print STDERR "DBG: No trailing /, append /\n" if ($debug);
1752 @command = ('cd', $todir);
1755 =head2 changetime I<timespec>
1757 This command changes the time used in generating the view of the
1758 filesystem. Files that were backed up before the specified time
1759 (optionally until the next full backup) will be the only files seen.
1761 The time can be specifed in almost any reasonable way. Here are a few
1779 elsif ($cmd eq 'changetime') {
1780 @command = ($cmd, join(' ', @args));
1783 =head2 client I<clientname> I<jobname>
1785 Specify the client and jobname to view.
1788 elsif ($cmd eq 'client') {
1791 return ('error', 'client takes a two arguments client-name job-name');
1794 @command = ('client', @args);
1802 elsif ($cmd eq 'debug') {
1803 @command = ('debug');
1806 =head2 delete [I<filelist>]
1808 Un-mark file that were previous marked for recovery. If I<filelist> is
1809 not specified, mark all files in the current directory. B<unmark> is an
1810 alias for this command.
1813 elsif ($cmd eq 'delete' || $cmd eq 'unmark') {
1814 @command = ('delete');
1816 foreach my $a (@args) {
1817 push(@command, &expand_files($a));
1824 Show list of command with brief description of what they do.
1827 elsif ($cmd eq 'help') {
1828 @command = ('help');
1833 Display command line history. B<h> is an alias for this command.
1836 elsif ($cmd eq 'h' || $cmd eq 'history') {
1837 @command = ('history');
1840 =head2 info [I<filelist>]
1842 Display information about the specified files. The format of the
1843 information provided is reminiscent of the bootstrap file.
1846 elsif ($cmd eq 'info') {
1847 push(@command, 'info');
1849 foreach my $a (@args) {
1850 push(@command, &expand_files($a));
1855 =head2 ls [I<filelist>]
1857 This command will list the specified files (defaults to all files in
1858 the current directory). Files are sorted alphabetically be default. It
1859 understand the following options.
1865 Causes ls to list files even if they are only on backups preceding the
1866 closest full backup to the currently selected date/time.
1870 List files in long format (like unix ls command).
1874 reverse direction of sort.
1887 elsif ($cmd eq 'ls' || $cmd eq 'dir' || $cmd eq 'll') {
1893 getopts("altSr", $vars) || return ('error', 'Bad ls usage.');
1894 $options->{'all'} = $vars->{'a'};
1895 $options->{'long'} = $vars->{'l'};
1896 $options->{'long'} = 1 if ($cmd eq 'dir' || $cmd eq 'll');
1898 $options->{'sort'} = 'time' if ($vars->{'t'});
1900 return ('error', 'Only one sort at a time allowed.')
1901 if ($options->{'sort'} && ($vars->{'S'}));
1903 $options->{'sort'} = 'size' if ($vars->{'S'});
1904 $options->{'sort'} = 'alpha' if (!$options->{'sort'});
1906 $options->{'sort'} = 'r' . $options->{'sort'} if ($vars->{'r'});
1908 @command = ('ls', $options);
1910 foreach my $a (@ARGV) {
1911 push(@command, &expand_files($a));
1918 Show current directory.
1921 elsif ($cmd eq 'pwd') {
1929 B<q>, B<exit> and B<x> are all aliases for this command.
1932 elsif ($cmd eq 'quit' || $cmd eq 'q' || $cmd eq 'exit' || $cmd eq 'x') {
1933 @command = ('quit');
1938 This command creates a table in the bacula catalog that case be used to
1939 restore the selected files. It will also display the command to enter
1940 into bconsole to start the restore.
1943 elsif ($cmd eq 'recover') {
1944 @command = ('recover');
1947 =head2 relocate I<directory>
1949 Specify the directory to restore files to. Defaults to /.
1952 elsif ($cmd eq 'relocate') {
1953 return ('error', 'relocate required a single directory to relocate to')
1956 my $todir = $args[0];
1957 $todir = `pwd` . $todir if (substr($todir, 0, 1) ne '/');
1958 @command = ('relocate', $todir);
1963 Show various information about B<recover.pl>. The following items can be specified.
1969 Display's a list of cached directories.
1973 Displays the name of the catalog we are talking to.
1977 Display current client and job named that are being viewed.
1981 Display the number of files and size to be restored.
1985 Display the volumes that will be required to perform a restore on the
1991 elsif ($cmd eq 'show') {
1992 return ('error', 'show takes a single argument') if (@args != 1);
1993 @command = ('show', $args[0]);
1998 Toggle verbose flag.
2001 elsif ($cmd eq 'verbose') {
2002 @command = ('verbose');
2005 =head2 versions [I<filelist>]
2007 View all version of specified files available from the current
2008 time. B<ver> is an alias for this command.
2011 elsif ($cmd eq 'versions' || $cmd eq 'ver') {
2012 push(@command, 'versions');
2014 foreach my $a (@args) {
2015 push(@command, &expand_files($a));
2022 Display the volumes that will be required to perform a restore on the
2026 elsif ($cmd eq 'volumes') {
2027 @command = ('volumes');
2030 @command = ('error', "$cmd: Unknown command");
2036 ##############################################################################
2037 ### Command processing
2038 ##############################################################################
2040 # Add files to restore list.
2046 my $save_rnum = $rnum;
2047 &select_files(1, $opts, $cwd, @flist);
2048 print "" . ($rnum - $save_rnum) . " files marked for restore\n";
2052 my $bsrfile = shift;
2057 # Get list of job ids to restore from.
2059 foreach my $fid (keys %restore) {
2060 $jobs{$restore{$fid}->[0]} = 1;
2063 my $jlist = join(', ', sort keys %jobs);
2066 print "Nothing to restore.\n";
2070 # Read in media info
2080 startfile as volfile,
2081 JobMedia.startblock,
2089 Job.jobid in ($jlist) and
2090 Job.jobid = JobMedia.jobid and
2091 JobMedia.mediaid = Media.mediaid
2098 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2099 $sth->execute || die "Can't execute $query\n";
2101 while (my $ref = $sth->fetchrow_hashref) {
2103 'jobid' => $ref->{'jobid'},
2104 'volumename' => $ref->{'volumename'},
2105 'mediatype' => $ref->{'mediatype'},
2106 'volsessionid' => $ref->{'volsessionid'},
2107 'volsessiontime' => $ref->{'volsessiontime'},
2108 'firstindex' => $ref->{'firstindex'},
2109 'lastindex' => $ref->{'lastindex'},
2110 'volfile' => $ref->{'volfile'},
2111 'startblock' => $ref->{'startblock'},
2112 'endblock' => $ref->{'endblock'},
2113 'volindex' => $ref->{'volindex'}
2117 # Gather bootstrap info
2119 # key - jobid.volumename.volumesession.volindex
2128 # array of file indexes.
2130 for my $info (values %restore) {
2131 my $jobid = $info->[0];
2132 my $fidx = $info->[1];
2134 foreach my $m (@media) {
2136 if ($jobid == $m->{'jobid'} && $fidx >= $m->{'firstindex'} && $fidx <= $m->{'lastindex'}) {
2137 my $key = "$jobid.";
2138 $key .= "$m->{volumename}.$m->{volsessionid}.$m->{volindex}";
2140 $bootstrap{$key} = {
2142 'name' => $m->{'volumename'},
2143 'type' => $m->{'mediatype'},
2144 'session' => $m->{'volsessionid'},
2145 'index' => $m->{'volindex'},
2146 'time' => $m->{'volsessiontime'},
2147 'file' => $m->{'volfile'},
2148 'startblock' => $m->{'startblock'},
2149 'endblock' => $m->{'endblock'}
2151 if (!$bootstrap{$key});
2153 $bootstrap{$key}->{'files'} = []
2154 if (!$bootstrap{$key}->{'files'});
2155 push(@{$bootstrap{$key}->{'files'}}, $fidx);
2164 print STDERR "DBG: Keys = " . join(', ', keys %bootstrap) . "\n"
2168 return $bootstrap{$a}->{'time'} <=> $bootstrap{$b}->{'time'}
2169 if ($bootstrap{$a}->{'time'} != $bootstrap{$b}->{'time'});
2170 return $bootstrap{$a}->{'name'} cmp $bootstrap{$b}->{'name'}
2171 if ($bootstrap{$a}->{'name'} ne $bootstrap{$b}->{'name'});
2172 return $bootstrap{$a}->{'session'} <=> $bootstrap{$b}->{'session'}
2173 if ($bootstrap{$a}->{'session'} != $bootstrap{$b}->{'session'});
2174 return $bootstrap{$a}->{'index'} <=> $bootstrap{$b}->{'index'};
2177 if (!open(BSR, ">$bsrfile")) {
2178 warn "$bsrfile: $|\n";
2182 foreach my $key (@keys) {
2183 my $info = $bootstrap{$key};
2184 print BSR "Volume=\"$info->{name}\"\n";
2185 print BSR "MediaType=\"$info->{type}\"\n";
2186 print BSR "VolSessionId=$info->{session}\n";
2187 print BSR "VolSessionTime=$info->{time}\n";
2188 print BSR "VolFile=$info->{file}\n";
2189 print BSR "VolBlock=$info->{startblock}-$info->{endblock}\n";
2191 my @fids = sort { $a <=> $b} @{$bootstrap{$key}->{'files'}};
2195 for (my $i = 0; $i < @fids; $i++) {
2196 $first = $fids[$i] if (!$first);
2200 if ($fids[$i] != $prev + 1) {
2201 print BSR "FileIndex=$first";
2202 print BSR "-$prev" if ($first != $prev);
2212 print BSR "FileIndex=$first";
2213 print BSR "-$prev" if ($first != $prev);
2215 print BSR "Count=" . (@fids) . "\n";
2228 $dir = $lwd if ($dir eq '-' && defined($lwd));
2231 $files = &fetch_dir($dir);
2234 warn "Previous director not defined.\n";
2242 print STDERR "Could not locate directory $dir\n";
2246 $cwd = '/' if (!$cwd);
2249 sub cmd_changetime {
2253 print "Time currently set to " . localtime($rtime) . "\n";
2257 my $newtime = parsedate($tstr, FUZZY => 1, PREFER_PAST => 1);
2259 if (defined($newtime)) {
2260 print STDERR "Time evaluated to $newtime\n" if ($debug);
2262 print "Setting date/time to " . localtime($rtime) . "\n";
2267 &cache_catalog if ($preload);
2269 # Get directory based on new time.
2270 $files = &fetch_dir($cwd);
2273 print STDERR "Could not parse $tstr as date/time\n";
2282 $jobname = shift; # Set global job name
2285 $client = &lookup_client($c);
2287 # Clear cache, we changed machines/jobs
2289 &cache_catalog if ($preload);
2291 # Find last full backup time.
2294 # Get current directory on new client.
2295 $files = &fetch_dir($cwd);
2297 # Clear restore info
2304 $debug = 1 - $debug;
2309 my $opts = {quiet=>1};
2311 my $save_rnum = $rnum;
2312 &select_files(0, $opts, $cwd, @flist);
2313 print "" . ($save_rnum - $rnum) . " files un-marked for restore\n";
2318 foreach my $h (sort keys %COMMANDS) {
2319 printf "%-12s %s\n", $h, $COMMANDS{$h};
2326 foreach my $h ($term->GetHistory) {
2332 # Print catalog/tape info about files
2336 @flist = ($cwd) if (!@flist);
2338 foreach my $f (@flist) {
2340 my ($fqdir, $dir, $file) = &path_parts($f);
2341 my $finfo = &fetch_dir($fqdir);
2343 if (!$finfo->{$file}) {
2345 if (!$finfo->{"$file/"}) {
2346 warn "$f: File not found.\n";
2353 my $fileid = $finfo->{$file}->{fileid};
2354 my $fileindex = $finfo->{$file}->{fileindex};
2355 my $jobid = $finfo->{$file}->{jobid};
2358 print "#FileID : $finfo->{$file}->{fileid}\n";
2359 print "#JobID : $jobid\n";
2360 print "#Visible : $finfo->{$file}->{visible}\n";
2368 JobMedia.startblock,
2375 Job.jobid = $jobid and
2376 Job.jobid = JobMedia.jobid and
2377 $fileindex >= firstindex and
2378 $fileindex <= lastindex and
2379 JobMedia.mediaid = Media.mediaid
2382 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2383 $sth->execute || die "Can't execute $query\n";
2385 while (my $ref = $sth->fetchrow_hashref) {
2386 print "Volume=\"$ref->{volumename}\"\n";
2387 print "MediaType=\"$ref->{mediatype}\"\n";
2388 print "VolSessionId=$ref->{volsessionid}\n";
2389 print "VolSessionTime=$ref->{volsessiontime}\n";
2390 print "VolFile=$ref->{startfile}\n";
2391 print "VolBlock=$ref->{startblock}-$ref->{endblock}\n";
2392 print "FileIndex=$finfo->{$file}->{fileindex}\n";
2408 print STDERR "DBG: " . (@flist) . " files to list.\n" if ($debug);
2411 @flist = keys %$files;
2414 # Sort files as specified.
2416 if ($opts->{sort} eq 'alpha') {
2417 print STDERR "DBG: Sort by alpha\n" if ($debug);
2418 @keys = sort @flist;
2420 elsif ($opts->{sort} eq 'ralpha') {
2421 print STDERR "DBG: Sort by reverse alpha\n" if ($debug);
2422 @keys = sort {$b cmp $a} @flist;
2424 elsif ($opts->{sort} eq 'time') {
2425 print STDERR "DBG: Sort by time\n" if ($debug);
2428 if ($files->{$b}->{'lstat'}->{'st_mtime'} ==
2429 $files->{$a}->{'lstat'}->{'st_mtime'});
2430 $files->{$b}->{'lstat'}->{'st_mtime'} <=>
2431 $files->{$a}->{'lstat'}->{'st_mtime'}
2434 elsif ($opts->{sort} eq 'rtime') {
2435 print STDERR "DBG: Sort by reverse time\n" if ($debug);
2438 if ($files->{$a}->{'lstat'}->{'st_mtime'} ==
2439 $files->{$b}->{'lstat'}->{'st_mtime'});
2440 $files->{$a}->{'lstat'}->{'st_mtime'} <=>
2441 $files->{$b}->{'lstat'}->{'st_mtime'}
2444 elsif ($opts->{sort} eq 'size') {
2445 print STDERR "DBG: Sort by size\n" if ($debug);
2448 if ($files->{$a}->{'lstat'}->{'st_size'} ==
2449 $files->{$b}->{'lstat'}->{'st_size'});
2450 $files->{$b}->{'lstat'}->{'st_size'} <=>
2451 $files->{$a}->{'lstat'}->{'st_size'}
2454 elsif ($opts->{sort} eq 'rsize') {
2455 print STDERR "DBG: Sort by reverse size\n" if ($debug);
2458 if ($files->{$a}->{'lstat'}->{'st_size'} ==
2459 $files->{$b}->{'lstat'}->{'st_size'});
2460 $files->{$a}->{'lstat'}->{'st_size'} <=>
2461 $files->{$b}->{'lstat'}->{'st_size'}
2465 print STDERR "DBG: $opts->{sort}, no sort\n" if ($debug);
2471 foreach my $f (@keys) {
2472 print STDERR "DBG: list $f\n" if ($debug);
2474 my ($fqdir, $dir, $file) = &path_parts($f);
2475 my $finfo = &fetch_dir($fqdir);
2477 if (!$finfo->{$file}) {
2479 if (!$finfo->{"$file/"}) {
2480 warn "$f: File not found.\n";
2487 my $fdata = $finfo->{$file};
2489 if ($opts->{'all'} || $fdata->{'visible'}) {
2490 push(@flist, ["$dir$file", $fdata]);
2495 if ($opts->{'long'}) {
2496 my $lfmt = &long_fmt(\@flist) if ($opts->{'long'});
2498 foreach my $f (@flist) {
2500 my $fdata = $f->[1];
2501 my $r = ($restore{$fdata->{'fileid'}}) ? '+' : ' ';
2502 my $lstat = $fdata->{'lstat'};
2504 printf $lfmt, $lstat->{'statstr'}, $lstat->{'st_nlink'},
2505 $lstat->{'st_uid'}, $lstat->{'st_gid'}, $lstat->{'st_size'},
2506 ls_date($lstat->{'st_mtime'}), "$r$file";
2510 &print_by_cols(@flist);
2519 # Create restore data for bconsole
2522 my $query = "create table recover (jobid int, fileindex int)";
2525 || warn "Could not create recover table. Hope it's already there.\n";
2527 if ($db eq 'postgres') {
2528 $query = "COPY recover FROM STDIN";
2530 $dbh->do($query) || die "Can't execute $query\n";
2532 foreach my $finfo (values %restore) {
2533 $dbh->pg_putline("$finfo->[0]\t$finfo->[1]\n");
2540 foreach my $finfo (values %restore) {
2541 $query = "insert into recover (
2542 'jobid', 'fileindex'
2545 $finfo->[0], $finfo->[1]
2547 $dbh->do($query) || die "Can't execute $query\n";
2552 $query = "GRANT all on recover to bacula";
2553 $dbh->do($query) || die "Can't execute $query\n";
2555 $query = "select name from Client where clientid = $client";
2556 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2557 $sth->execute || die "Can't execute $query\n";
2559 my $ref = $sth->fetchrow_hashref;
2560 print "Restore prepared. Run bconsole and enter the following command\n";
2561 print "restore client=$$ref{name} where=$restore_to file=\?recover\n";
2566 $restore_to = shift;
2569 # Display information about recover's state
2574 if ($what eq 'clients') {
2576 foreach my $c (sort keys %$clients) {
2581 elsif ($what eq 'catalog') {
2584 elsif ($what eq 'client') {
2585 my $query = "select name from Client where clientid = $client";
2586 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2587 $sth->execute || die "Can't execute $query\n";
2589 my $ref = $sth->fetchrow_hashref;
2590 print "$$ref{name}; $jobname\n";
2593 elsif ($what eq 'cache') {
2594 print "The following directories are cached\n";
2596 foreach my $d (sort keys %$dircache) {
2601 elsif ($what eq 'restore') {
2602 print "There are $rnum files marked for restore.\n";
2604 print STDERR "DBG: Bytes = $rbytes\n" if ($debug);
2606 if ($rbytes < 1024) {
2607 print "The restore will require $rbytes bytes.\n";
2609 elsif ($rbytes < 1024*1024) {
2610 my $rk = $rbytes/1024;
2611 printf "The restore will require %.2f KB.\n", $rk;
2613 elsif ($rbytes < 1024*1024*1024) {
2614 my $rm = $rbytes/1024/1024;
2615 printf "The restore will require %.2f MB.\n", $rm;
2618 my $rg = $rbytes/1024/1024/1024;
2619 printf "The restore will require %.2f GB.\n", $rg;
2622 print "Restores will be placed in $restore_to\n";
2624 elsif ($what eq 'volumes') {
2627 elsif ($what eq 'qinfo') {
2628 my $dl = length($cwd);
2629 print "? - 1: ftime = $ftime\n";
2630 print "? - 2: client = $client\n";
2631 print "? - 3: jobname = $jobname\n";
2632 print "? - 4: rtime = $rtime\n";
2633 print "? - 5: dir = $cwd\n";
2634 print "? - 6, 7: dl = $dl\n";
2635 print "? - 8: ftime = $ftime\n";
2636 print "? - 9: client = $client\n";
2637 print "? - 10: jobname = $jobname\n";
2638 print "? - 11: rtime = $rtime\n";
2639 print "? - 12: dir = $cwd\n";
2642 warn "Don't know how to show $what\n";
2648 $verbose = 1 - $verbose;
2654 @flist = ($cwd) if (!@flist);
2656 foreach my $f (@flist) {
2660 print STDERR "DBG: Get versions for $f\n" if ($debug);
2663 my ($fqdir, $dir, $file) = &path_parts($f);
2664 my $finfo = &fetch_dir($fqdir);
2666 if (!$finfo->{$file}) {
2668 if (!$finfo->{"$file/"}) {
2669 warn "$f: File not found.\n";
2676 if ($file =~ m|/$|) {
2677 $path = "$fqdir$file";
2684 print STDERR "DBG: Use $ftime, $path, $file, $client, $jobname\n"
2687 $ver_sth->execute($ftime, $rtime, $path, $file, $client, $jobname)
2688 || die "Can't execute $queries{$db}->{'ver'}\n";
2692 while (my $ref = $ver_sth->fetchrow_hashref) {
2693 my $f = "$ref->{name};$ref->{jobtdate}";
2694 $data->{$f} = &create_file_entry(
2697 $ref->{'fileindex'},
2703 $data->{$f}->{'jobtdate'} = $ref->{'jobtdate'};
2704 $data->{$f}->{'volume'} = $ref->{'volumename'};
2708 $data->{$a}->{'jobtdate'} <=>
2709 $data->{$b}->{'jobtdate'}
2714 foreach my $f (@keys) {
2715 push(@list, [$file, $data->{$f}]);
2718 my $lfmt = &long_fmt(\@list);
2719 print "\nVersions of \`$path$file' earlier than ";
2720 print localtime($rtime) . ":\n\n";
2722 foreach my $f (@keys) {
2723 my $lstat = $data->{$f}->{'lstat'};
2724 printf $lfmt, $lstat->{'statstr'}, $lstat->{'st_nlink'},
2725 $lstat->{'st_uid'}, $lstat->{'st_gid'}, $lstat->{'st_size'},
2726 time2str('%c', $lstat->{'st_mtime'}), $file;
2727 print "save time: " . localtime($data->{$f}->{'jobtdate'}) . "\n";
2728 print " location: $data->{$f}->{volume}\n\n";
2735 # List volumes needed for restore.
2743 my $query = "select mediaid, volumename from Media";
2744 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2746 $sth->execute || die "Can't execute $query\n";
2748 while (my $ref = $sth->fetchrow_hashref) {
2749 $media{$$ref{'mediaid'}} = $$ref{'volumename'};
2755 $query = "select mediaid, jobid, firstindex, lastindex from JobMedia";
2756 $sth = $dbh->prepare($query) || die "Can't prepare $query\n";
2758 $sth->execute || die "Can't execute $query\n";
2760 while (my $ref = $sth->fetchrow_hashref) {
2762 'mediaid' => $$ref{'mediaid'},
2763 'jobid' => $$ref{'jobid'},
2764 'firstindex' => $$ref{'firstindex'},
2765 'lastindex' => $$ref{'lastindex'}
2771 # Find needed volumes
2773 foreach my $fileid (keys %restore) {
2774 my ($jobid, $idx) = @{$restore{$fileid}};
2776 foreach my $jm (@jobmedia) {
2777 next if ($jm->{'jobid'}) != $jobid;
2779 if ($idx >= $jm->{'firstindex'} && $idx <= $jm->{'lastindex'}) {
2780 $volumes{$media{$jm->{'mediaid'}}} = 1;
2787 print "The following volumes are needed for restore.\n";
2789 foreach my $v (sort keys %volumes) {
2797 print STDERR "$msg\n";
2800 ##############################################################################
2801 ### Start of program
2802 ##############################################################################
2804 &cache_catalog if ($preload);
2806 print "Using $readline for command processing\n" if ($verbose);
2808 # Initialize command completion
2810 # Add binding for Perl readline. Issue warning.
2811 if ($readline eq 'Term::ReadLine::Gnu') {
2812 $term->ReadHistory($HIST_FILE);
2813 print STDERR "DBG: FCD - $tty_attribs->{filename_completion_desired}\n"
2815 $tty_attribs->{attempted_completion_function} = \&complete;
2816 $tty_attribs->{attempted_completion_function} = \&complete;
2817 print STDERR "DBG: Quote chars = '$tty_attribs->{filename_quote_characters}'\n" if ($debug);
2819 elsif ($readline eq 'Term::ReadLine::Perl') {
2820 readline::rl_bind('TAB', 'ViComplete');
2821 warn "Command completion disabled. $readline is seriously broken\n";
2824 warn "Can't deal with $readline, Command completion disabled.\n";
2827 &cmd_cd($start_dir);
2829 while (defined($cstr = $term->readline('recover> '))) {
2830 print "\n" if ($readline eq 'Term::ReadLine::Perl');
2831 my @command = parse_command($cstr);
2832 last if ($command[0] eq 'quit');
2833 next if ($command[0] eq 'nop');
2835 print STDERR "Execute $command[0] command.\n" if ($debug);
2837 my $cmd = \&{"cmd_$command[0]"};
2839 # The following line will call the subroutine named cmd_ prepended to
2840 # the name of the command returned by parse_command.
2842 &$cmd(@command[1..$#command]);
2850 print "\n" if (!defined($cstr));
2852 $term->WriteHistory($HIST_FILE) if ($readline eq 'Term::ReadLine::Gnu');
2856 The following CPAN modules are required to run this program.
2858 DBI, Term::ReadKey, Time::ParseDate, Date::Format, Text::ParseWords
2860 Additionally, you will only get command line completion if you also have
2866 Karl Hakimian <hakimian@aha.com>
2870 Copyright (C) 2006 Karl Hakimian
2872 This program is free software; you can redistribute it and/or modify
2873 it under the terms of the GNU General Public License as published by
2874 the Free Software Foundation; either version 2 of the License, or
2875 (at your option) any later version.
2877 This program is distributed in the hope that it will be useful,
2878 but WITHOUT ANY WARRANTY; without even the implied warranty of
2879 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2880 GNU General Public License for more details.
2882 You should have received a copy of the GNU General Public License
2883 along with this program; if not, write to the Free Software
2884 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA