1#!/usr/bin/perl -w 2 3=head1 NAME 4 5recover.pl - a script to provide an interface for restore files similar 6to Legatto Networker's recover program. 7 8=cut 9 10use strict; 11use Getopt::Std; 12use DBI; 13use Term::ReadKey; 14use Term::ReadLine; 15use Fcntl ':mode'; 16use Time::ParseDate; 17use Date::Format; 18use Text::ParseWords; 19 20# Location of config file. 21my $CONF_FILE = "$ENV{HOME}/.recoverrc"; 22my $HIST_FILE = "$ENV{HOME}/.recover.hist"; 23 24######################################################################## 25### Queries needed to gather files from directory. 26######################################################################## 27 28my %queries = ( 29 'postgres' => { 30 'dir' => 31 "( 32 select 33 distinct on (name) 34 Filename.name, 35 Path.path, 36 File.lstat, 37 File.fileid, 38 File.fileindex, 39 Job.jobtdate - ? as visible, 40 Job.jobid 41 from 42 Path, 43 File, 44 Filename, 45 Job 46 where 47 clientid = ? and 48 Job.name = ? and 49 Job.jobtdate <= ? and 50 Path.path = ? and 51 File.pathid = Path.pathid and 52 Filename.filenameid = File.filenameid and 53 Filename.name != '' and 54 File.jobid = Job.jobid 55 order by 56 name, 57 jobid desc 58 ) 59 union 60 ( 61 select 62 distinct on (name) 63 substring(Path.path from ? + 1) as name, 64 substring(Path.path from 1 for ?) as path, 65 File.lstat, 66 File.fileid, 67 File.fileindex, 68 Job.jobtdate - ? as visible, 69 Job.jobid 70 from 71 Path, 72 File, 73 Filename, 74 Job 75 where 76 clientid = ? and 77 Job.name = ? and 78 Job.jobtdate <= ? and 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 ~ ('^' || ? || '[^/]*/\$') 84 order by 85 name, 86 jobid desc 87 ) 88 order by 89 name 90 ", 91 'sel' => 92 "( 93 select 94 distinct on (name) 95 Path.path || Filename.name as name, 96 File.fileid, 97 File.lstat, 98 File.fileindex, 99 Job.jobid 100 from 101 Path, 102 File, 103 Filename, 104 Job 105 where 106 clientid = ? and 107 Job.name = ? and 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 115 order by 116 name, jobid desc 117 ) 118 union 119 ( 120 select 121 distinct on (name) 122 Path.path as name, 123 File.fileid, 124 File.lstat, 125 File.fileindex, 126 Job.jobid 127 from 128 Path, 129 File, 130 Filename, 131 Job 132 where 133 clientid = ? and 134 Job.name = ? and 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 ? || '%' 142 order by 143 name, jobid desc 144 ) 145 ", 146 'cache' => 147 "select 148 distinct on (path, name) 149 Path.path, 150 Filename.name, 151 File.fileid, 152 File.lstat, 153 File.fileindex, 154 Job.jobtdate - ? as visible, 155 Job.jobid 156 from 157 Path, 158 File, 159 Filename, 160 Job 161 where 162 clientid = ? and 163 Job.name = ? and 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 169 order by 170 path, name, jobid desc 171 ", 172 'ver' => 173 "select 174 Path.path, 175 Filename.name, 176 File.fileid, 177 File.fileindex, 178 File.lstat, 179 Job.jobtdate, 180 Job.jobid, 181 Job.jobtdate - ? as visible, 182 Media.volumename 183 from 184 Job, Path, Filename, File, JobMedia, Media 185 where 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 194 Path.path = ? and 195 Filename.name = ? and 196 Job.clientid = ? and 197 Job.name = ? 198 order by job 199 " 200 }, 201 'mysql' => { 202 'dir' => 203 " 204 ( 205 select 206 distinct(Filename.name), 207 Path.path, 208 File.lstat, 209 File.fileid, 210 File.fileindex, 211 Job.jobtdate - ? as visible, 212 Job.jobid 213 from 214 Path, 215 File, 216 Filename, 217 Job 218 where 219 clientid = ? and 220 Job.name = ? and 221 Job.jobtdate <= ? and 222 Path.path = ? and 223 File.pathid = Path.pathid and 224 Filename.filenameid = File.filenameid and 225 Filename.name != '' and 226 File.jobid = Job.jobid 227 group by 228 name 229 order by 230 name, 231 jobid desc 232 ) 233 union 234 ( 235 select 236 distinct(substring(Path.path from ? + 1)) as name, 237 substring(Path.path from 1 for ?) as path, 238 File.lstat, 239 File.fileid, 240 File.fileindex, 241 Job.jobtdate - ? as visible, 242 Job.jobid 243 from 244 Path, 245 File, 246 Filename, 247 Job 248 where 249 clientid = ? and 250 Job.name = ? and 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('^', ?, '[^/]*/\$') 257 group by 258 name 259 order by 260 name, 261 jobid desc 262 ) 263 order by 264 name 265 ", 266 'sel' => 267 " 268 ( 269 select 270 distinct(concat(Path.path, Filename.name)) as name, 271 File.fileid, 272 File.lstat, 273 File.fileindex, 274 Job.jobid 275 from 276 Path, 277 File, 278 Filename, 279 Job 280 where 281 Job.clientid = ? and 282 Job.name = ? and 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 290 group by 291 path, name 292 order by 293 name, 294 jobid desc 295 ) 296 union 297 ( 298 select 299 distinct(Path.path) as name, 300 File.fileid, 301 File.lstat, 302 File.fileindex, 303 Job.jobid 304 from 305 Path, 306 File, 307 Filename, 308 Job 309 where 310 Job.clientid = ? and 311 Job.name = ? and 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(?, '%') 319 group by 320 path 321 order by 322 name, 323 jobid desc 324 ) 325 ", 326 'cache' => 327 "select 328 distinct path, 329 Filename.name, 330 File.fileid, 331 File.lstat, 332 File.fileindex, 333 Job.jobtdate - ? as visible, 334 Job.jobid 335 from 336 Path, 337 File, 338 Filename, 339 Job 340 where 341 clientid = ? and 342 Job.name = ? and 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 348 group by 349 path, name 350 order by 351 path, name, jobid desc 352 ", 353 'ver' => 354 "select 355 Path.path, 356 Filename.name, 357 File.fileid, 358 File.fileindex, 359 File.lstat, 360 Job.jobtdate, 361 Job.jobid, 362 Job.jobtdate - ? as visible, 363 Media.volumename 364 from 365 Job, Path, Filename, File, JobMedia, Media 366 where 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 375 Path.path = ? and 376 Filename.name = ? and 377 Job.clientid = ? and 378 Job.name = ? 379 order by job 380 " 381 } 382); 383 384############################################################################ 385### Command lists for help and file completion 386############################################################################ 387 388my %COMMANDS = ( 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.' 408); 409 410my %SHOW = ( 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.' 417); 418 419############################################################################## 420### Read config and command line. 421############################################################################## 422 423my %catalogs; 424my $catalog; # Current catalog 425 426## Globals 427 428my %restore; 429my $rnum = 0; 430my $rbytes = 0; 431my $debug = 0; 432my $verbose = 0; 433my $rtime; 434my $cwd; 435my $lwd; 436my $files; 437my $restore_to = '/'; 438my $start_dir; 439my $preload; 440my $dircache = {}; 441my $usecache = 1; 442 443=head1 SYNTAX 444 445B<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>] 447 448B<recover.pl> [B<-h>] 449 450Most of the command line arguments can be specified in the init file 451B<$HOME/.recoverrc> (see CONFIG FILE FORMAT below). The command 452line arguments will override the options in the init file. If no 453I<catalogname> is specified, the first one found in the init file will 454be used. 455 456=head1 DESCRIPTION 457 458B<recover.pl> will read the specified catalog and provide a shell like 459environment from which a time based view of the specified client/jobname 460and be exampled and selected for restoration. 461 462The command line option B<-b> specified the DBI compatible connect 463script to use when connecting to the catalog database. The B<-c> and 464B<-j> options specify the client and jobname respectively to view from 465the catalog database. The B<-i> option will set the initial directory 466you are viewing to the specified directory. if B<-i> is not specified, 467it will default to /. You can set the initial time to view the catalog 468from using the B<-t> option. 469 470The B<-p> option will pre-load the entire catalog into memory. This 471could take a lot of memory, so use it with caution. 472 473The B<-d> option turns on debugging and the B<-v> option turns on 474verbose output. 475 476By specifying a I<catalogname>, the default options for connecting to 477the catalog database will be taken from the section of the init file 478specified by that name. 479 480The B<-h> option will display this document. 481 482In order for this program to have a chance of not being painfully slow, 483the following indexs should be added to your database. 484 485B<CREATE INDEX file_pathid_idx on file(pathid);> 486 487B<CREATE INDEX file_filenameid_idx on file(filenameid);> 488 489=cut 490 491my $vars = {}; 492getopts("c:b:hi:j:pt:vd", $vars) || die "Usage: bad arguments\n"; 493 494if ($vars->{'h'}) { 495 system("perldoc $0"); 496 exit; 497} 498 499$preload = $vars->{'p'} if ($vars->{'p'}); 500$debug = $vars->{'d'} if ($vars->{'d'}); 501$verbose = $vars->{'v'} if ($vars->{'v'}); 502 503# Set initial time to view the catalog 504 505if ($vars->{'t'}) { 506 $rtime = parsedate($vars->{'t'}, FUZZY => 1, PREFER_PAST => 1); 507} 508else { 509 $rtime = time(); 510} 511 512my $dbconnect; 513my $username = ""; 514my $password = ""; 515my $db; 516my $client; 517my $jobname; 518my $jobs; 519my $ftime; 520 521my $cstr; 522 523# Read config file (if available). 524 525&read_config($CONF_FILE); 526 527# Set defaults 528 529$catalog = $ARGV[0] if (@ARGV); 530 531if ($catalog) { 532 $cstr = ${catalogs{$catalog}}->{'client'} 533 if (${catalogs{$catalog}}->{'client'}); 534 535 $jobname = $catalogs{$catalog}->{'jobname'} 536 if ($catalogs{$catalog}->{'jobname'}); 537 538 $dbconnect = $catalogs{$catalog}->{'dbconnect'} 539 if ($catalogs{$catalog}->{'dbconnect'}); 540 541 $username = $catalogs{$catalog}->{'username'} 542 if ($catalogs{$catalog}->{'username'}); 543 544 $password = $catalogs{$catalog}->{'password'} 545 if ($catalogs{$catalog}->{'password'}); 546 547 $start_dir = $catalogs{$catalog}->{'cd'} 548 if ($catalogs{$catalog}->{'cd'}); 549 550 $preload = $catalogs{$catalog}->{'preload'} 551 if ($catalogs{$catalog}->{'preload'} && !defined($vars->{'p'})); 552 553 $verbose = $catalogs{$catalog}->{'verbose'} 554 if ($catalogs{$catalog}->{'verbose'} && !defined($vars->{'v'})); 555 556 $debug = $catalogs{$catalog}->{'debug'} 557 if ($catalogs{$catalog}->{'debug'} && !defined($vars->{'d'})); 558} 559 560#### Command line overries config file 561 562$start_dir = $vars->{'i'} if ($vars->{'i'}); 563$start_dir = '/' if (!$start_dir); 564 565$start_dir .= '/' if (substr($start_dir, length($start_dir) - 1, 1) ne '/'); 566 567if ($vars->{'b'}) { 568 $dbconnect = $vars->{'b'}; 569} 570 571die "You must supply a db connect string.\n" if (!defined($dbconnect)); 572 573if ($dbconnect =~ /^dbi:Pg/) { 574 $db = 'postgres'; 575} 576elsif ($dbconnect =~ /^dbi:mysql/) { 577 $db = 'mysql'; 578} 579else { 580 die "Unknown database type specified in $dbconnect\n"; 581} 582 583# Initialize database connection 584 585print STDERR "DBG: Connect using: $dbconnect\n" if ($debug); 586 587my $dbh = DBI->connect($dbconnect, $username, $password) || 588 die "Can't open bacula database\nDatabase connect string '$dbconnect'"; 589 590die "Client id required.\n" if (!($cstr || $vars->{'c'})); 591 592$cstr = $vars->{'c'} if ($vars->{'c'}); 593$client = &lookup_client($cstr); 594 595# Set job information 596$jobname = $vars->{'j'} if ($vars->{'j'}); 597 598die "You need to specify a job name.\n" if (!$jobname); 599 600&setjob; 601 602die "Failed to set client\n" if (!$client); 603 604# Prepare our query 605my $dir_sth = $dbh->prepare($queries{$db}->{'dir'}) 606 || die "Can't prepare $queries{$db}->{'dir'}\n"; 607 608my $sel_sth = $dbh->prepare($queries{$db}->{'sel'}) 609 || die "Can't prepare $queries{$db}->{'sel'}\n"; 610 611my $ver_sth = $dbh->prepare($queries{$db}->{'ver'}) 612 || die "Can't prepare $queries{$db}->{'ver'}\n"; 613 614my $clients; 615 616# Initialize readline. 617my $term = new Term::ReadLine('Bacula Recover'); 618$term->ornaments(0); 619 620my $readline = $term->ReadLine; 621my $tty_attribs = $term->Attribs; 622 623# Needed for base64 decode 624 625my @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', '+', '/' 631); 632my @base64_map = (0) x 128; 633 634for (my $i=0; $i<64; $i++) { 635 $base64_map[ord($base64_digits[$i])] = $i; 636} 637 638############################################################################## 639### Support routines 640############################################################################## 641 642=head1 FILES 643 644B<$HOME/.recoverrc> Configuration file for B<recover.pl>. 645 646=head1 CONFIG FILE FORMAT 647 648The config file will allow you to specify the defaults for your 649catalog(s). Each catalog definition starts with B<[>I<catalogname>B<]>. 650Blank lines and lines starting with # are ignored. 651 652The first catalog specified will be used as the default catalog. 653 654All values are specified in I<item> B<=> I<value> format. You can 655specify the following I<item>s for each catalog. 656 657=cut 658 659sub read_config { 660 my $conf_file = shift; 661 my $c; 662 663 # No nothing if config file can't be read. 664 665 if (-r $conf_file) { 666 open(CONF, "<$conf_file") || die "$!: Can't open $conf_file\n"; 667 668 while (<CONF>) { 669 chomp; 670 # Skip comments and blank links 671 next if (/^\s*#/); 672 next if (/^\s*$/); 673 674 if (/^\[(\w+)\]$/) { 675 $c = $1; 676 $catalog = $c if (!$catalog); 677 678 if ($catalogs{$c}) { 679 die "Duplicate catalog definition in $conf_file\n"; 680 } 681 682 $catalogs{$c} = {}; 683 } 684 elsif (!$c) { 685 die "Conf file must start with catalog definition [catname]\n"; 686 } 687 else { 688 689 if (/^(\w+)\s*=\s*(.*)/) { 690 my $item = $1; 691 my $value = $2; 692 693=head2 client 694 695The name of the default client to view when connecting to this 696catalog. This can be changed later with the B<client> command. 697 698=cut 699 700 if ($item eq 'client') { 701 $catalogs{$c}->{'client'} = $value; 702 } 703 704=head2 dbconnect 705 706The DBI compatible database string to use to connect to this catalog. 707 708=over 4 709 710=item B<example:> 711 712dbi:Pg:dbname=bacula;host=backuphost 713 714=back 715 716=cut 717 elsif ($item eq 'dbconnect') { 718 $catalogs{$c}->{'dbconnect'} = $value; 719 } 720 721=head2 jobname 722 723The name of the default job to view when connecting to the catalog. This 724can be changed later with the B<client> command. 725 726=cut 727 elsif ($item eq 'jobname') { 728 $catalogs{$c}->{'jobname'} = $value; 729 } 730 731=head2 password 732 733The password to use when connecing to the catalog database. 734 735=cut 736 elsif ($item eq 'password') { 737 $catalogs{$c}->{'password'} = $value; 738 } 739 740=head2 preload 741 742Set the preload flag. A preload flag of 1 or on will load the entire 743catalog when recover.pl is start. This is a memory hog, so use with 744caution. 745 746=cut 747 elsif ($item eq 'preload') { 748 749 if ($value =~ /^(1|on)$/i) { 750 $catalogs{$c}->{'preload'} = 1; 751 } 752 elsif ($value =~ /^(0|off)$/i) { 753 $catalogs{$c}->{'preload'} = 0; 754 } 755 else { 756 die "$value: Unknown value for preload.\n"; 757 } 758 759 } 760 761=head2 username 762 763The username to use when connecing to the catalog database. 764 765=cut 766 elsif ($item eq 'username') { 767 $catalogs{$c}->{'username'} = $value; 768 } 769 else { 770 die "Unknown opton $item in $conf_file.\n"; 771 } 772 773 } 774 else { 775 die "Bad line $_ in $conf_file.\n"; 776 } 777 778 } 779 780 } 781 782 close(CONF); 783 } 784 785} 786 787sub create_file_entry { 788 my $name = shift; 789 my $fileid = shift; 790 my $fileindex = shift; 791 my $jobid = shift; 792 my $visible = shift; 793 my $lstat = shift; 794 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); 801 802 my $data = { 803 fileid => $fileid, 804 fileindex => $fileindex, 805 jobid => $jobid, 806 visible => ($visible >= 0) ? 1 : 0 807 }; 808 809 # decode file stat 810 my @stat = (); 811 812 foreach my $s (split(' ', $lstat)) { 813 print STDERR "DBG: Add $s to stat array.\n" if ($debug); 814 push(@stat, from_base64($s)); 815 } 816 817 $data->{'lstat'} = { 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] 834 }; 835 836 # Create mode string. 837 my $sstr = &mode2str($stat[2]); 838 $data->{'lstat'}->{'statstr'} = $sstr; 839 return $data; 840} 841# Read directory data, return hash reference. 842 843sub fetch_dir { 844 my $dir = shift; 845 846 return $dircache->{$dir} if ($dircache->{$dir}); 847 848 print "$dir not cached, fetching from database.\n" if ($verbose); 849 my $data = {}; 850 my $fmax = 0; 851 852 my $dl = length($dir); 853 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); 865 866 print STDERR "DBG: Execute - $queries{$db}->{'dir'}\n" if ($debug); 867 $dir_sth->execute( 868 $ftime, 869 $client, 870 $jobname, 871 $rtime, 872 $dir, 873 $dl, $dl, 874 $ftime, 875 $client, 876 $jobname, 877 $rtime, 878 $dir 879 ) || die "Can't execute $queries{$db}->{'dir'}\n"; 880 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); 886 887 $data->{$file} = &create_file_entry( 888 $file, 889 $ref->{'fileid'}, 890 $ref->{'fileindex'}, 891 $ref->{'jobid'}, 892 $ref->{'visible'}, 893 $ref->{'lstat'} 894 ); 895 } 896 897 return undef if (!$fmax); 898 899 $dircache->{$dir} = $data if ($usecache); 900 return $data; 901} 902 903sub cache_catalog { 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"; 910 911 print "Query complete, building catalog cache...\n" if ($verbose); 912 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); 917 918 next if ($dir eq '/' and $file eq ''); # Skip data for / 919 920 # Rearrange directory 921 922 if ($file eq '' and $dir =~ m|(.*/)([^/]+/)$|) { 923 $dir = $1; 924 $file = $2; 925 } 926 927 my $data = &create_file_entry( 928 $file, 929 $ref->{'fileid'}, 930 $ref->{'fileindex'}, 931 $ref->{'jobid'}, 932 $ref->{'visible'}, 933 $ref->{'lstat'} 934 ); 935 936 $dircache->{$dir} = {} if (!$dircache->{$dir}); 937 $dircache->{$dir}->{$file} = $data; 938 } 939 940 $sth->finish(); 941} 942 943# Break a path up into dir and file. 944 945sub path_parts { 946 my $path = shift; 947 my $fqdir; 948 my $dir; 949 my $file; 950 951 if (substr($path, 0, 1) eq '/') { 952 953 # Find dir vs. file 954 if ($path =~ m|^(/.*/)([^/]*$)|) { 955 $fqdir = $dir = $1; 956 $file = $2; 957 } 958 else { # Must be in / 959 $fqdir = $dir = '/'; 960 $file = substr($path, 1); 961 } 962 963 print STDERR "DBG: / Dir - $dir; file = $file\n" if ($debug); 964 } 965 # relative path 966 elsif ($path =~ m|^(.*/)([^/]*)$|) { 967 $fqdir = "$cwd$1"; 968 $dir = $1; 969 $file = $2; 970 print STDERR "DBG: Dir - $dir; file = $file\n" if ($debug); 971 } 972 # File is in our current directory. 973 else { 974 $fqdir = $cwd; 975 $dir = ''; 976 $file = $path; 977 print STDERR "DBG: Set dir to $dir\n" if ($debug); 978 } 979 980 return ($fqdir, $dir, $file); 981} 982 983sub lookup_client { 984 my $c = shift; 985 986 if (!$clients) { 987 $clients = {}; 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"; 991 992 while (my $ref = $sth->fetchrow_hashref) { 993 $clients->{$ref->{'name'}} = $ref->{'clientid'}; 994 } 995 996 $sth->finish; 997 } 998 999 if ($c !~ /^\d+$/) { 1000 1001 if ($clients->{$c}) { 1002 $c = $clients->{$c}; 1003 } 1004 else { 1005 warn "Could not find client $c\n"; 1006 $c = $client; 1007 } 1008 1009 } 1010 1011 return $c; 1012} 1013 1014sub setjob { 1015 1016 if (!$jobs) { 1017 $jobs = {}; 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"; 1021 1022 while (my $ref = $sth->fetchrow_hashref) { 1023 $jobs->{$$ref{'name'}} = $$ref{'name'}; 1024 } 1025 1026 $sth->finish; 1027 } 1028 1029 my $query = "select 1030 jobtdate 1031 from 1032 Job 1033 where 1034 jobtdate <= $rtime and 1035 name = '$jobname' and 1036 level = 'F' 1037 order by jobtdate desc 1038 limit 1 1039 "; 1040 1041 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n"; 1042 $sth->execute || die "Can't execute $query\n"; 1043 1044 if ($sth->rows == 1) { 1045 my $ref = $sth->fetchrow_hashref; 1046 $ftime = $$ref{jobtdate}; 1047 } 1048 else { 1049 warn "Could not find full backup. Setting full time to 0.\n"; 1050 $ftime = 0; 1051 } 1052 1053 $sth->finish; 1054} 1055 1056sub select_files { 1057 my $mark = shift; 1058 my $opts = shift; 1059 my $dir = shift; 1060 my @flist = @_; 1061 1062 if (!@flist) { 1063 1064 if ($cwd eq '/') { 1065 my $finfo = &fetch_dir('/'); 1066 @flist = keys %$finfo; 1067 } 1068 else { 1069 @flist = ($cwd); 1070 } 1071 1072 } 1073 1074 foreach my $f (@flist) { 1075 $f =~ s|/+$||; 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); 1079 1080 if (!$finfo->{$file}) { 1081 1082 if (!$finfo->{"$file/"}) { 1083 warn "$f: File not found.\n"; 1084 next; 1085 } 1086 1087 $file .= '/'; 1088 } 1089 1090 my $info = $finfo->{$file}; 1091 1092 my $fid = $info->{'fileid'}; 1093 my $fidx = $info->{'fileindex'}; 1094 my $jid = $info->{'jobid'}; 1095 my $size = $info->{'lstat'}->{'st_size'}; 1096 1097 if ($opts->{'all'} || $info->{'visible'}) { 1098 print STDERR "DBG: $file - $size bytes\n" 1099 if ($debug); 1100 1101 if ($mark) { 1102 1103 if (!$restore{$fid}) { 1104 print "Adding $fqdir$file\n" if (!$opts->{'quiet'}); 1105 $restore{$fid} = [$jid, $fidx]; 1106 $rnum++; 1107 $rbytes += $size; 1108 } 1109 1110 } 1111 else { 1112 1113 if ($restore{$fid}) { 1114 print "Removing $fqdir$file\n" if (!$opts->{'quiet'}); 1115 delete $restore{$fid}; 1116 $rnum--; 1117 $rbytes -= $size; 1118 } 1119 1120 } 1121 1122 if ($file =~ m|/$|) { 1123 1124 # Use preloaded files if we already retrieved them. 1125 if ($preload) { 1126 my $newdir = "$dir$file"; 1127 my $finfo = &fetch_dir($newdir); 1128 &select_files($mark, $opts, $newdir, keys %$finfo); 1129 next; 1130 } 1131 else { 1132 my $newdir = "$fqdir$file"; 1133 my $begin = ($opts->{'all'}) ? 0 : $ftime; 1134 1135 print STDERR "DBG: Execute - $queries{$db}->{'sel'}\n" 1136 if ($debug); 1137 1138 $sel_sth->execute( 1139 $client, 1140 $jobname, 1141 $rtime, 1142 $begin, 1143 $newdir, 1144 $client, 1145 $jobname, 1146 $rtime, 1147 $begin, 1148 $newdir 1149 ) || die "Can't execute $queries{$db}->{'sel'}\n"; 1150 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]); 1158 1159 if ($mark) { 1160 1161 if (!$restore{$fid}) { 1162 print "Adding $file\n" if (!$opts->{'quiet'}); 1163 $restore{$fid} = [$jid, $fidx]; 1164 $rnum++; 1165 $rbytes += $size; 1166 } 1167 1168 } 1169 else { 1170 1171 if ($restore{$fid}) { 1172 print "Removing $file\n" if (!$opts->{'quiet'}); 1173 delete $restore{$fid}; 1174 $rnum--; 1175 $rbytes -= $size; 1176 } 1177 1178 } 1179 1180 } 1181 1182 } 1183 1184 } 1185 1186 } 1187 1188 } 1189 1190} 1191 1192# Expand shell wildcards 1193 1194sub expand_files { 1195 my $path = shift; 1196 my ($fqdir, $dir, $file) = &path_parts($path); 1197 my $finfo = &fetch_dir($fqdir); 1198 return ($path) if (!$finfo); 1199 1200 my $pat = "^$file\$"; 1201 1202 # Add / for dir match 1203 my $dpat = $file; 1204 $dpat =~ s|/+$||; 1205 $dpat = "^$dpat/\$"; 1206 1207 my @match; 1208 1209 $pat =~ s/\./\\./g; 1210 $dpat =~ s/\./\\./g; 1211 $pat =~ s/\?/./g; 1212 $dpat =~ s/\?/./g; 1213 $pat =~ s/\*/.*/g; 1214 $dpat =~ s/\*/.*/g; 1215 1216 foreach my $f (sort keys %$finfo) { 1217 1218 if ($f =~ /$pat/) { 1219 push (@match, ($fqdir eq $cwd) ? $f : "$fqdir$f"); 1220 } 1221 elsif ($f =~ /$dpat/) { 1222 push (@match, ($fqdir eq $cwd) ? $f : "$fqdir$f"); 1223 } 1224 1225 } 1226 1227 return ($path) if (!@match); 1228 return @match; 1229} 1230 1231sub expand_dirs { 1232 my $path = shift; 1233 my ($fqdir, $dir, $file) = &path_parts($path, 1); 1234 1235 print STDERR "Expand $path\n" if ($debug); 1236 1237 my $finfo = &fetch_dir($fqdir); 1238 return ($path) if (!$finfo); 1239 1240 $file =~ s|/+$||; 1241 1242 my $pat = "^$file/\$"; 1243 my @match; 1244 1245 $pat =~ s/\./\\./g; 1246 $pat =~ s/\?/./g; 1247 $pat =~ s/\*/.*/g; 1248 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/); 1252 } 1253 1254 return ($path) if (!@match); 1255 return @match; 1256} 1257 1258sub mode2str { 1259 my $mode = shift; 1260 my $sstr = ''; 1261 1262 if (S_ISDIR($mode)) { 1263 $sstr = 'd'; 1264 } 1265 elsif (S_ISCHR($mode)) { 1266 $sstr = 'c'; 1267 } 1268 elsif (S_ISBLK($mode)) { 1269 $sstr = 'b'; 1270 } 1271 elsif (S_ISREG($mode)) { 1272 $sstr = '-'; 1273 } 1274 elsif (S_ISFIFO($mode)) { 1275 $sstr = 'f'; 1276 } 1277 elsif (S_ISLNK($mode)) { 1278 $sstr = 'l'; 1279 } 1280 elsif (S_ISSOCK($mode)) { 1281 $sstr = 's'; 1282 } 1283 else { 1284 $sstr = '?'; 1285 } 1286 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' : '-'); 1302 1303 return $sstr; 1304} 1305 1306# Base 64 decoder 1307# Algorithm copied from bacula source 1308 1309sub from_base64 { 1310 my $where = shift; 1311 my $val = 0; 1312 my $i = 0; 1313 my $neg = 0; 1314 1315 if (substr($where, 0, 1) eq '-') { 1316 $neg = 1; 1317 $where = substr($where, 1); 1318 } 1319 1320 while ($where ne '') { 1321 $val <<= 6; 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); 1326 } 1327 1328 return $val; 1329} 1330 1331### Command completion code 1332 1333sub get_match { 1334 my @m = @_; 1335 my $r = ''; 1336 1337 for (my $i = 0, my $matched = 1; $i < length($m[0]) && $matched; $i++) { 1338 my $c = substr($m[0], $i, 1); 1339 1340 for (my $j = 1; $j < @m; $j++) { 1341 1342 if ($c ne substr($m[$j], $i, 1)) { 1343 $matched = 0; 1344 last; 1345 } 1346 1347 } 1348 1349 $r .= $c if ($matched); 1350 } 1351 1352 return $r; 1353} 1354 1355sub complete { 1356 my $text = shift; 1357 my $line = shift; 1358 my $start = shift; 1359 my $end = shift; 1360 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" 1364 if ($debug); 1365 1366 # Complete command if we are at start of line. 1367 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; 1373 } 1374 else { 1375 # Count arguments 1376 my $cstr = $line; 1377 $cstr =~ s/^\s+//; # Remove leading spaces 1378 1379 my ($cmd, @args) = shellwords($cstr); 1380 return () if (!defined($cmd)); 1381 1382 # Complete dirs for cd 1383 if ($cmd eq 'cd') { 1384 return () if (@args > 1); 1385 return &complete_files($text, 1); 1386 } 1387 # Complete files/dirs for info and ls 1388 elsif ($cmd =~ /^(add|delete|info|ls|mark|unmark|versions)$/) { 1389 return &complete_files($text, 0); 1390 } 1391 # Complete clients for client 1392 elsif ($cmd eq 'client') { 1393 return () if (@args > 2); 1394 my $pat = $text; 1395 $pat =~ s/\./\\./g; 1396 my @flist; 1397 1398 print STDERR "DBG: " . (@args) . " arguments found.\n" if ($debug); 1399 1400 if (@args < 1 || (@args == 1 and $line =~ /[^\s]$/)) { 1401 @flist = grep (/^$pat/, sort keys %$clients); 1402 } 1403 else { 1404 @flist = grep (/^$pat/, sort keys %$jobs); 1405 } 1406 1407 return () if (!@flist); 1408 my $match = (@flist > 1) ? &get_match(@flist) : ''; 1409 1410 #return $match, map {s/ /\\ /g; $_} @flist; 1411 return $match, @flist; 1412 } 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; 1421 } 1422 elsif ($cmd =~ /^(bsr|bootstrap|relocate)$/) { 1423 $tty_attribs->{completion_entry_function} = 1424 $tty_attribs->{filename_completion_function}; 1425 } 1426 1427 } 1428 1429 return (); 1430} 1431 1432sub complete_files { 1433 my $path = shift; 1434 my $dironly = shift; 1435 my $finfo; 1436 my @flist; 1437 1438 my ($fqdir, $dir, $pat) = &path_parts($path, 1); 1439 1440 $pat =~ s/([.\[\]\\])/\\$1/g; 1441 # First check for absolute name. 1442 1443 $finfo = &fetch_dir($fqdir); 1444 print STDERR "DBG: " . join(', ', keys %$finfo) . "\n" if ($debug); 1445 return () if (!$finfo); # Nothing if dir not found. 1446 1447 if ($dironly) { 1448 @flist = grep (m|^$pat.*/$|, sort keys %$finfo); 1449 } 1450 else { 1451 @flist = grep (/^$pat/, sort keys %$finfo); 1452 } 1453 1454 return undef if (!@flist); 1455 1456 print STDERR "DBG: Files found\n" if ($debug); 1457 1458 if (@flist == 1 && $flist[0] =~ m|/$|) { 1459 $tty_attribs->{'completion_append_character'} = ''; 1460 } 1461 1462 @flist = map {s/ /\\ /g; ($fqdir eq $cwd) ? $_ : "$dir$_"} @flist; 1463 my $match = (@flist > 1) ? &get_match(@flist) : ''; 1464 1465 print STDERR "DBG: Dir - $dir; cwd - $cwd\n" if ($debug); 1466 # Fill in dir if necessary. 1467 return $match, @flist; 1468} 1469 1470sub nocomplete { 1471 return (); 1472} 1473 1474# subroutine to create printf format for long listing of ls 1475 1476sub long_fmt { 1477 my $flist = shift; 1478 my $fmax = 0; 1479 my $lmax = 0; 1480 my $umax = 0; 1481 my $gmax = 0; 1482 my $smax = 0; 1483 1484 foreach my $f (@$flist) { 1485 my $file = $f->[0]; 1486 my $info = $f->[1]; 1487 my $lstat = $info->{'lstat'}; 1488 1489 my $l = length($file); 1490 $fmax = $l if ($l > $fmax); 1491 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); 1500 } 1501 1502 return "%s %${lmax}d %${umax}d %${gmax}d %${smax}d %s %s\n"; 1503} 1504 1505sub print_by_cols { 1506 my @list = @_; 1507 my $l = @list; 1508 my $w = $term->get_screen_size; 1509 my @wds = (1); 1510 my $m = $w/3 + 1; 1511 my $max_cols = ($m < @list) ? $w : @list; 1512 my $fpc = 1; 1513 my $cols = 1; 1514 1515 print STDERR "Need to print $l files\n" if ($debug); 1516 1517 while($max_cols > 1) { 1518 my $used = 0; 1519 1520 # Initialize array of widths 1521 @wds = 0 x $max_cols; 1522 1523 for ($cols = 0; $cols < $max_cols && $used < $w; $cols++) { 1524 my $cw = 0; 1525 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); 1529 } 1530 1531 $wds[$cols] = $cw; 1532 $used += $cw; 1533 print STDERR "DBG: Total so far is $used\n" if ($debug); 1534 1535 if ($used >= $w) { 1536 $cols++; 1537 last; 1538 } 1539 1540 $used += 3; 1541 } 1542 1543 print STDERR "DBG: $cols of $max_cols columns uses $used space.\n" 1544 if ($debug); 1545 1546 print STDERR "DBG: Print $fpc files per column\n" 1547 if ($debug); 1548 1549 last if ($used <= $w && $cols == $max_cols); 1550 $fpc = int($l/$cols); 1551 $fpc++ if ($l % $cols); 1552 $max_cols = $cols - 1; 1553 } 1554 1555 if ($max_cols == 1) { 1556 $cols = 1; 1557 $fpc = $l; 1558 } 1559 1560 print STDERR "Print out $fpc rows with $cols columns\n" 1561 if ($debug); 1562 1563 for (my $i = 0; $i < $fpc; $i++) { 1564 1565 for (my $j = $i; $j < $fpc*$cols; $j += $fpc) { 1566 my $cw = $wds[($j - $i)/$fpc]; 1567 my $fmt = "%s%-${cw}s"; 1568 my $file; 1569 my $r; 1570 1571 if ($j < @list) { 1572 $file = $list[$j]->[0]; 1573 my $fdata = $list[$j]->[1]; 1574 $r = ($restore{$fdata->{'fileid'}}) ? '+' : ' '; 1575 } 1576 else { 1577 $file = ''; 1578 $r = ' '; 1579 } 1580 1581 print ' ' if ($i != $j); 1582 printf $fmt, $r, $file; 1583 } 1584 1585 print "\n"; 1586 } 1587 1588} 1589 1590sub ls_date { 1591 my $seconds = shift; 1592 my $date; 1593 1594 if (abs(time() - $seconds) > 15724800) { 1595 $date = time2str('%b %e %Y', $seconds); 1596 } 1597 else { 1598 $date = time2str('%b %e %R', $seconds); 1599 } 1600 1601 return $date; 1602} 1603 1604# subroutine to load entire bacula database. 1605=head1 SHELL 1606 1607Once running, B<recover.pl> will present the user with a shell like 1608environment where file can be exampled and selected for recover. The 1609shell will provide command history and editing and if you have the 1610Gnu readline module installed on your system, it will also provide 1611command completion. When interacting with files, wildcards should work 1612as expected. 1613 1614The following commands are understood. 1615 1616=cut 1617 1618sub parse_command { 1619 my $cstr = shift; 1620 my @command; 1621 my $cmd; 1622 my @args; 1623 1624 # Nop on blank or commented lines 1625 return ('nop') if ($cstr =~ /^\s*$/); 1626 return ('nop') if ($cstr =~ /^\s*#/); 1627 1628 # Get rid of leading white space to make shellwords work better 1629 $cstr =~ s/^\s*//; 1630 1631 ($cmd, @args) = shellwords($cstr); 1632 1633 if (!defined($cmd)) { 1634 warn "Could not warse $cstr\n"; 1635 return ('nop'); 1636 } 1637 1638=head2 add [I<filelist>] 1639 1640Mark I<filelist> for recovery. If I<filelist> is not specified, mark all 1641files in the current directory. B<mark> is an alias for this command. 1642 1643=cut 1644 elsif ($cmd eq 'add' || $cmd eq 'mark') { 1645 my $options = {}; 1646 @ARGV = @args; 1647 1648 # Parse ls options 1649 my $vars = {}; 1650 getopts("aq", $vars) || return ('error', 'Add: Usage add [-q|-a] files'); 1651 $options->{'all'} = $vars->{'a'}; 1652 $options->{'quiet'} =$vars->{'q'}; 1653 1654 1655 @command = ('add', $options); 1656 1657 foreach my $a (@ARGV) { 1658 push(@command, &expand_files($a)); 1659 } 1660 1661 } 1662 1663=head2 bootstrap I<bootstrapfile> 1664 1665Create a bootstrap file suitable for use with the bacula B<bextract> 1666command. B<bsr> is an alias for this command. 1667 1668=cut 1669 elsif ($cmd eq 'bootstrap' || $cmd eq 'bsr') { 1670 return ('error', 'bootstrap takes single argument (file to write to)') 1671 if (@args != 1); 1672 @command = ('bootstrap', $args[0]); 1673 } 1674 1675=head2 cd I<directory> 1676 1677Allows you to set your current directory. This command understands . for 1678the current directory and .. for the parent. Also, cd - will change you 1679back to the previous directory you were in. 1680 1681=cut 1682 elsif ($cmd eq 'cd') { 1683 # Cd with no args goes to / 1684 @args = ('/') if (!@args); 1685 1686 if (@args != 1) { 1687 return ('error', 'Bad cd. cd requires 1 and only 1 argument.'); 1688 } 1689 1690 my $todir = $args[0]; 1691 1692 # cd - should cd to previous directory. It is handled later. 1693 return ('cd', '-') if ($todir eq '-'); 1694 1695 # Expand wilecards 1696 my @e = expand_dirs($todir); 1697 1698 if (@e > 1) { 1699 return ('error', 'Bad cd. Wildcard expands to more than 1 dir.'); 1700 } 1701 1702 $todir = $e[0]; 1703 1704 print STDERR "Initial target is $todir\n" if ($debug); 1705 1706 # remove prepended . 1707 1708 while ($todir =~ m|^\./(.*)|) { 1709 $todir = $1; 1710 $todir = '.' if (!$todir); 1711 } 1712 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); 1716 1717 # Now deal with .. 1718 my $prefix = $cwd; 1719 1720 while ($todir =~ m|^\.\./(.*)|) { 1721 $todir = $1; 1722 print STDERR "DBG: ../ found, new todir - $todir\n" if ($debug); 1723 $prefix =~ s|/[^/]*/$|/|; 1724 } 1725 1726 if ($todir eq '..') { 1727 $prefix =~ s|/[^/]*/$|/|; 1728 $todir = ''; 1729 } 1730 1731 print STDERR "target after .. processing is $todir\n" if ($debug); 1732 print STDERR "DBG: Final prefix - $prefix\n" if ($debug); 1733 1734 $todir = "$prefix$todir" if ($prefix ne $cwd); 1735 1736 print STDERR "DBG: todir after .. handling - $todir\n" if ($debug); 1737 1738 # Turn relative directories into absolute directories. 1739 1740 if (substr($todir, 0, 1) ne '/') { 1741 print STDERR "DBG: $todir has no leading /, prepend $cwd\n" if ($debug); 1742 $todir = "$cwd$todir"; 1743 } 1744 1745 # Make sure we have a trailing / 1746 1747 if (substr($todir, length($todir) - 1) ne '/') { 1748 print STDERR "DBG: No trailing /, append /\n" if ($debug); 1749 $todir .= '/'; 1750 } 1751 1752 @command = ('cd', $todir); 1753 } 1754 1755=head2 changetime I<timespec> 1756 1757This command changes the time used in generating the view of the 1758filesystem. Files that were backed up before the specified time 1759(optionally until the next full backup) will be the only files seen. 1760 1761The time can be specifed in almost any reasonable way. Here are a few 1762examples: 1763 1764=over 4 1765 1766=item 1/1/2006 1767 1768=item yesterday 1769 1770=item sunday 1771 1772=item 5 days ago 1773 1774=item last month 1775 1776=back 1777 1778=cut 1779 elsif ($cmd eq 'changetime') { 1780 @command = ($cmd, join(' ', @args)); 1781 } 1782 1783=head2 client I<clientname> I<jobname> 1784 1785Specify the client and jobname to view. 1786 1787=cut 1788 elsif ($cmd eq 'client') { 1789 1790 if (@args != 2) { 1791 return ('error', 'client takes a two arguments client-name job-name'); 1792 } 1793 1794 @command = ('client', @args); 1795 } 1796 1797=head2 debug 1798 1799Toggle debug flag. 1800 1801=cut 1802 elsif ($cmd eq 'debug') { 1803 @command = ('debug'); 1804 } 1805 1806=head2 delete [I<filelist>] 1807 1808Un-mark file that were previous marked for recovery. If I<filelist> is 1809not specified, mark all files in the current directory. B<unmark> is an 1810alias for this command. 1811 1812=cut 1813 elsif ($cmd eq 'delete' || $cmd eq 'unmark') { 1814 @command = ('delete'); 1815 1816 foreach my $a (@args) { 1817 push(@command, &expand_files($a)); 1818 } 1819 1820 } 1821 1822=head2 help 1823 1824Show list of command with brief description of what they do. 1825 1826=cut 1827 elsif ($cmd eq 'help') { 1828 @command = ('help'); 1829 } 1830 1831=head2 history 1832 1833Display command line history. B<h> is an alias for this command. 1834 1835=cut 1836 elsif ($cmd eq 'h' || $cmd eq 'history') { 1837 @command = ('history'); 1838 } 1839 1840=head2 info [I<filelist>] 1841 1842Display information about the specified files. The format of the 1843information provided is reminiscent of the bootstrap file. 1844 1845=cut 1846 elsif ($cmd eq 'info') { 1847 push(@command, 'info'); 1848 1849 foreach my $a (@args) { 1850 push(@command, &expand_files($a)); 1851 } 1852 1853 } 1854 1855=head2 ls [I<filelist>] 1856 1857This command will list the specified files (defaults to all files in 1858the current directory). Files are sorted alphabetically be default. It 1859understand the following options. 1860 1861=over 4 1862 1863=item -a 1864 1865Causes ls to list files even if they are only on backups preceding the 1866closest full backup to the currently selected date/time. 1867 1868=item -l 1869 1870List files in long format (like unix ls command). 1871 1872=item -r 1873 1874reverse direction of sort. 1875 1876=item -S 1877 1878Sort files by size. 1879 1880=item -t 1881 1882Sort files by time 1883 1884=back 1885 1886=cut 1887 elsif ($cmd eq 'ls' || $cmd eq 'dir' || $cmd eq 'll') { 1888 my $options = {}; 1889 @ARGV = @args; 1890 1891 # Parse ls options 1892 my $vars = {}; 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'); 1897 1898 $options->{'sort'} = 'time' if ($vars->{'t'}); 1899 1900 return ('error', 'Only one sort at a time allowed.') 1901 if ($options->{'sort'} && ($vars->{'S'})); 1902 1903 $options->{'sort'} = 'size' if ($vars->{'S'}); 1904 $options->{'sort'} = 'alpha' if (!$options->{'sort'}); 1905 1906 $options->{'sort'} = 'r' . $options->{'sort'} if ($vars->{'r'}); 1907 1908 @command = ('ls', $options); 1909 1910 foreach my $a (@ARGV) { 1911 push(@command, &expand_files($a)); 1912 } 1913 1914 } 1915 1916=head2 pwd 1917 1918Show current directory. 1919 1920=cut 1921 elsif ($cmd eq 'pwd') { 1922 @command = ('pwd'); 1923 } 1924 1925=head2 quit 1926 1927Exit program. 1928 1929B<q>, B<exit> and B<x> are all aliases for this command. 1930 1931=cut 1932 elsif ($cmd eq 'quit' || $cmd eq 'q' || $cmd eq 'exit' || $cmd eq 'x') { 1933 @command = ('quit'); 1934 } 1935 1936=head2 recover 1937 1938This command creates a table in the bacula catalog that case be used to 1939restore the selected files. It will also display the command to enter 1940into bconsole to start the restore. 1941 1942=cut 1943 elsif ($cmd eq 'recover') { 1944 @command = ('recover'); 1945 } 1946 1947=head2 relocate I<directory> 1948 1949Specify the directory to restore files to. Defaults to /. 1950 1951=cut 1952 elsif ($cmd eq 'relocate') { 1953 return ('error', 'relocate required a single directory to relocate to') 1954 if (@args != 1); 1955 1956 my $todir = $args[0]; 1957 $todir = `pwd` . $todir if (substr($todir, 0, 1) ne '/'); 1958 @command = ('relocate', $todir); 1959 } 1960 1961=head2 show I<item> 1962 1963Show various information about B<recover.pl>. The following items can be specified. 1964 1965=over 4 1966 1967=item cache 1968 1969Display's a list of cached directories. 1970 1971=item catalog 1972 1973Displays the name of the catalog we are talking to. 1974 1975=item client 1976 1977Display current client and job named that are being viewed. 1978 1979=item restore 1980 1981Display the number of files and size to be restored. 1982 1983=item volumes 1984 1985Display the volumes that will be required to perform a restore on the 1986selected files. 1987 1988=back 1989 1990=cut 1991 elsif ($cmd eq 'show') { 1992 return ('error', 'show takes a single argument') if (@args != 1); 1993 @command = ('show', $args[0]); 1994 } 1995 1996=head2 verbose 1997 1998Toggle verbose flag. 1999 2000=cut 2001 elsif ($cmd eq 'verbose') { 2002 @command = ('verbose'); 2003 } 2004 2005=head2 versions [I<filelist>] 2006 2007View all version of specified files available from the current 2008time. B<ver> is an alias for this command. 2009 2010=cut 2011 elsif ($cmd eq 'versions' || $cmd eq 'ver') { 2012 push(@command, 'versions'); 2013 2014 foreach my $a (@args) { 2015 push(@command, &expand_files($a)); 2016 } 2017 2018 } 2019 2020=head2 volumes 2021 2022Display the volumes that will be required to perform a restore on the 2023selected files. 2024 2025=cut 2026 elsif ($cmd eq 'volumes') { 2027 @command = ('volumes'); 2028 } 2029 else { 2030 @command = ('error', "$cmd: Unknown command"); 2031 } 2032 2033 return @command; 2034} 2035 2036############################################################################## 2037### Command processing 2038############################################################################## 2039 2040# Add files to restore list. 2041 2042sub cmd_add { 2043 my $opts = shift; 2044 my @flist = @_; 2045 2046 my $save_rnum = $rnum; 2047 &select_files(1, $opts, $cwd, @flist); 2048 print "" . ($rnum - $save_rnum) . " files marked for restore\n"; 2049} 2050 2051sub cmd_bootstrap { 2052 my $bsrfile = shift; 2053 my %jobs; 2054 my @media; 2055 my %bootstrap; 2056 2057 # Get list of job ids to restore from. 2058 2059 foreach my $fid (keys %restore) { 2060 $jobs{$restore{$fid}->[0]} = 1; 2061 } 2062 2063 my $jlist = join(', ', sort keys %jobs); 2064 2065 if (!$jlist) { 2066 print "Nothing to restore.\n"; 2067 return; 2068 } 2069 2070 # Read in media info 2071 2072 my $query = "select 2073 Job.jobid, 2074 volumename, 2075 mediatype, 2076 volsessionid, 2077 volsessiontime, 2078 firstindex, 2079 lastindex, 2080 startfile as volfile, 2081 JobMedia.startblock, 2082 JobMedia.endblock, 2083 volindex 2084 from 2085 Job, 2086 Media, 2087 JobMedia 2088 where 2089 Job.jobid in ($jlist) and 2090 Job.jobid = JobMedia.jobid and 2091 JobMedia.mediaid = Media.mediaid 2092 order by 2093 volumename, 2094 volsessionid, 2095 volindex 2096 "; 2097 2098 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n"; 2099 $sth->execute || die "Can't execute $query\n"; 2100 2101 while (my $ref = $sth->fetchrow_hashref) { 2102 push(@media, { 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'} 2114 }); 2115 } 2116 2117# Gather bootstrap info 2118# 2119# key - jobid.volumename.volumesession.volindex 2120# job 2121# name 2122# type 2123# session 2124# time 2125# file 2126# startblock 2127# endblock 2128# array of file indexes. 2129 2130 for my $info (values %restore) { 2131 my $jobid = $info->[0]; 2132 my $fidx = $info->[1]; 2133 2134 foreach my $m (@media) { 2135 2136 if ($jobid == $m->{'jobid'} && $fidx >= $m->{'firstindex'} && $fidx <= $m->{'lastindex'}) { 2137 my $key = "$jobid."; 2138 $key .= "$m->{volumename}.$m->{volsessionid}.$m->{volindex}"; 2139 2140 $bootstrap{$key} = { 2141 'job' => $jobid, 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'} 2150 } 2151 if (!$bootstrap{$key}); 2152 2153 $bootstrap{$key}->{'files'} = [] 2154 if (!$bootstrap{$key}->{'files'}); 2155 push(@{$bootstrap{$key}->{'files'}}, $fidx); 2156 } 2157 2158 } 2159 2160 } 2161 2162 # print bootstrap 2163 2164 print STDERR "DBG: Keys = " . join(', ', keys %bootstrap) . "\n" 2165 if ($debug); 2166 2167 my @keys = sort { 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'}; 2175 } keys %bootstrap; 2176 2177 if (!open(BSR, ">$bsrfile")) { 2178 warn "$bsrfile: $|\n"; 2179 return; 2180 } 2181 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"; 2190 2191 my @fids = sort { $a <=> $b} @{$bootstrap{$key}->{'files'}}; 2192 my $first; 2193 my $prev; 2194 2195 for (my $i = 0; $i < @fids; $i++) { 2196 $first = $fids[$i] if (!$first); 2197 2198 if ($prev) { 2199 2200 if ($fids[$i] != $prev + 1) { 2201 print BSR "FileIndex=$first"; 2202 print BSR "-$prev" if ($first != $prev); 2203 print BSR "\n"; 2204 $first = $fids[$i]; 2205 } 2206 2207 } 2208 2209 $prev = $fids[$i]; 2210 } 2211 2212 print BSR "FileIndex=$first"; 2213 print BSR "-$prev" if ($first != $prev); 2214 print BSR "\n"; 2215 print BSR "Count=" . (@fids) . "\n"; 2216 } 2217 2218 close(BSR); 2219} 2220 2221# Change directory 2222 2223sub cmd_cd { 2224 my $dir = shift; 2225 2226 my $save = $files; 2227 2228 $dir = $lwd if ($dir eq '-' && defined($lwd)); 2229 2230 if ($dir ne '-') { 2231 $files = &fetch_dir($dir); 2232 } 2233 else { 2234 warn "Previous director not defined.\n"; 2235 } 2236 2237 if ($files) { 2238 $lwd = $cwd; 2239 $cwd = $dir; 2240 } 2241 else { 2242 print STDERR "Could not locate directory $dir\n"; 2243 $files = $save; 2244 } 2245 2246 $cwd = '/' if (!$cwd); 2247} 2248 2249sub cmd_changetime { 2250 my $tstr = shift; 2251 2252 if (!$tstr) { 2253 print "Time currently set to " . localtime($rtime) . "\n"; 2254 return; 2255 } 2256 2257 my $newtime = parsedate($tstr, FUZZY => 1, PREFER_PAST => 1); 2258 2259 if (defined($newtime)) { 2260 print STDERR "Time evaluated to $newtime\n" if ($debug); 2261 $rtime = $newtime; 2262 print "Setting date/time to " . localtime($rtime) . "\n"; 2263 &setjob; 2264 2265 # Clean cache. 2266 $dircache = {}; 2267 &cache_catalog if ($preload); 2268 2269 # Get directory based on new time. 2270 $files = &fetch_dir($cwd); 2271 } 2272 else { 2273 print STDERR "Could not parse $tstr as date/time\n"; 2274 } 2275 2276} 2277 2278# Change client 2279 2280sub cmd_client { 2281 my $c = shift; 2282 $jobname = shift; # Set global job name 2283 2284 # Lookup client id. 2285 $client = &lookup_client($c); 2286 2287 # Clear cache, we changed machines/jobs 2288 $dircache = {}; 2289 &cache_catalog if ($preload); 2290 2291 # Find last full backup time. 2292 &setjob; 2293 2294 # Get current directory on new client. 2295 $files = &fetch_dir($cwd); 2296 2297 # Clear restore info 2298 $rnum = 0; 2299 $rbytes = 0; 2300 %restore = (); 2301} 2302 2303sub cmd_debug { 2304 $debug = 1 - $debug; 2305} 2306 2307sub cmd_delete { 2308 my @flist = @_; 2309 my $opts = {quiet=>1}; 2310 2311 my $save_rnum = $rnum; 2312 &select_files(0, $opts, $cwd, @flist); 2313 print "" . ($save_rnum - $rnum) . " files un-marked for restore\n"; 2314} 2315 2316sub cmd_help { 2317 2318 foreach my $h (sort keys %COMMANDS) { 2319 printf "%-12s %s\n", $h, $COMMANDS{$h}; 2320 } 2321 2322} 2323 2324sub cmd_history { 2325 2326 foreach my $h ($term->GetHistory) { 2327 print "$h\n"; 2328 } 2329 2330} 2331 2332# Print catalog/tape info about files 2333 2334sub cmd_info { 2335 my @flist = @_; 2336 @flist = ($cwd) if (!@flist); 2337 2338 foreach my $f (@flist) { 2339 $f =~ s|/+$||; 2340 my ($fqdir, $dir, $file) = &path_parts($f); 2341 my $finfo = &fetch_dir($fqdir); 2342 2343 if (!$finfo->{$file}) { 2344 2345 if (!$finfo->{"$file/"}) { 2346 warn "$f: File not found.\n"; 2347 next; 2348 } 2349 2350 $file .= '/'; 2351 } 2352 2353 my $fileid = $finfo->{$file}->{fileid}; 2354 my $fileindex = $finfo->{$file}->{fileindex}; 2355 my $jobid = $finfo->{$file}->{jobid}; 2356 2357 print "#$f -\n"; 2358 print "#FileID : $finfo->{$file}->{fileid}\n"; 2359 print "#JobID : $jobid\n"; 2360 print "#Visible : $finfo->{$file}->{visible}\n"; 2361 2362 my $query = "select 2363 volumename, 2364 mediatype, 2365 volsessionid, 2366 volsessiontime, 2367 startfile, 2368 JobMedia.startblock, 2369 JobMedia.endblock 2370 from 2371 Job, 2372 Media, 2373 JobMedia 2374 where 2375 Job.jobid = $jobid and 2376 Job.jobid = JobMedia.jobid and 2377 $fileindex >= firstindex and 2378 $fileindex <= lastindex and 2379 JobMedia.mediaid = Media.mediaid 2380 "; 2381 2382 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n"; 2383 $sth->execute || die "Can't execute $query\n"; 2384 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"; 2393 print "Count=1\n"; 2394 } 2395 2396 $sth->finish; 2397 } 2398 2399} 2400 2401# List files. 2402 2403sub cmd_ls { 2404 my $opts = shift; 2405 my @flist = @_; 2406 my @keys; 2407 2408 print STDERR "DBG: " . (@flist) . " files to list.\n" if ($debug); 2409 2410 if (!@flist) { 2411 @flist = keys %$files; 2412 } 2413 2414 # Sort files as specified. 2415 2416 if ($opts->{sort} eq 'alpha') { 2417 print STDERR "DBG: Sort by alpha\n" if ($debug); 2418 @keys = sort @flist; 2419 } 2420 elsif ($opts->{sort} eq 'ralpha') { 2421 print STDERR "DBG: Sort by reverse alpha\n" if ($debug); 2422 @keys = sort {$b cmp $a} @flist; 2423 } 2424 elsif ($opts->{sort} eq 'time') { 2425 print STDERR "DBG: Sort by time\n" if ($debug); 2426 @keys = sort { 2427 return $a cmp $b 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'} 2432 } @flist; 2433 } 2434 elsif ($opts->{sort} eq 'rtime') { 2435 print STDERR "DBG: Sort by reverse time\n" if ($debug); 2436 @keys = sort { 2437 return $b cmp $a 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'} 2442 } @flist; 2443 } 2444 elsif ($opts->{sort} eq 'size') { 2445 print STDERR "DBG: Sort by size\n" if ($debug); 2446 @keys = sort { 2447 return $a cmp $b 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'} 2452 } @flist; 2453 } 2454 elsif ($opts->{sort} eq 'rsize') { 2455 print STDERR "DBG: Sort by reverse size\n" if ($debug); 2456 @keys = sort { 2457 return $b cmp $a 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'} 2462 } @flist; 2463 } 2464 else { 2465 print STDERR "DBG: $opts->{sort}, no sort\n" if ($debug); 2466 @keys = @flist; 2467 } 2468 2469 @flist = (); 2470 2471 foreach my $f (@keys) { 2472 print STDERR "DBG: list $f\n" if ($debug); 2473 $f =~ s|/+$||; 2474 my ($fqdir, $dir, $file) = &path_parts($f); 2475 my $finfo = &fetch_dir($fqdir); 2476 2477 if (!$finfo->{$file}) { 2478 2479 if (!$finfo->{"$file/"}) { 2480 warn "$f: File not found.\n"; 2481 next; 2482 } 2483 2484 $file .= '/'; 2485 } 2486 2487 my $fdata = $finfo->{$file}; 2488 2489 if ($opts->{'all'} || $fdata->{'visible'}) { 2490 push(@flist, ["$dir$file", $fdata]); 2491 } 2492 2493 } 2494 2495 if ($opts->{'long'}) { 2496 my $lfmt = &long_fmt(\@flist) if ($opts->{'long'}); 2497 2498 foreach my $f (@flist) { 2499 my $file = $f->[0]; 2500 my $fdata = $f->[1]; 2501 my $r = ($restore{$fdata->{'fileid'}}) ? '+' : ' '; 2502 my $lstat = $fdata->{'lstat'}; 2503 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"; 2507 } 2508 } 2509 else { 2510 &print_by_cols(@flist); 2511 } 2512 2513} 2514 2515sub cmd_pwd { 2516 print "$cwd\n"; 2517} 2518 2519# Create restore data for bconsole 2520 2521sub cmd_recover { 2522 my $query = "create table recover (jobid int, fileindex int)"; 2523 2524 $dbh->do($query) 2525 || warn "Could not create recover table. Hope it's already there.\n"; 2526 2527 if ($db eq 'postgres') { 2528 $query = "COPY recover FROM STDIN"; 2529 2530 $dbh->do($query) || die "Can't execute $query\n"; 2531 2532 foreach my $finfo (values %restore) { 2533 $dbh->pg_putline("$finfo->[0]\t$finfo->[1]\n"); 2534 } 2535 2536 $dbh->pg_endcopy; 2537 } 2538 else { 2539 2540 foreach my $finfo (values %restore) { 2541 $query = "insert into recover ( 2542 'jobid', 'fileindex' 2543 ) 2544 values ( 2545 $finfo->[0], $finfo->[1] 2546 )"; 2547 $dbh->do($query) || die "Can't execute $query\n"; 2548 } 2549 2550 } 2551 2552 $query = "GRANT all on recover to bacula"; 2553 $dbh->do($query) || die "Can't execute $query\n"; 2554 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"; 2558 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"; 2562 $sth->finish; 2563} 2564 2565sub cmd_relocate { 2566 $restore_to = shift; 2567} 2568 2569# Display information about recover's state 2570 2571sub cmd_show { 2572 my $what = shift; 2573 2574 if ($what eq 'clients') { 2575 2576 foreach my $c (sort keys %$clients) { 2577 print "$c\n"; 2578 } 2579 2580 } 2581 elsif ($what eq 'catalog') { 2582 print "$catalog\n"; 2583 } 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"; 2588 2589 my $ref = $sth->fetchrow_hashref; 2590 print "$$ref{name}; $jobname\n"; 2591 $sth->finish; 2592 } 2593 elsif ($what eq 'cache') { 2594 print "The following directories are cached\n"; 2595 2596 foreach my $d (sort keys %$dircache) { 2597 print "$d\n"; 2598 } 2599 2600 } 2601 elsif ($what eq 'restore') { 2602 print "There are $rnum files marked for restore.\n"; 2603 2604 print STDERR "DBG: Bytes = $rbytes\n" if ($debug); 2605 2606 if ($rbytes < 1024) { 2607 print "The restore will require $rbytes bytes.\n"; 2608 } 2609 elsif ($rbytes < 1024*1024) { 2610 my $rk = $rbytes/1024; 2611 printf "The restore will require %.2f KB.\n", $rk; 2612 } 2613 elsif ($rbytes < 1024*1024*1024) { 2614 my $rm = $rbytes/1024/1024; 2615 printf "The restore will require %.2f MB.\n", $rm; 2616 } 2617 else { 2618 my $rg = $rbytes/1024/1024/1024; 2619 printf "The restore will require %.2f GB.\n", $rg; 2620 } 2621 2622 print "Restores will be placed in $restore_to\n"; 2623 } 2624 elsif ($what eq 'volumes') { 2625 &cmd_volumes; 2626 } 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"; 2640 } 2641 else { 2642 warn "Don't know how to show $what\n"; 2643 } 2644 2645} 2646 2647sub cmd_verbose { 2648 $verbose = 1 - $verbose; 2649} 2650 2651sub cmd_versions { 2652 my @flist = @_; 2653 2654 @flist = ($cwd) if (!@flist); 2655 2656 foreach my $f (@flist) { 2657 my $path; 2658 my $data = {}; 2659 2660 print STDERR "DBG: Get versions for $f\n" if ($debug); 2661 2662 $f =~ s|/+$||; 2663 my ($fqdir, $dir, $file) = &path_parts($f); 2664 my $finfo = &fetch_dir($fqdir); 2665 2666 if (!$finfo->{$file}) { 2667 2668 if (!$finfo->{"$file/"}) { 2669 warn "$f: File not found.\n"; 2670 next; 2671 } 2672 2673 $file .= '/'; 2674 } 2675 2676 if ($file =~ m|/$|) { 2677 $path = "$fqdir$file"; 2678 $file = ''; 2679 } 2680 else { 2681 $path = $fqdir; 2682 } 2683 2684 print STDERR "DBG: Use $ftime, $path, $file, $client, $jobname\n" 2685 if ($debug); 2686 2687 $ver_sth->execute($ftime, $rtime, $path, $file, $client, $jobname) 2688 || die "Can't execute $queries{$db}->{'ver'}\n"; 2689 2690 # Gather stats 2691 2692 while (my $ref = $ver_sth->fetchrow_hashref) { 2693 my $f = "$ref->{name};$ref->{jobtdate}"; 2694 $data->{$f} = &create_file_entry( 2695 $f, 2696 $ref->{'fileid'}, 2697 $ref->{'fileindex'}, 2698 $ref->{'jobid'}, 2699 $ref->{'visible'}, 2700 $ref->{'lstat'} 2701 ); 2702 2703 $data->{$f}->{'jobtdate'} = $ref->{'jobtdate'}; 2704 $data->{$f}->{'volume'} = $ref->{'volumename'}; 2705 } 2706 2707 my @keys = sort { 2708 $data->{$a}->{'jobtdate'} <=> 2709 $data->{$b}->{'jobtdate'} 2710 } keys %$data; 2711 2712 my @list = (); 2713 2714 foreach my $f (@keys) { 2715 push(@list, [$file, $data->{$f}]); 2716 } 2717 2718 my $lfmt = &long_fmt(\@list); 2719 print "\nVersions of \`$path$file' earlier than "; 2720 print localtime($rtime) . ":\n\n"; 2721 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"; 2729 } 2730 2731 } 2732 2733} 2734 2735# List volumes needed for restore. 2736 2737sub cmd_volumes { 2738 my %media; 2739 my @jobmedia; 2740 my %volumes; 2741 2742 # Get media. 2743 my $query = "select mediaid, volumename from Media"; 2744 my $sth = $dbh->prepare($query) || die "Can't prepare $query\n"; 2745 2746 $sth->execute || die "Can't execute $query\n"; 2747 2748 while (my $ref = $sth->fetchrow_hashref) { 2749 $media{$$ref{'mediaid'}} = $$ref{'volumename'}; 2750 } 2751 2752 $sth->finish(); 2753 2754 # Get media usage. 2755 $query = "select mediaid, jobid, firstindex, lastindex from JobMedia"; 2756 $sth = $dbh->prepare($query) || die "Can't prepare $query\n"; 2757 2758 $sth->execute || die "Can't execute $query\n"; 2759 2760 while (my $ref = $sth->fetchrow_hashref) { 2761 push(@jobmedia, { 2762 'mediaid' => $$ref{'mediaid'}, 2763 'jobid' => $$ref{'jobid'}, 2764 'firstindex' => $$ref{'firstindex'}, 2765 'lastindex' => $$ref{'lastindex'} 2766 }); 2767 } 2768 2769 $sth->finish(); 2770 2771 # Find needed volumes 2772 2773 foreach my $fileid (keys %restore) { 2774 my ($jobid, $idx) = @{$restore{$fileid}}; 2775 2776 foreach my $jm (@jobmedia) { 2777 next if ($jm->{'jobid'}) != $jobid; 2778 2779 if ($idx >= $jm->{'firstindex'} && $idx <= $jm->{'lastindex'}) { 2780 $volumes{$media{$jm->{'mediaid'}}} = 1; 2781 } 2782 2783 } 2784 2785 } 2786 2787 print "The following volumes are needed for restore.\n"; 2788 2789 foreach my $v (sort keys %volumes) { 2790 print "$v\n"; 2791 } 2792 2793} 2794 2795sub cmd_error { 2796 my $msg = shift; 2797 print STDERR "$msg\n"; 2798} 2799 2800############################################################################## 2801### Start of program 2802############################################################################## 2803 2804&cache_catalog if ($preload); 2805 2806print "Using $readline for command processing\n" if ($verbose); 2807 2808# Initialize command completion 2809 2810# Add binding for Perl readline. Issue warning. 2811if ($readline eq 'Term::ReadLine::Gnu') { 2812 $term->ReadHistory($HIST_FILE); 2813 print STDERR "DBG: FCD - $tty_attribs->{filename_completion_desired}\n" 2814 if ($debug); 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); 2818} 2819elsif ($readline eq 'Term::ReadLine::Perl') { 2820 readline::rl_bind('TAB', 'ViComplete'); 2821 warn "Command completion disabled. $readline is seriously broken\n"; 2822} 2823else { 2824 warn "Can't deal with $readline, Command completion disabled.\n"; 2825} 2826 2827&cmd_cd($start_dir); 2828 2829while (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'); 2834 2835 print STDERR "Execute $command[0] command.\n" if ($debug); 2836 2837 my $cmd = \&{"cmd_$command[0]"}; 2838 2839 # The following line will call the subroutine named cmd_ prepended to 2840 # the name of the command returned by parse_command. 2841 2842 &$cmd(@command[1..$#command]); 2843}; 2844 2845$dir_sth->finish(); 2846$sel_sth->finish(); 2847$ver_sth->finish(); 2848$dbh->disconnect(); 2849 2850print "\n" if (!defined($cstr)); 2851 2852$term->WriteHistory($HIST_FILE) if ($readline eq 'Term::ReadLine::Gnu'); 2853 2854=head1 DEPENDENCIES 2855 2856The following CPAN modules are required to run this program. 2857 2858DBI, Term::ReadKey, Time::ParseDate, Date::Format, Text::ParseWords 2859 2860Additionally, you will only get command line completion if you also have 2861 2862Term::ReadLine::Gnu 2863 2864=head1 AUTHOR 2865 2866Karl Hakimian <hakimian@aha.com> 2867 2868=head1 LICENSE 2869 2870Copyright (C) 2006 Karl Hakimian 2871 2872This program is free software; you can redistribute it and/or modify 2873it under the terms of the GNU General Public License as published by 2874the Free Software Foundation; either version 2 of the License, or 2875(at your option) any later version. 2876 2877This program is distributed in the hope that it will be useful, 2878but WITHOUT ANY WARRANTY; without even the implied warranty of 2879MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 2880GNU General Public License for more details. 2881 2882You should have received a copy of the GNU General Public License 2883along with this program; if not, write to the Free Software 2884Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 2885 2886=cut 2887