]> git.sur5r.net Git - bacula/bacula/blob - bacula/examples/recover.pl
This commit was manufactured by cvs2svn to create tag
[bacula/bacula] / bacula / examples / recover.pl
1 #!/usr/bin/perl -w
2
3 =head1 NAME
4
5 recover.pl - a script to provide an interface for restore files similar
6 to Legatto Networker's recover program.
7
8 =cut
9
10 use strict;
11 use Getopt::Std;
12 use DBI;
13 use Term::ReadKey;
14 use Term::ReadLine;
15 use Fcntl ':mode';
16 use Time::ParseDate;
17 use Date::Format;
18 use Text::ParseWords;
19
20 # Location of config file.
21 my $CONF_FILE = "$ENV{HOME}/.recoverrc"; 
22 my $HIST_FILE = "$ENV{HOME}/.recover.hist";
23
24 ########################################################################
25 ### Queries needed to gather files from directory.
26 ########################################################################
27
28 my %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
388 my %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
410 my %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
423 my %catalogs;
424 my $catalog;    # Current catalog
425
426 ## Globals
427
428 my %restore;
429 my $rnum = 0;
430 my $rbytes = 0;
431 my $debug = 0;
432 my $verbose = 0;
433 my $rtime;
434 my $cwd;
435 my $lwd;
436 my $files;
437 my $restore_to = '/';
438 my $start_dir;
439 my $preload;
440 my $dircache = {};
441 my $usecache = 1;
442
443 =head1 SYNTAX
444
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>]
447
448 B<recover.pl> [B<-h>]
449
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
454 be used.
455
456 =head1 DESCRIPTION
457
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.
461
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.
469
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.
472
473 The B<-d> option turns on debugging and the B<-v> option turns on
474 verbose output.
475
476 By specifying a I<catalogname>, the default options for connecting to
477 the catalog database will be taken from the section of the inti file
478 specified by that name.
479
480 The B<-h> option will display this document.
481
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.
484
485 B<CREATE INDEX file_pathid_idx on file(pathid);>
486
487 B<CREATE INDEX file_filenameid_idx on file(filenameid);>
488
489 =cut
490
491 my $vars = {};
492 getopts("c:b:hi:j:pt:vd", $vars) || die "Usage: bad arguments\n";
493
494 if ($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
505 if ($vars->{'t'}) {
506         $rtime = parsedate($vars->{'t'}, FUZZY => 1, PREFER_PAST => 1);
507 }
508 else {
509         $rtime = time();
510 }
511
512 my $dbconnect;
513 my $username = "";
514 my $password = "";
515 my $db;
516 my $client;
517 my $jobname;
518 my $jobs;
519 my $ftime;
520
521 my $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
531 if ($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
567 if ($vars->{'b'}) {
568         $dbconnect = $vars->{'b'};
569 }
570
571 die "You must supply a db connect string.\n" if (!defined($dbconnect));
572
573 if ($dbconnect =~ /^dbi:Pg/) {
574         $db = 'postgres';
575 }
576 elsif ($dbconnect =~ /^dbi:mysql/) {
577         $db = 'mysql';
578 }
579 else {
580         die "Unknown database type specified in $dbconnect\n";
581 }
582
583 # Initialize database connection
584
585 print STDERR "DBG: Connect using: $dbconnect\n" if ($debug);
586
587 my $dbh = DBI->connect($dbconnect, $username, $password) ||
588         die "Can't open bacula database\nDatabase connect string '$dbconnect'";
589
590 die "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
598 die "You need to specify a job name.\n" if (!$jobname);
599
600 &setjob;
601
602 die "Failed to set client\n" if (!$client);
603
604 # Prepare our query
605 my $dir_sth = $dbh->prepare($queries{$db}->{'dir'})
606         || die "Can't prepare $queries{$db}->{'dir'}\n";
607
608 my $sel_sth = $dbh->prepare($queries{$db}->{'sel'})
609         || die "Can't prepare $queries{$db}->{'sel'}\n";
610
611 my $ver_sth = $dbh->prepare($queries{$db}->{'ver'})
612         || die "Can't prepare $queries{$db}->{'ver'}\n";
613
614 my $clients;
615
616 # Initialize readline.
617 my $term = new Term::ReadLine('Bacula Recover');
618 $term->ornaments(0);
619
620 my $readline = $term->ReadLine;
621 my $tty_attribs = $term->Attribs;
622
623 # Needed for base64 decode
624
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', '+', '/'
631 );
632 my @base64_map = (0) x 128;
633
634 for (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
644 B<$HOME/.recoverrc> Configuration file for B<recover.pl>.
645
646 =head1 CONFIG FILE FORMAT
647
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.
651
652 The first catalog specified will be used as the default catalog.
653
654 All values are specified in I<item> B<=> I<value> format. You can
655 specify the following I<item>s for each catalog.
656
657 =cut
658
659 sub 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
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.
697
698 =cut
699
700                                         if ($item eq 'client') {
701                                                 $catalogs{$c}->{'client'} = $value;
702                                         }
703
704 =head2 dbconnect
705
706 The DBI compatible database string to use to connect to this catalog.
707
708 =over 4
709
710 =item B<example:>
711
712 dbi: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
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.
725
726 =cut
727                                         elsif ($item eq 'jobname') {
728                                                 $catalogs{$c}->{'jobname'} = $value;
729                                         }
730
731 =head2 password
732
733 The 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
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
744 caution.
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
763 The 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
787 sub 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
843 sub 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
903 sub 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
945 sub 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
983 sub 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
1014 sub 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
1056 sub 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
1194 sub 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
1231 sub 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
1258 sub 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
1309 sub 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
1333 sub 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
1355 sub 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
1432 sub 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
1470 sub nocomplete {
1471         return ();
1472 }
1473
1474 # subroutine to create printf format for long listing of ls
1475
1476 sub 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
1505 sub 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
1590 sub 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
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
1612 as expected.
1613
1614 The following commands are understood.
1615
1616 =cut
1617
1618 sub 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
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.
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
1665 Create a bootstrap file suitable for use with the bacula B<bextract>
1666 command. 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
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.
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
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.
1760
1761 The time can be specifed in almost any reasonable way. Here are a few
1762 examples:
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
1785 Specify 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
1799 Toggle debug flag.
1800
1801 =cut
1802         elsif ($cmd eq 'debug') {
1803                 @command = ('debug');
1804         }
1805
1806 =head2 delete [I<filelist>]
1807
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.
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
1824 Show 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
1833 Display 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
1842 Display information about the specified files. The format of the
1843 information 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
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.
1860
1861 =over 4
1862
1863 =item -a
1864
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.
1867
1868 =item -l
1869
1870 List files in long format (like unix ls command).
1871
1872 =item -r
1873
1874 reverse direction of sort.
1875
1876 =item -S
1877
1878 Sort files by size.
1879
1880 =item -t
1881
1882 Sort 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
1918 Show current directory.
1919
1920 =cut
1921         elsif ($cmd eq 'pwd') {
1922                 @command = ('pwd');
1923         }
1924
1925 =head2 quit
1926
1927 Exit program.
1928
1929 B<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
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.
1941
1942 =cut
1943         elsif ($cmd eq 'recover') {
1944                 @command = ('recover');
1945         }
1946
1947 =head2 relocate I<directory>
1948
1949 Specify 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
1963 Show various information about B<recover.pl>. The following items can be specified.
1964
1965 =over 4
1966
1967 =item cache
1968
1969 Display's a list of cached directories.
1970
1971 =item catalog
1972
1973 Displays the name of the catalog we are talking to.
1974
1975 =item client
1976
1977 Display current client and job named that are being viewed.
1978
1979 =item restore
1980
1981 Display the number of files and size to be restored.
1982
1983 =item volumes
1984
1985 Display the volumes that will be required to perform a restore on the
1986 selected 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
1998 Toggle verbose flag.
1999
2000 =cut
2001         elsif ($cmd eq 'verbose') {
2002                 @command = ('verbose');
2003         }
2004
2005 =head2 versions [I<filelist>]
2006
2007 View all version of specified files available from the current
2008 time. 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
2022 Display the volumes that will be required to perform a restore on the
2023 selected 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
2042 sub 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
2051 sub 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
2223 sub 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
2249 sub 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
2280 sub 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
2303 sub cmd_debug {
2304         $debug = 1 - $debug;
2305 }
2306
2307 sub 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
2316 sub cmd_help {
2317
2318         foreach my $h (sort keys %COMMANDS) {
2319                 printf "%-12s %s\n", $h, $COMMANDS{$h};
2320         }
2321
2322 }
2323
2324 sub 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
2334 sub 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
2403 sub 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
2515 sub cmd_pwd {
2516         print "$cwd\n";
2517 }
2518
2519 # Create restore data for bconsole
2520
2521 sub 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
2565 sub cmd_relocate {
2566         $restore_to = shift;
2567 }
2568
2569 # Display information about recover's state
2570
2571 sub 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
2647 sub cmd_verbose {
2648         $verbose = 1 - $verbose;
2649 }
2650
2651 sub 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
2737 sub 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
2795 sub 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
2806 print "Using $readline for command processing\n" if ($verbose);
2807
2808 # Initialize command completion
2809
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"
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 }
2819 elsif ($readline eq 'Term::ReadLine::Perl') {
2820         readline::rl_bind('TAB', 'ViComplete');
2821         warn "Command completion disabled. $readline is seriously broken\n";
2822 }
2823 else {
2824         warn "Can't deal with $readline, Command completion disabled.\n";
2825 }
2826
2827 &cmd_cd($start_dir);
2828
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');
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
2850 print "\n" if (!defined($cstr));
2851
2852 $term->WriteHistory($HIST_FILE) if ($readline eq 'Term::ReadLine::Gnu');
2853
2854 =head1 DEPENDENCIES
2855
2856 The following CPAN modules are required to run this program.
2857
2858 DBI, Term::ReadKey, Time::ParseDate, Date::Format, Text::ParseWords
2859
2860 Additionally, you will only get command line completion if you also have
2861
2862 Term::ReadLine::Gnu
2863
2864 =head1 AUTHOR
2865
2866 Karl Hakimian <hakimian@aha.com>
2867
2868 =head1 LICENSE
2869
2870 Copyright (C) 2006 Karl Hakimian
2871
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.
2876
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.
2881
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
2885
2886 =cut