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