]> git.sur5r.net Git - bacula/bacula/blob - gui/brestore/brestore.pl
d5c33a33bba97cfeae2b28dc5caea6f8ca81282b
[bacula/bacula] / gui / brestore / brestore.pl
1 #!/usr/bin/perl -w
2 use strict ;
3
4 # path to your brestore.glade
5 my $glade_file = 'brestore.glade' ;
6
7 =head1 NAME
8
9     brestore.pl - A Perl/Gtk console for Bacula
10
11 =head1 VERSION
12
13     $Id$
14
15 =head1 INSTALL
16   
17   Setup ~/.brestore.conf to find your brestore.glade
18
19   On debian like system, you need :
20     - libgtk2-gladexml-perl
21     - libdbd-mysql-perl or libdbd-pg-perl
22     - libexpect-perl
23
24   You have to add brestore_xxx tables to your catalog.
25
26   To speed up database query you have to create theses indexes
27     - CREATE INDEX file_pathid on File(PathId);
28     - ...
29
30   To follow restore job, you must have a running Bweb installation.
31
32 =head1 COPYRIGHT
33
34   Copyright (C) 2006 Marc Cousin and Eric Bollengier
35
36   This library is free software; you can redistribute it and/or
37   modify it under the terms of the GNU Lesser General Public
38   License as published by the Free Software Foundation; either
39   version 2 of the License, or (at your option) any later version.
40  
41   This library is distributed in the hope that it will be useful,
42   but WITHOUT ANY WARRANTY; without even the implied warranty of
43   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
44   Lesser General Public License for more details.
45   
46   You should have received a copy of the GNU Lesser General Public
47   License along with this library; if not, write to the
48   Free Software Foundation, Inc., 59 Temple Place - Suite 330,
49   Boston, MA 02111-1307, USA.
50   
51   Base 64 functions from Karl Hakimian <hakimian@aha.com>
52   Integrally copied from recover.pl from bacula source distribution.
53
54 =cut
55
56 use Gtk2;               # auto-initialize Gtk2
57 use Gtk2::GladeXML;
58 use Gtk2::SimpleList;           # easy wrapper for list views
59 use Gtk2::Gdk::Keysyms;         # keyboard code constants
60 use Data::Dumper qw/Dumper/;
61 use DBI;
62 my $debug=0;                    # can be on brestore.conf
63
64 ################################################################
65
66 package DlgFileVersion;
67
68 sub on_versions_close_clicked
69 {
70     my ($self, $widget)=@_;
71     $self->{version}->destroy();
72 }
73
74 sub on_selection_button_press_event
75 {
76     print STDERR "on_selection_button_press_event()\n";
77 }
78
79 sub fileview_data_get
80 {
81     my ($self, $widget, $context, $data, $info, $time,$string) = @_;
82
83     DlgResto::drag_set_info($widget, 
84                             $self->{cwd},
85                             $data);
86 }
87
88 sub new
89 {
90     my ($class, $dbh, $client, $path, $file) = @_;
91     my $self = bless {
92         cwd       => $path,
93         version   => undef, # main window
94         };
95
96     # we load version widget of $glade_file
97     my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_version");
98
99     # Connect signals magically
100     $glade_box->signal_autoconnect_from_package($self);
101
102     $glade_box->get_widget("version_label")
103         ->set_markup("<b>File revisions : $client:$path/$file</b>");
104
105     my $widget = $glade_box->get_widget('version_fileview');
106     my $fileview = Gtk2::SimpleList->new_from_treeview(
107                    $widget,
108                    'h_name'        => 'hidden',
109                    'h_jobid'       => 'hidden',
110                    'h_type'        => 'hidden',
111
112                    'InChanger'     => 'pixbuf',
113                    'Volume'        => 'text',
114                    'JobId'         => 'text',
115                    'Size'          => 'text',
116                    'Date'          => 'text',
117                    'MD5'           => 'text',
118                                                        );
119     DlgResto::init_drag_drop($fileview);
120
121     my @v = DlgResto::get_all_file_versions($dbh, 
122                                             "$path/", 
123                                             $file,
124                                             $client,
125                                             1);
126     for my $ver (@v) {
127         my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
128             = @{$ver};
129         my $icon = ($inchanger)?$DlgResto::yesicon:$DlgResto::noicon;
130
131         DlgResto::listview_push($fileview,
132                                 $file, $jobid, 'file', 
133                                 $icon, $volname, $jobid,DlgResto::human($size),
134                                 scalar(localtime($mtime)), $md5);
135     }
136
137     $self->{version} = $glade_box->get_widget('dlg_version');
138     $self->{version}->show();
139     
140     return $self;
141 }
142
143 sub on_forward_keypress
144 {
145     return 0;
146 }
147
148 1;
149 ################################################################
150 package DlgWarn;
151
152 sub new
153 {
154     my ($package, $text) = @_;
155
156     my $self = bless {};
157
158     my $glade = Gtk2::GladeXML->new($glade_file, "dlg_warn");
159
160     # Connect signals magically
161     $glade->signal_autoconnect_from_package($self);
162     $glade->get_widget('label_warn')->set_text($text);
163
164     print STDERR "$text\n";
165
166     $self->{window} = $glade->get_widget('dlg_warn');
167     $self->{window}->show_all();
168     return $self;
169 }
170
171 sub on_close_clicked
172 {
173     my ($self) = @_;
174     $self->{window}->destroy();
175 }
176 1;
177
178 ################################################################
179 package BwebConsole;
180 use LWP::UserAgent;
181 use HTTP::Request::Common;
182
183 sub new
184 {
185     my ($class, %arg) = @_;
186
187     my $self = bless {
188         pref => $arg{pref},     # Pref object
189         timeout => $arg{timeout} || 20,
190         debug   => $arg{debug} || 0,
191         'list_job'     => '',
192         'list_client'  => '',
193         'list_fileset' => '',
194         'list_storage' => '',
195         'run'          => '',
196     };
197
198     return $self;
199 }
200
201 sub prepare
202 {
203     my ($self, @what) = @_;
204     my $ua = LWP::UserAgent->new();
205     $ua->agent("Brestore ");
206     my $req = POST($self->{pref}->{bconsole},
207                    Content_Type => 'form-data',
208                    Content => [ map { (action => $_) } @what ]);
209     #$req->authorization_basic('eric', 'test');
210
211     my $res = $ua->request($req);
212
213     if ($res->is_success) {
214         foreach my $l (split(/\n/, $res->content)) {
215             my ($k, $c) = split(/=/,$l,2);
216             $self->{$k} = $c;
217         }
218     } else {
219         $self->{error} = "Can't connect to bweb : " . $res->status_line;
220         new DlgWarn($self->{error});
221     }
222 }
223
224 sub run
225 {
226     my ($self, %arg) = @_;
227
228     my $ua = LWP::UserAgent->new();
229     $ua->agent("Brestore ");
230     my $req = POST($self->{pref}->{bconsole},
231                    Content_Type => 'form-data',
232                    Content => [ job     => $arg{job},
233                                 client  => $arg{client},
234                                 storage => $arg{storage} || '',
235                                 fileset => $arg{fileset} || '',
236                                 where   => $arg{where},
237                                 replace => $arg{replace},
238                                 priority=> $arg{prio}    || '',
239                                 action  => 'run',
240                                 timeout => 10,
241                                 bootstrap => [$arg{bootstrap}],
242                                 ]);
243     #$req->authorization_basic('eric', 'test');
244
245     my $res = $ua->request($req);
246
247     if ($res->is_success) {
248         foreach my $l (split(/\n/, $res->content)) {
249             my ($k, $c) = split(/=/,$l,2);
250             $self->{$k} = $c;
251         }
252     } 
253
254     if ($self->{run}) {
255         unlink($arg{bootstrap});
256         new DlgWarn("Can't connect to bweb : " . $res->status_line);
257     } 
258
259     return $self->{run};
260 }
261
262 sub list_job
263 {
264     my ($self) = @_;
265     return sort split(/;/, $self->{'list_job'});
266 }
267
268 sub list_fileset
269 {
270     my ($self) = @_;
271     return sort split(/;/, $self->{'list_fileset'});
272 }
273
274 sub list_storage
275 {
276     my ($self) = @_;
277     return sort split(/;/, $self->{'list_storage'});
278 }
279 sub list_client
280 {
281     my ($self) = @_;
282     return sort split(/;/, $self->{'list_client'});
283 }
284
285 1;
286
287 ################################################################
288
289 package DlgLaunch;
290
291 #use Bconsole;
292
293 # %arg = (bsr_file => '/path/to/bsr',       # on director
294 #         volumes  => [ '00001', '00004']
295 #         pref     => ref Pref
296 #         );
297
298 sub get_bconsole
299 {
300     my ($pref) = (@_);
301
302     if ($pref->{bconsole} =~ /^http/) {
303         return new BwebConsole(pref => $pref);
304     } else {
305         if (eval { require Bconsole; }) {
306             return new Bconsole(pref => $pref);
307         } else {
308             new DlgWarn("Can't use bconsole, verify your setup");
309             return undef;
310         }
311     }
312 }
313
314 sub new
315 {
316     my ($class, %arg) = @_;
317
318     my $self = bless {
319         bsr_file => $arg{bsr_file}, # /path/to/bsr on director
320         pref     => $arg{pref}, # Pref ref
321         glade => undef,         # GladeXML ref
322         bconsole => undef,      # Bconsole ref
323     };
324
325     my $console = $self->{bconsole} = get_bconsole($arg{pref});
326     unless ($console) {
327         return undef;
328     }
329
330     # we load launch widget of $glade_file
331     my $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file, 
332                                                      "dlg_launch");
333
334     # Connect signals magically
335     $glade->signal_autoconnect_from_package($self);
336
337     my $widget = $glade->get_widget('volumeview');
338     my $volview = Gtk2::SimpleList->new_from_treeview(
339                    $widget,
340                    'InChanger'     => 'pixbuf',
341                    'Volume'        => 'text', 
342                    );       
343
344     my $infos = get_volume_inchanger($arg{pref}->{dbh}, $arg{volumes}) ;
345     
346     # we replace 0 and 1 by $noicon and $yesicon
347     for my $i (@{$infos}) {
348         if ($i->[0] == 0) {
349             $i->[0] = $DlgResto::noicon;
350         } else {
351             $i->[0] = $DlgResto::yesicon;
352         }
353     }
354
355     # fill volume view
356     push @{ $volview->{data} }, @{$infos} ;
357     
358     $console->prepare(qw/list_client list_job list_fileset list_storage/);
359
360     # fill client combobox (with director defined clients
361     my @clients = $console->list_client() ; # get from bconsole
362     if ($console->{error}) {
363         new DlgWarn("Can't use bconsole:\n$arg{pref}->{bconsole}: $console->{error}") ;
364     }
365     my $w = $self->{combo_client} = $glade->get_widget('combo_launch_client') ;
366     $self->{list_client} = DlgResto::init_combo($w, 'text');
367     DlgResto::fill_combo($self->{list_client}, 
368                          $DlgResto::client_list_empty,
369                          @clients);
370     $w->set_active(0);
371
372     # fill fileset combobox
373     my @fileset = $console->list_fileset() ;
374     $w = $self->{combo_fileset} = $glade->get_widget('combo_launch_fileset') ;
375     $self->{list_fileset} = DlgResto::init_combo($w, 'text');
376     DlgResto::fill_combo($self->{list_fileset}, '', @fileset); 
377
378     # fill job combobox
379     my @job = $console->list_job() ;
380     $w = $self->{combo_job} = $glade->get_widget('combo_launch_job') ;
381     $self->{list_job} = DlgResto::init_combo($w, 'text');
382     DlgResto::fill_combo($self->{list_job}, '', @job);
383     
384     # find default_restore_job in jobs list
385     my $default_restore_job = $arg{pref}->{default_restore_job} ;
386     my $index=0;
387     my $i=1;                    # 0 is ''
388     for my $j (@job) {
389         if ($j =~ /$default_restore_job/io) {
390             $index=$i;
391             last;
392         }
393         $i++;
394     }
395     $w->set_active($index);
396
397     # fill storage combobox
398     my @storage = $console->list_storage() ;
399     $w = $self->{combo_storage} = $glade->get_widget('combo_launch_storage') ;
400     $self->{list_storage} = DlgResto::init_combo($w, 'text');
401     DlgResto::fill_combo($self->{list_storage}, '', @storage);
402
403     $glade->get_widget('dlg_launch')->show_all();
404
405     return $self;
406 }
407
408 sub show_job
409 {
410     my ($self, $client, $jobid) = @_;
411
412     my $ret = $self->{pref}->go_bweb("?action=dsp_cur_job;jobid=$jobid;client=$client", "view job status");
413
414     if ($ret == -1) {
415         my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'info', 'close', 
416 "Your job have been submited to bacula.
417 To follow it, you must use bconsole (or install/configure bweb)");
418         $widget->run;
419         $widget->destroy();
420     }
421
422     $self->on_cancel_resto_clicked();
423 }
424
425 sub on_cancel_resto_clicked
426 {
427     my ($self) = @_ ;
428     $self->{glade}->get_widget('dlg_launch')->destroy();
429 }
430
431 sub on_submit_resto_clicked
432 {
433     my ($self) = @_ ;
434     my $glade = $self->{glade};
435
436     my $r = $self->copy_bsr($self->{bsr_file}, $self->{pref}->{bsr_dest}) ;
437     
438     unless ($r) {
439         new DlgWarn("Can't copy bsr file to director ($self->{error})");
440         return;
441     }
442
443     my $fileset = $glade->get_widget('combo_launch_fileset')
444                                ->get_active_text();
445
446     my $storage = $glade->get_widget('combo_launch_storage')
447                                ->get_active_text();
448
449     my $where = $glade->get_widget('entry_launch_where')->get_text();
450
451     my $job = $glade->get_widget('combo_launch_job')
452                                ->get_active_text();
453
454     if (! $job) {
455         new DlgWarn("Can't use this job");
456         return;
457     }
458
459     my $client = $glade->get_widget('combo_launch_client')
460                                ->get_active_text();
461
462     if (! $client or $client eq $DlgResto::client_list_empty) {
463         new DlgWarn("Can't use this client ($client)");
464         return;
465     }
466
467     my $prio = $glade->get_widget('spin_launch_priority')->get_value();
468
469     my $replace = $glade->get_widget('chkbp_launch_replace')->get_active();
470     $replace=($replace)?'always':'never';    
471
472     my $jobid = $self->{bconsole}->run(job => $job,
473                                        client  => $client,
474                                        storage => $storage,
475                                        fileset => $fileset,
476                                        where   => $where,
477                                        replace => $replace,
478                                        priority=> $prio,
479                                        bootstrap => $r);
480
481     $self->show_job($client, $jobid);
482 }
483
484 sub on_combo_storage_button_press_event
485 {
486     my ($self) = @_;
487     print "on_combo_storage_button_press_event()\n";
488 }
489
490 sub on_combo_fileset_button_press_event
491 {
492     my ($self) = @_;
493     print "on_combo_fileset_button_press_event()\n";
494
495 }
496
497 sub on_combo_job_button_press_event
498 {
499     my ($self) = @_;
500     print "on_combo_job_button_press_event()\n";
501 }
502
503 sub get_volume_inchanger
504 {
505     my ($dbh, $vols) = @_;
506
507     my $lst = join(',', map { $dbh->quote($_) } @{ $vols } ) ;
508
509     my $rq = "SELECT InChanger, VolumeName 
510                FROM  Media  
511                WHERE VolumeName IN ($lst)
512              ";
513
514     my $res = $dbh->selectall_arrayref($rq);
515     return $res;                # [ [ 1, VolName].. ]
516 }
517
518
519 use File::Copy qw/copy/;
520 use File::Basename qw/basename/; 
521
522 # We must kown the path+filename destination
523 # $self->{error} contains error message
524 # it return 0/1 if fail/success
525 sub copy_bsr
526 {
527     my ($self, $src, $dst) = @_ ;
528     print "$src => $dst\n"
529         if ($debug);
530
531     my $ret=0 ;
532     my $err ; 
533     my $dstfile;
534
535     if ($dst =~ m!file:/(/.+)!) {
536         $ret = copy($src, $1);
537         $err = $!;
538         $dstfile = "$1/" . basename($src) ;
539
540     } elsif ($dst =~ m!scp://([^:]+:(.+))!) {
541         $err = `scp $src $1 2>&1` ;
542         $ret = ($? == 0) ;
543         $dstfile = "$2/" . basename($src) ;
544
545     } else {
546         $ret = 0;
547         $err = "$dst not implemented yet";
548         File::Copy::copy($src, \*STDOUT);
549     }
550
551     $self->{error} = $err;
552
553     if ($ret == 0) {
554         $self->{error} = $err;
555         return '';
556
557     } else {
558         return $dstfile;
559     }
560 }
561 1;
562
563 ################################################################
564
565 package DlgAbout;
566
567 my $about_widget;
568
569 sub display
570 {
571     unless ($about_widget) {
572         my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_about") ;
573         $about_widget = $glade_box->get_widget("dlg_about") ;
574         $glade_box->signal_autoconnect_from_package('DlgAbout');
575     }
576     $about_widget->show() ;
577 }
578
579 sub on_about_okbutton_clicked
580 {
581     $about_widget->hide() ;
582 }
583
584 1;
585
586 ################################################################
587 # preference reader
588 package Pref;
589
590 sub new
591 {
592     my ($class, $config_file) = @_;
593     
594     my $self = bless {
595         config_file => $config_file,
596         password => '',         # db passwd
597         username => '',         # db username
598         connection_string => '',# db connection string
599         bconsole => 'bconsole', # path and arg to bconsole
600         bsr_dest => '',         # destination url for bsr files
601         debug    => 0,          # debug level 0|1
602         use_ok_bkp_only => 1,   # dont use bad backup
603         bweb     => 'http://localhost/cgi-bin/bweb/bweb.pl', # bweb url
604         glade_file => $glade_file,
605         see_all_versions => 0,  # display all file versions in FileInfo
606         mozilla  => 'mozilla',  # mozilla bin
607         default_restore_job => 'restore', # regular expression to select default
608                                    # restore job
609
610         # keywords that are used to fill DlgPref
611         chk_keyword =>  [ qw/use_ok_bkp_only debug see_all_versions/ ],
612         entry_keyword => [ qw/username password bweb mozilla
613                           connection_string default_restore_job
614                           bconsole bsr_dest glade_file/],
615     };
616
617     $self->read_config();
618
619     return $self;
620 }
621
622 sub read_config
623 {
624     my ($self) = @_;
625
626     # We read the parameters. They come from the configuration files
627     my $cfgfile ; my $tmpbuffer;
628     if (open FICCFG, $self->{config_file})
629     {
630         while(read FICCFG,$tmpbuffer,4096)
631         {
632             $cfgfile .= $tmpbuffer;
633         }
634         close FICCFG;
635         my $refparams;
636         no strict; # I have no idea of the contents of the file
637         eval '$refparams' . " = $cfgfile";
638         use strict;
639         
640         for my $p (keys %{$refparams}) {
641             $self->{$p} = $refparams->{$p};
642         }
643
644         if (defined $self->{debug}) {
645             $debug = $self->{debug} ;
646         }
647     } else {
648         # TODO : Force dumb default values and display a message
649     }
650 }
651
652 sub write_config
653 {
654     my ($self) = @_;
655     
656     my %parameters;
657
658     for my $k (@{ $self->{entry_keyword} }) { 
659         $parameters{$k} = $self->{$k};
660     }
661
662     for my $k (@{ $self->{chk_keyword} }) { 
663         $parameters{$k} = $self->{$k};
664     }
665
666     if (open FICCFG,">$self->{config_file}")
667     {
668         print FICCFG Data::Dumper->Dump([\%parameters], [qw($parameters)]);
669         close FICCFG;
670     }
671     else
672     {
673         # TODO : Display a message
674     }
675 }
676
677 sub connect_db
678 {
679     my $self = shift ;
680
681     if ($self->{dbh}) {
682         $self->{dbh}->disconnect() ;
683     }
684
685     delete $self->{dbh};
686     delete $self->{error};
687
688     if (not $self->{connection_string})
689     {
690         # The parameters have not been set. Maybe the conf
691         # file is empty for now
692         $self->{error} = "No configuration found for database connection. " .
693                          "Please set this up.";
694         return 0;
695     }
696     
697     if (not eval {
698         $self->{dbh} = DBI->connect($self->{connection_string}, 
699                                     $self->{username},
700                                     $self->{password})
701         })
702     {
703         $self->{error} = "Can't open bacula database. " . 
704                          "Database connect string '" . 
705                          $self->{connection_string} ."' $!";
706         return 0;
707     }
708     $self->{dbh}->{RowCacheSize}=100;
709     return 1;
710 }
711
712 sub go_bweb
713 {    
714     my ($self, $url, $msg) = @_;
715
716     unless ($self->{mozilla} and $self->{bweb}) {
717         new DlgWarn("You must install Bweb and set your mozilla bin to $msg");
718         return -1;
719     }
720
721     if ($^O eq 'MSWin32') {
722         system("start /B $self->{mozilla} \"$self->{bweb}$url\"");
723
724     } elsif (!fork()) {
725         system("$self->{mozilla} -remote 'Ping()'");
726         my $cmd = "$self->{mozilla} '$self->{bweb}$url'";
727         if ($? == 0) {
728             $cmd = "$self->{mozilla} -remote 'OpenURL($self->{bweb}$url,new-tab)'" ;
729         }
730         exec($cmd);
731         exit 1;
732     } 
733     return ($? == 0);
734 }
735
736 1;
737
738 ################################################################
739 # Manage preference
740 package DlgPref;
741
742 # my $pref = new Pref(config_file => 'brestore.conf');
743 # my $dlg = new DlgPref($pref);
744 # my $dlg_resto = new DlgResto($pref);
745 # $dlg->display($dlg_resto);
746 sub new
747 {
748     my ($class, $pref) = @_;
749
750     my $self = bless {
751         pref => $pref,          # Pref ref
752         dlgresto => undef,      # DlgResto ref
753         };
754
755     return $self;
756 }
757
758 sub display
759 {
760     my ($self, $dlgresto) = @_ ;
761
762     unless ($self->{glade}) {
763         $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_pref") ;
764         $self->{glade}->signal_autoconnect_from_package($self);
765     }
766
767     $self->{dlgresto} = $dlgresto;
768
769     my $g = $self->{glade};
770     my $p = $self->{pref};
771
772     for my $k (@{ $p->{entry_keyword} }) {
773         $g->get_widget("entry_$k")->set_text($p->{$k}) ;
774     }
775
776     for my $k (@{ $p->{chk_keyword} }) {
777         $g->get_widget("chkbp_$k")->set_active($p->{$k}) ;
778     }
779
780     $g->get_widget("dlg_pref")->show_all() ;
781 }
782
783 sub on_applybutton_clicked
784 {
785     my ($self) = @_;
786     my $glade = $self->{glade};
787     my $pref  = $self->{pref};
788
789     for my $k (@{ $pref->{entry_keyword} }) {
790         my $w = $glade->get_widget("entry_$k") ;
791         $pref->{$k} = $w->get_text();
792     }
793
794     for my $k (@{ $pref->{chk_keyword} }) {
795         my $w = $glade->get_widget("chkbp_$k") ;
796         $pref->{$k} = $w->get_active();
797     }
798
799     $pref->write_config();
800     if ($pref->connect_db()) {
801         $self->{dlgresto}->set_dbh($pref->{dbh});
802         $self->{dlgresto}->set_status('Preferences updated');
803         $self->{dlgresto}->init_server_backup_combobox();
804         $self->{dlgresto}->create_brestore_tables();
805         $self->{dlgresto}->set_status($pref->{error});
806     } else {
807         $self->{dlgresto}->set_status($pref->{error});
808     }
809 }
810
811 # Handle prefs ok click (apply/dismiss dialog)
812 sub on_okbutton_clicked 
813 {
814     my ($self) = @_;
815     $self->on_applybutton_clicked();
816
817     unless ($self->{pref}->{error}) {
818         $self->on_cancelbutton_clicked();
819     }
820 }
821 sub on_dialog_delete_event
822 {
823     my ($self) = @_;
824     $self->on_cancelbutton_clicked();
825     1;
826 }
827
828 sub on_cancelbutton_clicked
829 {
830     my ($self) = @_;
831     $self->{glade}->get_widget('dlg_pref')->hide();
832     delete $self->{dlgresto};
833 }
834 1;
835
836 ################################################################
837 # Main Interface
838
839 package DlgResto;
840
841 our $diricon;
842 our $fileicon;
843 our $yesicon;
844 our $noicon;
845
846 # Kept as is from the perl-gtk example. Draws the pretty icons
847 sub render_icons 
848 {
849     my $self = shift;
850     unless ($diricon) {
851         my $size = 'button';
852         $diricon  = $self->{mainwin}->render_icon('gtk-open', $size); 
853         $fileicon = $self->{mainwin}->render_icon('gtk-new',  $size);
854         $yesicon  = $self->{mainwin}->render_icon('gtk-yes',  $size); 
855         $noicon   = $self->{mainwin}->render_icon('gtk-no',   $size);
856     }
857 }
858
859 # init combo (and create ListStore object)
860 sub init_combo
861 {
862     my ($widget, @type) = @_ ;
863     my %type_info = ('text' => 'Glib::String',
864                      'markup' => 'Glib::String',
865                      ) ;
866     
867     my $lst = new Gtk2::ListStore ( map { $type_info{$_} } @type );
868
869     $widget->set_model($lst);
870     my $i=0;
871     for my $t (@type) {
872         my $cell;
873         if ($t eq 'text' or $t eq 'markup') {
874             $cell = new Gtk2::CellRendererText();
875         }
876         $widget->pack_start($cell, 1);
877         $widget->add_attribute($cell, $t, $i++);
878     }
879     return $lst;
880 }
881
882 # fill simple combo (one element per row)
883 sub fill_combo
884 {
885     my ($list, @what) = @_;
886
887     $list->clear();
888     
889     foreach my $w (@what)
890     {
891         chomp($w);
892         my $i = $list->append();
893         $list->set($i, 0, $w);
894     }
895 }
896
897 # display Mb/Gb/Kb
898 sub human
899 {
900     my @unit = qw(b Kb Mb Gb Tb);
901     my $val = shift;
902     my $i=0;
903     my $format = '%i %s';
904     while ($val / 1024 > 1) {
905         $i++;
906         $val /= 1024;
907     }
908     $format = ($i>0)?'%0.1f %s':'%i %s';
909     return sprintf($format, $val, $unit[$i]);
910 }
911
912 sub set_dbh
913 {
914     my ($self, $dbh) = @_;
915     $self->{dbh} = $dbh;
916 }
917
918 sub init_drag_drop
919 {
920     my ($fileview) = shift;
921     my $fileview_target_entry = {target => 'STRING',
922                                  flags => ['GTK_TARGET_SAME_APP'],
923                                  info => 40 };
924
925     $fileview->enable_model_drag_source(['button1_mask', 'button3_mask'],
926                                         ['copy'],$fileview_target_entry);
927     $fileview->get_selection->set_mode('multiple');
928
929     # set some useful SimpleList properties    
930     $fileview->set_headers_clickable(0);
931     foreach ($fileview->get_columns()) 
932     {
933         $_->set_resizable(1);
934         $_->set_sizing('grow-only');
935     }
936 }
937
938 sub debug
939 {
940     my ($self, $what) = @_;
941
942     if ($debug) {
943         if (ref $what) {
944             print Data::Dumper::Dumper($what);
945         } elsif (defined $what) {
946             print "$what\n";
947         }
948     }
949 }
950
951 sub dbh_prepare
952 {
953     my ($self, $query) = @_;
954     $self->debug($query);
955     return $self->{dbh}->prepare($query);    
956 }
957
958 sub dbh_do
959 {
960     my ($self, $query) = @_;
961     $self->debug($query);
962     return $self->{dbh}->do($query);
963 }
964
965 sub dbh_selectall_arrayref
966 {
967     my ($self, $query) = @_;
968     $self->debug($query);
969     return $self->{dbh}->selectall_arrayref($query);
970 }
971
972 sub dbh_selectrow_arrayref
973 {
974     my ($self, $query) = @_;
975     $self->debug($query);
976     return $self->{dbh}->selectrow_arrayref($query);
977 }
978
979 sub new
980 {
981     my ($class, $pref) = @_;
982     my $self = bless { 
983         pref => $pref,
984         dirtree => undef,
985         CurrentJobIds => [],
986         location => undef,      # location entry widget
987         mainwin  => undef,      # mainwin widget
988         filelist_file_menu => undef, # file menu widget
989         filelist_dir_menu => undef,  # dir menu widget
990         glade => undef,         # glade object
991         status => undef,        # status bar widget
992         dlg_pref => undef,      # DlgPref object
993         fileattrib => {},       # cache file
994         fileview   => undef,    # fileview widget SimpleList
995         fileinfo   => undef,    # fileinfo widget SimpleList
996         cwd   => '/',
997         client_combobox => undef, # client_combobox widget
998         restore_backup_combobox => undef, # date combobox widget
999         list_client => undef,   # Gtk2::ListStore
1000         list_backup => undef,   # Gtk2::ListStore
1001         cache_ppathid => {},    # 
1002     };
1003
1004     # load menu (to use handler with self reference)
1005     my $glade = Gtk2::GladeXML->new($glade_file, "filelist_file_menu");
1006     $glade->signal_autoconnect_from_package($self);
1007     $self->{filelist_file_menu} = $glade->get_widget("filelist_file_menu");
1008
1009     $glade = Gtk2::GladeXML->new($glade_file, "filelist_dir_menu");
1010     $glade->signal_autoconnect_from_package($self);
1011     $self->{filelist_dir_menu} = $glade->get_widget("filelist_dir_menu");
1012
1013     $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_resto");
1014     $glade->signal_autoconnect_from_package($self);
1015
1016     $self->{status}  = $glade->get_widget('statusbar');
1017     $self->{mainwin} = $glade->get_widget('dlg_resto');
1018     $self->{location} = $glade->get_widget('entry_location');
1019     $self->render_icons();
1020
1021     $self->{dlg_pref} = new DlgPref($pref);
1022
1023     my $c = $self->{client_combobox} = $glade->get_widget('combo_client');    
1024     $self->{list_client} = init_combo($c, 'text');
1025
1026     $c = $self->{restore_backup_combobox} = $glade->get_widget('combo_list_backups');
1027     $self->{list_backup} = init_combo($c, 'text', 'markup');
1028  
1029     # Connect glade-fileview to Gtk2::SimpleList
1030     # and set up drag n drop between $fileview and $restore_list
1031
1032     # WARNING : we have big dirty thinks with gtk/perl and utf8/iso strings
1033     # we use an hidden field uuencoded to bypass theses bugs (h_name)
1034
1035     my $widget = $glade->get_widget('fileview');
1036     my $fileview = $self->{fileview} = Gtk2::SimpleList->new_from_treeview(
1037                                               $widget,
1038                                               'h_name'        => 'hidden',
1039                                               'h_jobid'       => 'hidden',
1040                                               'h_type'        => 'hidden',
1041
1042                                               ''              => 'pixbuf',
1043                                               'File Name'     => 'text',
1044                                               'Size'          => 'text',
1045                                               'Date'          => 'text');
1046     init_drag_drop($fileview);
1047     $fileview->set_search_column(4); # search on File Name
1048
1049     # Connect glade-restore_list to Gtk2::SimpleList
1050     $widget = $glade->get_widget('restorelist');
1051     my $restore_list = $self->{restore_list} = Gtk2::SimpleList->new_from_treeview(
1052                                               $widget,
1053                                               'h_name'      => 'hidden',
1054                                               'h_jobid'     => 'hidden',
1055                                               'h_type'      => 'hidden',
1056                                               'h_curjobid'  => 'hidden',
1057
1058                                               ''            => 'pixbuf',
1059                                               'File Name'   => 'text',
1060                                               'JobId'       => 'text',
1061                                               'FileIndex'   => 'text',
1062
1063                                               'Nb Files'    => 'text', #8
1064                                               'Size'        => 'text', #9
1065                                               'size_b'      => 'hidden', #10
1066                                               );
1067
1068     my @restore_list_target_table = ({'target' => 'STRING',
1069                                       'flags' => [], 
1070                                       'info' => 40 });  
1071
1072     $restore_list->enable_model_drag_dest(['copy'],@restore_list_target_table);
1073     $restore_list->get_selection->set_mode('multiple');
1074     
1075     $widget = $glade->get_widget('infoview');
1076     my $infoview = $self->{fileinfo} = Gtk2::SimpleList->new_from_treeview(
1077                    $widget,
1078                    'h_name'        => 'hidden',
1079                    'h_jobid'       => 'hidden',
1080                    'h_type'        => 'hidden',
1081
1082                    'InChanger'     => 'pixbuf',
1083                    'Volume'        => 'text',
1084                    'JobId'         => 'text',
1085                    'Size'          => 'text',
1086                    'Date'          => 'text',
1087                    'MD5'           => 'text');
1088
1089     init_drag_drop($infoview);
1090
1091     $pref->connect_db() ||  $self->{dlg_pref}->display($self);
1092
1093     if ($pref->{dbh}) {
1094         $self->{dbh} = $pref->{dbh};
1095         $self->init_server_backup_combobox();
1096         $self->create_brestore_tables();
1097     }
1098
1099     $self->set_status($pref->{error});
1100 }
1101
1102 # set status bar informations
1103 sub set_status
1104 {
1105     my ($self, $string) = @_;
1106     return unless ($string);
1107
1108     my $context = $self->{status}->get_context_id('Main');
1109     $self->{status}->push($context, $string);
1110 }
1111
1112 sub on_time_select_changed
1113 {
1114     my ($self) = @_;
1115 }
1116
1117 sub get_active_time
1118 {
1119     my ($self) = @_;
1120     my $c = $self->{glade}->get_widget('combo_time');
1121     return $c->get_active_text;
1122 }
1123
1124 # This sub returns all clients declared in DB
1125 sub get_all_clients
1126 {
1127     my $dbh = shift;
1128     my $query = "SELECT Name FROM Client ORDER BY Name";
1129     print STDERR $query,"\n" if $debug;
1130
1131     my $result = $dbh->selectall_arrayref($query);
1132
1133     return map { $_->[0] } @$result;
1134 }
1135
1136 sub get_wanted_job_status
1137 {
1138     my ($ok_only) = @_;
1139
1140     if ($ok_only) {
1141         return "'T'";
1142     } else {
1143         return "'T', 'A', 'E'";
1144     }
1145 }
1146
1147 # This sub gives a full list of the EndTimes for a ClientId
1148 # ( [ 'Date', 'FileSet', 'Type', 'Status', 'JobId'], 
1149 #   ['Date', 'FileSet', 'Type', 'Status', 'JobId']..)
1150 sub get_all_endtimes_for_job
1151 {
1152     my ($dbh, $client, $ok_only)=@_;
1153     my $status = get_wanted_job_status($ok_only);
1154     my $query = "
1155  SELECT Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus, Job.JobId
1156   FROM Job,Client,FileSet
1157   WHERE Job.ClientId=Client.ClientId
1158   AND Client.Name = '$client'
1159   AND Job.Type = 'B'
1160   AND JobStatus IN ($status)
1161   AND Job.FileSetId = FileSet.FileSetId
1162   ORDER BY EndTime desc";
1163     print STDERR $query,"\n" if $debug;
1164     my $result = $dbh->selectall_arrayref($query);
1165
1166     return @$result;
1167 }
1168
1169
1170 # init infoview widget
1171 sub clear_infoview
1172 {
1173     my $self = shift;
1174     @{$self->{fileinfo}->{data}} = ();
1175 }
1176
1177 # init restore_list
1178 sub on_clear_clicked
1179 {
1180     my $self = shift;
1181     @{$self->{restore_list}->{data}} = ();
1182 }
1183
1184 sub on_estimate_clicked
1185 {
1186     my ($self) = @_;
1187
1188     my $size_total=0;
1189     my $nb_total=0;
1190
1191     # TODO : If we get here, things could get lenghty ... draw a popup window .
1192     my $widget = Gtk2::MessageDialog->new($self->{mainwin}, 
1193                                           'destroy-with-parent', 
1194                                           'info', 'close', 
1195                                           'Computing size...');
1196     $widget->show;
1197     refresh_screen();
1198
1199     my $title = "Computing size...\n";
1200     my $txt="";
1201     foreach my $entry (@{$self->{restore_list}->{data}})
1202     {
1203         unless ($entry->[9]) {
1204             my ($size, $nb) = $self->estimate_restore_size($entry);
1205             $entry->[10] = $size;
1206             $entry->[9] = human($size);
1207             $entry->[8] = $nb;
1208         }
1209
1210         my $name = unpack('u', $entry->[0]);
1211
1212         $txt .= "\n<i>$name</i> : " . $entry->[8] . " file(s)/" . $entry->[9] ;
1213         $widget->set_markup($title . $txt);
1214         
1215         $size_total+=$entry->[10];
1216         $nb_total+=$entry->[8];
1217         refresh_screen();
1218     }
1219     
1220     $txt .= "\n\n<b>Total</b> : $nb_total file(s)/" . human($size_total);
1221     $widget->set_markup("Size estimation :\n" . $txt);
1222     $widget->signal_connect ("response", sub { my $w=shift; $w->destroy();});
1223
1224     return 0;
1225 }
1226
1227 sub on_gen_bsr_clicked
1228 {
1229     my ($self) = @_;
1230     
1231     my @options = ("Choose a bsr file", $self->{mainwin}, 'save', 
1232                    'gtk-save','ok', 'gtk-cancel', 'cancel');
1233
1234     
1235     my $w = new Gtk2::FileChooserDialog ( @options );
1236     my $ok = 0;
1237     my $save;
1238     while (!$ok) {
1239         my $a = $w->run();
1240         if ($a eq 'cancel') {
1241             $ok = 1;
1242         }
1243
1244         if ($a eq 'ok') {
1245             my $f = $w->get_filename();
1246             if (-f $f) {
1247                 my $dlg = Gtk2::MessageDialog->new($self->{mainwin}, 
1248                                                    'destroy-with-parent', 
1249                                                    'warning', 'ok-cancel', 'This file already exists, do you want to overwrite it ?');
1250                 if ($dlg->run() eq 'ok') {
1251                     $save = $f;
1252                 }
1253                 $dlg->destroy();
1254             } else {
1255                 $save = $f;
1256             }
1257             $ok = 1;
1258         }
1259     }
1260
1261     $w->destroy();
1262     
1263     if ($save) {
1264         if (open(FP, ">$save")) {
1265             my $bsr = $self->create_filelist();
1266             print FP $bsr;
1267             close(FP);
1268             $self->set_status("Dumping BSR to $save ok");
1269         } else {
1270             $self->set_status("Can't dump BSR to $save: $!");
1271         }
1272     }
1273 }
1274
1275 use File::Temp qw/tempfile/;
1276
1277 sub on_go_button_clicked 
1278 {
1279     my $self = shift;
1280     unless (scalar(@{$self->{restore_list}->{data}})) {
1281         new DlgWarn("No file to restore");
1282         return 0;
1283     }
1284     my $bsr = $self->create_filelist();
1285     my ($fh, $filename) = tempfile();
1286     $fh->print($bsr);
1287     close($fh);
1288     chmod(0644, $filename);
1289
1290     print "Dumping BSR info to $filename\n"
1291         if ($debug);
1292
1293     # we get Volume list
1294     my %a = map { $_ => 1 } ($bsr =~ /Volume="(.+)"/g);
1295     my $vol = [ keys %a ] ;     # need only one occurrence of each volume
1296
1297     new DlgLaunch(pref     => $self->{pref},
1298                   volumes  => $vol,
1299                   bsr_file => $filename,
1300                   );
1301
1302 }
1303
1304 our $client_list_empty = 'Clients list'; 
1305 our %type_markup = ('F' => '<b>$label F</b>',
1306                     'D' => '$label D',
1307                     'I' => '$label I',
1308                     'B' => '<b>$label B</b>',
1309
1310                     'A' => '<span foreground=\"red\">$label</span>',
1311                     'T' => '$label',
1312                     'E' => '<span foreground=\"red\">$label</span>',
1313                     );
1314
1315 sub on_list_client_changed 
1316 {
1317     my ($self, $widget) = @_;
1318     return 0 unless defined $self->{fileview};
1319     my $dbh = $self->{dbh};
1320
1321     $self->{list_backup}->clear();
1322
1323     if ($self->current_client eq $client_list_empty) {
1324         return 0 ;
1325     }
1326
1327     my @endtimes=get_all_endtimes_for_job($dbh, 
1328                                           $self->current_client,
1329                                           $self->{pref}->{use_ok_bkp_only});
1330     foreach my $endtime (@endtimes)
1331     {
1332         my $i = $self->{list_backup}->append();
1333
1334         my $label = $endtime->[1] . " (" . $endtime->[4] . ")";
1335         eval "\$label = \"$type_markup{$endtime->[2]}\""; # job type
1336         eval "\$label = \"$type_markup{$endtime->[3]}\""; # job status
1337
1338         $self->{list_backup}->set($i, 
1339                                   0, $endtime->[0],
1340                                   1, $label,
1341                                   );
1342     }
1343     $self->{restore_backup_combobox}->set_active(0);
1344
1345     $self->{CurrentJobIds} = [
1346                               set_job_ids_for_date($dbh,
1347                                                    $self->current_client,
1348                                                    $self->current_date,
1349                                                    $self->{pref}->{use_ok_bkp_only})
1350                               ];
1351
1352     $self->update_brestore_table(@{$self->{CurrentJobIds}});
1353
1354     $self->ch_dir('');
1355     $self->refresh_fileview();
1356     0;
1357 }
1358
1359 sub fill_server_list
1360 {
1361     my ($dbh, $combo, $list) = @_;
1362
1363     my @clients=get_all_clients($dbh);
1364
1365     $list->clear();
1366     
1367     my $i = $list->append();
1368     $list->set($i, 0, $client_list_empty);
1369     
1370     foreach my $client (@clients)
1371     {
1372         $i = $list->append();
1373         $list->set($i, 0, $client);
1374     }
1375     $combo->set_active(0);
1376 }
1377
1378 sub init_server_backup_combobox
1379 {
1380     my $self = shift ;
1381     fill_server_list($self->{dbh}, 
1382                      $self->{client_combobox},
1383                      $self->{list_client}) ;
1384 }
1385
1386 #----------------------------------------------------------------------
1387 #Refreshes the file-view Redraws everything. The dir data is cached, the file
1388 #data isn't.  There is additionnal complexity for dirs (visibility problems),
1389 #so the @CurrentJobIds is not sufficient.
1390 sub refresh_fileview 
1391 {
1392     my ($self) = @_;
1393     my $fileview = $self->{fileview};
1394     my $client_combobox = $self->{client_combobox};
1395     my $cwd = $self->{cwd};
1396
1397     @{$fileview->{data}} = ();
1398
1399     $self->clear_infoview();
1400     
1401     my $client_name = $self->current_client;
1402
1403     if (!$client_name or ($client_name eq $client_list_empty)) {
1404         $self->set_status("Client list empty");
1405         return;
1406     }
1407
1408     my @list_dirs     = $self->list_dirs($cwd,$client_name);
1409     # [ [listfiles.id, listfiles.Name, File.LStat, File.JobId]..]
1410     my $files    = $self->list_files($cwd); 
1411     print "CWD : $cwd\n" if ($debug);
1412     
1413     my $file_count = 0 ;
1414     my $total_bytes = 0;
1415     
1416     # Add directories to view
1417     foreach my $dir_entry (@list_dirs) {
1418         #my $time = localtime($self->dir_attrib("$cwd/$dir",'st_mtime'));
1419         my $time = localtime(lstat_attrib($dir_entry->[1],'st_mtime'));
1420         my $dir = $dir_entry->[0];
1421         $total_bytes += 4096;
1422         $file_count++;
1423
1424         listview_push($fileview,
1425                       $dir,
1426                       $self->dir_attrib("$cwd/$dir",'jobid'),
1427                       'dir',
1428
1429                       $diricon, 
1430                       $dir, 
1431                       "4 Kb", 
1432                       $time);
1433     }
1434     
1435     # Add files to view 
1436     foreach my $file (@$files) 
1437     {
1438         my $size = file_attrib($file,'st_size');
1439         my $time = localtime(file_attrib($file,'st_mtime'));
1440         $total_bytes += $size;
1441         $file_count++;
1442         # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
1443
1444         listview_push($fileview,
1445                       $file->[1],
1446                       $file->[3],
1447                       'file',
1448                       
1449                       $fileicon, 
1450                       $file->[1], 
1451                       human($size), $time);
1452     }
1453     
1454     $self->set_status("$file_count files/" . human($total_bytes));
1455
1456     # set a decent default selection (makes keyboard nav easy)
1457     $fileview->select(0);
1458 }
1459
1460
1461 sub on_about_activate
1462 {
1463     DlgAbout::display();
1464 }
1465
1466 sub drag_set_info
1467 {
1468     my ($tree, $path, $data) = @_;
1469
1470     my @items = listview_get_all($tree) ;
1471     my @ret;
1472     foreach my $i (@items)
1473     {
1474         my @file_info = @{$i};
1475
1476         # doc ligne 93
1477         # Ok, we have a corner case :
1478         # path can be empty
1479         my $file;
1480         if ($path eq '')
1481         {
1482             $file = pack("u", $file_info[0]);
1483         }
1484         else
1485         {
1486                 $file = pack("u", $path . '/' . $file_info[0]);
1487         }
1488         push @ret, join(" ; ", $file, 
1489                         $file_info[1], # $jobid
1490                         $file_info[2], # $type
1491                         );
1492     }
1493
1494     my $data_get = join(" :: ", @ret);
1495     
1496     $data->set_text($data_get,-1);
1497 }
1498
1499 sub fileview_data_get
1500 {
1501     my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1502     drag_set_info($widget, $self->{cwd}, $data);
1503 }
1504
1505 sub fileinfo_data_get
1506 {
1507     my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1508     drag_set_info($widget, $self->{cwd}, $data);
1509 }
1510
1511 sub restore_list_data_received
1512 {
1513     my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_;
1514     my @ret;
1515
1516     if  ($info eq 40 || $info eq 0) # patch for display!=:0
1517     {
1518         foreach my $elt (split(/ :: /, $data->data()))
1519         {
1520             
1521             my ($file, $jobid, $type) = 
1522                 split(/ ; /, $elt);
1523             $file = unpack("u", $file);
1524             
1525             $self->add_selected_file_to_list($file, $jobid, $type);
1526         }
1527     }
1528 }
1529
1530 sub on_back_button_clicked {
1531     my $self = shift;
1532     $self->up_dir();
1533 }
1534 sub on_location_go_button_clicked 
1535 {
1536     my $self = shift; 
1537     $self->ch_dir($self->{location}->get_text());
1538 }
1539 sub on_quit_activate {Gtk2->main_quit;}
1540 sub on_preferences_activate
1541 {
1542     my $self = shift; 
1543     $self->{dlg_pref}->display($self) ;
1544 }
1545 sub on_main_delete_event {Gtk2->main_quit;}
1546 sub on_bweb_activate
1547 {
1548     my $self = shift; 
1549     $self->set_status("Open bweb on your browser");
1550     $self->{pref}->go_bweb('', "go on bweb");
1551 }
1552
1553 # Change to parent directory
1554 sub up_dir
1555 {
1556     my $self = shift ;
1557     if ($self->{cwd} eq '/')
1558     {
1559         $self->ch_dir('');
1560     }
1561     my @dirs = split(/\//, $self->{cwd});
1562     pop @dirs;
1563     $self->ch_dir(join('/', @dirs));
1564 }
1565
1566 # Change the current working directory
1567 #   * Updates fileview, location, and selection
1568 #
1569 sub ch_dir 
1570 {
1571     my $self = shift;
1572     $self->{cwd} = shift;
1573     
1574     $self->refresh_fileview();
1575     $self->{location}->set_text($self->{cwd});
1576     
1577     1;
1578 }
1579
1580 # Handle dialog 'close' (window-decoration induced close)
1581 #   * Just hide the dialog, and tell Gtk not to do anything else
1582 #
1583 sub on_delete_event 
1584 {
1585     my ($self, $w) = @_;
1586     $w->hide; 
1587     Gtk2::main_quit();
1588     1; # consume this event!
1589 }
1590
1591 # Handle key presses in location text edit control
1592 #   * Translate a Return/Enter key into a 'Go' command
1593 #   * All other key presses left for GTK
1594 #
1595 sub on_location_entry_key_release_event 
1596 {
1597     my $self = shift;
1598     my $widget = shift;
1599     my $event = shift;
1600     
1601     my $keypress = $event->keyval;
1602     if ($keypress == $Gtk2::Gdk::Keysyms{KP_Enter} ||
1603         $keypress == $Gtk2::Gdk::Keysyms{Return}) 
1604     {
1605         $self->ch_dir($widget->get_text());
1606         
1607         return 1; # consume keypress
1608     }
1609
1610     return 0; # let gtk have the keypress
1611 }
1612
1613 sub on_fileview_key_press_event
1614 {
1615     my ($self, $widget, $event) = @_;
1616     return 0;
1617 }
1618
1619 sub listview_get_first
1620 {
1621     my ($list) = shift; 
1622     my @selected = $list->get_selected_indices();
1623     if (@selected > 0) {
1624         my ($name, @other) = @{$list->{data}->[$selected[0]]};
1625         return (unpack('u', $name), @other);
1626     } else {
1627         return undef;
1628     }
1629 }
1630
1631 sub listview_get_all
1632 {
1633     my ($list) = shift; 
1634
1635     my @selected = $list->get_selected_indices();
1636     my @ret;
1637     for my $i (@selected) {
1638         my ($name, @other) = @{$list->{data}->[$i]};
1639         push @ret, [unpack('u', $name), @other];
1640     } 
1641     return @ret;
1642 }
1643
1644
1645 sub listview_push
1646 {
1647     my ($list, $name, @other) = @_;
1648     push @{$list->{data}}, [pack('u', $name), @other];
1649 }
1650
1651 #----------------------------------------------------------------------
1652 # Handle keypress in file-view
1653 #   * Translates backspace into a 'cd ..' command 
1654 #   * All other key presses left for GTK
1655 #
1656 sub on_fileview_key_release_event 
1657 {
1658     my ($self, $widget, $event) = @_;
1659     if (not $event->keyval)
1660     {
1661         return 0;
1662     }
1663     if ($event->keyval == $Gtk2::Gdk::Keysyms{BackSpace}) {
1664         $self->up_dir();
1665         return 1; # eat keypress
1666     }
1667
1668     return 0; # let gtk have keypress
1669 }
1670
1671 sub on_forward_keypress
1672 {
1673     return 0;
1674 }
1675
1676 #----------------------------------------------------------------------
1677 # Handle double-click (or enter) on file-view
1678 #   * Translates into a 'cd <dir>' command
1679 #
1680 sub on_fileview_row_activated 
1681 {
1682     my ($self, $widget) = @_;
1683     
1684     my ($name, undef, $type, undef) = listview_get_first($widget);
1685
1686     if ($type eq 'dir')
1687     {
1688         if ($self->{cwd} eq '')
1689         {
1690                 $self->ch_dir($name);
1691         }
1692         elsif ($self->{cwd} eq '/')
1693         {
1694                 $self->ch_dir('/' . $name);
1695         }
1696         else
1697         {
1698                 $self->ch_dir($self->{cwd} . '/' . $name);
1699         }
1700
1701     } else {
1702         $self->fill_infoview($self->{cwd}, $name);
1703     }
1704     
1705     return 1; # consume event
1706 }
1707
1708 sub fill_infoview
1709 {
1710     my ($self, $path, $file) = @_;
1711     $self->clear_infoview();
1712     my @v = get_all_file_versions($self->{dbh}, 
1713                                   "$path/", 
1714                                   $file,
1715                                   $self->current_client,
1716                                   $self->{pref}->{see_all_versions});
1717     for my $ver (@v) {
1718         my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
1719             = @{$ver};
1720         my $icon = ($inchanger)?$yesicon:$noicon;
1721
1722         $mtime = localtime($mtime) ;
1723
1724         listview_push($self->{fileinfo},
1725                       $file, $jobid, 'file', 
1726                       $icon, $volname, $jobid, human($size), $mtime, $md5);
1727     }
1728 }
1729
1730 sub current_date
1731 {
1732     my $self = shift ;
1733     return $self->{restore_backup_combobox}->get_active_text;
1734 }
1735
1736 sub current_client
1737 {
1738     my $self = shift ;
1739     return $self->{client_combobox}->get_active_text;
1740 }
1741
1742 sub on_list_backups_changed 
1743 {
1744     my ($self, $widget) = @_;
1745     return 0 unless defined $self->{fileview};
1746
1747     $self->{CurrentJobIds} = [
1748                               set_job_ids_for_date($self->{dbh},
1749                                                    $self->current_client,
1750                                                    $self->current_date,
1751                                                    $self->{pref}->{use_ok_bkp_only})
1752                               ];
1753     $self->update_brestore_table(@{$self->{CurrentJobIds}});
1754
1755     $self->refresh_fileview();
1756     0;
1757 }
1758
1759 sub on_restore_list_keypress
1760 {
1761     my ($self, $widget, $event) = @_;
1762     if ($event->keyval == $Gtk2::Gdk::Keysyms{Delete})
1763     {
1764         my @sel = $widget->get_selected_indices;
1765         foreach my $elt (reverse(sort {$a <=> $b} @sel))
1766         {
1767             splice @{$self->{restore_list}->{data}},$elt,1;
1768         }
1769     }
1770 }
1771
1772 sub on_fileview_button_press_event
1773 {
1774     my ($self,$widget,$event) = @_;
1775     if ($event->button == 3)
1776     {
1777         $self->on_right_click_filelist($widget,$event);
1778         return 1;
1779     }
1780     
1781     if ($event->button == 2)
1782     {
1783         $self->on_see_all_version();
1784         return 1;
1785     }
1786
1787     return 0;
1788 }
1789
1790 sub on_see_all_version
1791 {
1792     my ($self) = @_;
1793     
1794     my @lst = listview_get_all($self->{fileview});
1795
1796     for my $i (@lst) {
1797         my ($name, undef) = @{$i};
1798
1799         new DlgFileVersion($self->{dbh}, 
1800                            $self->current_client, 
1801                            $self->{cwd}, $name);
1802     }
1803 }
1804
1805 sub on_right_click_filelist
1806 {
1807     my ($self,$widget,$event) = @_;
1808     # I need to know what's selected
1809     my @sel = listview_get_all($self->{fileview});
1810     
1811     my $type = '';
1812
1813     if (@sel == 1) {
1814         $type = $sel[0]->[2];   # $type
1815     }
1816
1817     my $w;
1818
1819     if (@sel >=2 or $type eq 'dir')
1820     {
1821         # We have selected more than one or it is a directories
1822         $w = $self->{filelist_dir_menu};
1823     }
1824     else
1825     {
1826         $w = $self->{filelist_file_menu};
1827     }
1828     $w->popup(undef,
1829               undef,
1830               undef,
1831               undef,
1832               $event->button, $event->time);
1833 }
1834
1835 sub context_add_to_filelist
1836 {
1837     my ($self) = @_;
1838
1839     my @sel = listview_get_all($self->{fileview});
1840
1841     foreach my $i (@sel)
1842     {
1843         my ($file, $jobid, $type, undef) = @{$i};
1844         $file = $self->{cwd} . '/' . $file;
1845         $self->add_selected_file_to_list($file, $jobid, $type);
1846     }
1847 }
1848
1849 # Adds a file to the filelist
1850 sub add_selected_file_to_list
1851 {
1852     my ($self, $name, $jobid, $type)=@_;
1853
1854     my $dbh = $self->{dbh};
1855     my $restore_list = $self->{restore_list};
1856
1857     my $curjobids=join(',', @{$self->{CurrentJobIds}});
1858
1859     if ($type eq 'dir')
1860     {
1861         # dirty hack
1862         $name =~ s!^//+!/!;
1863
1864         if ($name and substr $name,-1 ne '/')
1865         {
1866                 $name .= '/'; # For bacula
1867         }
1868         my $dirfileindex = get_fileindex_from_dir_jobid($dbh,$name,$jobid);
1869         listview_push($restore_list, 
1870                       $name, $jobid, 'dir', $curjobids,
1871                       $diricon, $name,$curjobids,$dirfileindex);
1872     }
1873     elsif ($type eq 'file')
1874     {
1875         my $fileindex = get_fileindex_from_file_jobid($dbh,$name,$jobid);
1876
1877         listview_push($restore_list,
1878                       $name, $jobid, 'file', $curjobids,
1879                       $fileicon, $name, $jobid, $fileindex );
1880     }
1881 }
1882
1883 # TODO : we want be able to restore files from a bad ended backup
1884 # we have JobStatus IN ('T', 'A', 'E') and we must 
1885
1886 # Data acces subs from here. Interaction with SGBD and caching
1887
1888 # This sub retrieves the list of jobs corresponding to the jobs selected in the
1889 # GUI and stores them in @CurrentJobIds
1890 sub set_job_ids_for_date
1891 {
1892     my ($dbh, $client, $date, $only_ok)=@_;
1893
1894     if (!$client or !$date) {
1895         return ();
1896     }
1897     
1898     my $status = get_wanted_job_status($only_ok);
1899         
1900     # The algorithm : for a client, we get all the backups for each
1901     # fileset, in reverse order Then, for each fileset, we store the 'good'
1902     # incrementals and differentials until we have found a full so it goes
1903     # like this : store all incrementals until we have found a differential
1904     # or a full, then find the full #
1905
1906     my $query = "SELECT JobId, FileSet, Level, JobStatus
1907                 FROM Job, Client, FileSet
1908                 WHERE Job.ClientId = Client.ClientId
1909                 AND FileSet.FileSetId = Job.FileSetId
1910                 AND EndTime <= '$date'
1911                 AND Client.Name = '$client'
1912                 AND Type IN ('B')
1913                 AND JobStatus IN ($status)
1914                 ORDER BY FileSet, JobTDate DESC";
1915         
1916     print STDERR $query,"\n" if $debug;
1917     my @CurrentJobIds;
1918     my $result = $dbh->selectall_arrayref($query);
1919     my %progress;
1920     foreach my $refrow (@$result)
1921     {
1922         my $jobid = $refrow->[0];
1923         my $fileset = $refrow->[1];
1924         my $level = $refrow->[2];
1925                 
1926         defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
1927                 
1928         next if $progress{$fileset} eq 'F'; # It's over for this fileset...
1929                 
1930         if ($level eq 'I')
1931         {
1932             next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
1933             push @CurrentJobIds,($jobid);
1934         }
1935         elsif ($level eq 'D')
1936         {
1937             next if $progress{$fileset} eq 'D'; # We allready have a differential
1938             push @CurrentJobIds,($jobid);
1939         }
1940         elsif ($level eq 'F')
1941         {
1942             push @CurrentJobIds,($jobid);
1943         }
1944
1945         my $status = $refrow->[3] ;
1946         if ($status eq 'T') {              # good end of job
1947             $progress{$fileset} = $level;
1948         }
1949     }
1950     print Data::Dumper::Dumper(\@CurrentJobIds) if $debug;
1951
1952     return @CurrentJobIds;
1953 }
1954
1955 # Lists all directories contained inside a directory.
1956 # Uses the current dir, the client name, and CurrentJobIds for visibility.
1957 # Returns an array of dirs
1958 sub list_dirs
1959 {
1960     my ($self,$dir,$client)=@_;
1961
1962     print "list_dirs(<$dir>, <$client>)\n" if $debug;
1963
1964     if ($dir ne '' and substr $dir,-1 ne '/')
1965     {
1966         $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1967     }
1968
1969     my $dbh = $self->{dbh};
1970     my $query = "SELECT PathId FROM Path WHERE Path = ?
1971                  UNION SELECT PathId FROM brestore_missing_path WHERE PATH = ?";
1972     my $sth = $dbh->prepare($query);
1973     $sth->execute($dir,$dir);
1974     my $result = $sth->fetchrow_arrayref();
1975     $sth->finish();
1976     my $pathid = $result->[0];
1977     my @jobids = @{$self->{CurrentJobIds}};
1978     my $jobclause = join (',', @jobids);
1979     # Let's retrieve the list of the visible dirs in this dir ...
1980     # First, I need the empty filenameid to locate efficiently the dirs in the file table
1981     $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
1982     $sth = $dbh->prepare($query);
1983     $sth->execute();
1984     $result = $sth->fetchrow_arrayref();
1985     $sth->finish();
1986     my $dir_filenameid = $result->[0];
1987      
1988     # Then we get all the dir entries from File ...
1989     # It's ugly because there are records in brestore_missing_path ...
1990     $query = "
1991 SELECT Path, JobId, Lstat FROM(
1992     (
1993     SELECT Path.Path, lower(Path.Path), 
1994            listfile.JobId, listfile.Lstat
1995     FROM (
1996         SELECT DISTINCT brestore_pathhierarchy.PathId
1997         FROM brestore_pathhierarchy
1998         JOIN Path 
1999             ON (brestore_pathhierarchy.PathId = Path.PathId)
2000         JOIN brestore_pathvisibility 
2001             ON (brestore_pathhierarchy.PathId = brestore_pathvisibility.PathId)
2002         WHERE brestore_pathhierarchy.PPathId = $pathid
2003         AND brestore_pathvisibility.jobid IN ($jobclause)) AS listpath
2004     JOIN Path ON (listpath.PathId = Path.PathId)
2005     LEFT JOIN (
2006         SELECT File.PathId, File.JobId, File.Lstat FROM File
2007         WHERE File.FilenameId = $dir_filenameid
2008         AND File.JobId IN ($jobclause)) AS listfile
2009         ON (listpath.PathId = listfile.PathId)
2010     UNION
2011     SELECT brestore_missing_path.Path, lower(brestore_missing_path.Path), 
2012            listfile.JobId, listfile.Lstat
2013     FROM (
2014         SELECT DISTINCT brestore_pathhierarchy.PathId
2015         FROM brestore_pathhierarchy
2016         JOIN brestore_missing_path 
2017             ON (brestore_pathhierarchy.PathId = brestore_missing_path.PathId)
2018         JOIN brestore_pathvisibility 
2019             ON (brestore_pathhierarchy.PathId = brestore_pathvisibility.PathId)
2020         WHERE brestore_pathhierarchy.PPathId = $pathid
2021         AND brestore_pathvisibility.jobid IN ($jobclause)) AS listpath
2022     JOIN brestore_missing_path ON (listpath.PathId = brestore_missing_path.PathId)
2023     LEFT JOIN (
2024         SELECT File.PathId, File.JobId, File.Lstat FROM File
2025         WHERE File.FilenameId = $dir_filenameid
2026         AND File.JobId IN ($jobclause)) AS listfile
2027         ON (listpath.PathId = listfile.PathId))
2028 ORDER BY 2,3 DESC ) As a";
2029     print STDERR "$query\n" if $debug;
2030     $sth=$dbh->prepare($query);
2031     $sth->execute();
2032     $result = $sth->fetchall_arrayref();
2033     my @return_list;
2034     my $prev_dir='';
2035     foreach my $refrow (@{$result})
2036     {
2037         my $dir = $refrow->[0];
2038         my $jobid = $refrow->[1];
2039         my $lstat = $refrow->[2];
2040         next if ($dir eq $prev_dir);
2041         # We have to clean up this dirname ... we only want it's 'basename'
2042         my $return_value;
2043         if ($dir ne '/')
2044         {
2045             my @temp = split ('/',$dir);
2046             $return_value = pop @temp;
2047         }
2048         else
2049         {
2050             $return_value = '/';
2051         }
2052         my @return_array = ($return_value,$lstat);
2053         push @return_list,(\@return_array);
2054         $prev_dir = $dir;
2055     }
2056     return @return_list;    
2057 }
2058
2059
2060 # List all files in a directory. dir as parameter, CurrentJobIds for visibility
2061 # Returns an array of dirs
2062 sub list_files
2063 {
2064     my ($self, $dir)=@_;
2065     my $dbh = $self->{dbh};
2066
2067     my $empty = [];
2068
2069     print "list_files($dir)\n" if $debug;
2070
2071     if ($dir ne '' and substr $dir,-1 ne '/')
2072     {
2073         $dir .= '/'; # In the db, there is a / at the end of the dirs ...
2074     }
2075
2076     my $query = "SELECT Path.PathId 
2077                     FROM Path 
2078                     WHERE Path.Path = '$dir'
2079                  UNION 
2080                  SELECT brestore_missing_path.PathId 
2081                     FROM brestore_missing_path 
2082                     WHERE brestore_missing_path.Path = '$dir'";
2083     print $query,"\n" if $debug;
2084     my @list_pathid=();
2085     my $result = $dbh->selectall_arrayref($query);
2086     foreach my $refrow (@$result)
2087     {
2088         push @list_pathid,($refrow->[0]);
2089     }
2090         
2091     if  (@list_pathid == 0)
2092     {
2093         print "No pathid found for $dir\n" if $debug;
2094         return $empty;
2095     }
2096         
2097     my $inlistpath = join (',', @list_pathid);
2098     my $inclause = join (',', @{$self->{CurrentJobIds}});
2099     if ($inclause eq '')
2100     {
2101         return $empty;
2102     }
2103         
2104     $query = 
2105 "SELECT listfiles.id, listfiles.Name, File.LStat, File.JobId
2106  FROM
2107         (SELECT Filename.Name, max(File.FileId) as id
2108          FROM File, Filename
2109          WHERE File.FilenameId = Filename.FilenameId
2110            AND Filename.Name != ''
2111            AND File.PathId IN ($inlistpath)
2112            AND File.JobId IN ($inclause)
2113          GROUP BY Filename.Name
2114          ORDER BY Filename.Name) AS listfiles,
2115 File
2116 WHERE File.FileId = listfiles.id";
2117         
2118     print STDERR $query,"\n" if $debug;
2119     $result = $dbh->selectall_arrayref($query);
2120         
2121     return $result;
2122 }
2123
2124 sub refresh_screen
2125 {
2126     Gtk2->main_iteration while (Gtk2->events_pending);
2127 }
2128
2129 sub create_brestore_tables
2130 {
2131     my ($self) = @_;
2132
2133     my $verif = "SELECT 1 FROM brestore_knownjobid LIMIT 1";
2134
2135     unless ($self->dbh_do($verif)) {
2136         new DlgWarn("brestore can't find brestore_xxx tables on your database. I will create them.");
2137
2138         $self->{error} = "Creating internal brestore tables";
2139         my $req = "
2140     CREATE TABLE brestore_knownjobid
2141     (
2142      JobId int4 NOT NULL,
2143      CONSTRAINT brestore_knownjobid_pkey PRIMARY KEY (JobId)
2144     )";
2145         $self->dbh_do($req);
2146     }
2147     
2148     $verif = "SELECT 1 FROM brestore_pathhierarchy LIMIT 1";
2149     unless ($self->dbh_do($verif)) {
2150         my $req = "
2151    CREATE TABLE brestore_pathhierarchy
2152    (
2153      PathId int4 NOT NULL,
2154      PPathId int4 NOT NULL,
2155      CONSTRAINT brestore_pathhierarchy_pkey PRIMARY KEY (PathId)
2156    )";
2157         $self->dbh_do($req);
2158
2159
2160         $req = "CREATE INDEX brestore_pathhierarchy_ppathid 
2161                           ON brestore_pathhierarchy (PPathId)";
2162         $self->dbh_do($req);
2163     }
2164     
2165     $verif = "SELECT 1 FROM brestore_pathvisibility LIMIT 1";
2166     unless ($self->dbh_do($verif)) {
2167         my $req = "
2168     CREATE TABLE brestore_pathvisibility
2169     (
2170       PathId int4 NOT NULL,
2171       JobId int4 NOT NULL,
2172       CONSTRAINT brestore_pathvisibility_pkey PRIMARY KEY (JobId, PathId)
2173     )";
2174         $self->dbh_do($req);
2175
2176         $req = "CREATE INDEX brestore_pathvisibility_jobid
2177                           ON brestore_pathvisibility (JobId)";
2178         $self->dbh_do($req);
2179     }
2180     
2181     $verif = "SELECT 1 FROM brestore_missing_path LIMIT 1";
2182     unless ($self->dbh_do($verif)) {
2183         my $req = "
2184     CREATE TABLE brestore_missing_path
2185     (
2186       PathId int4 NOT NULL,
2187       Path text NOT NULL,
2188       CONSTRAINT brestore_missing_path_pkey PRIMARY KEY (PathId)
2189     )";
2190         $self->dbh_do($req);
2191
2192         $req = "CREATE INDEX brestore_missing_path_path
2193                           ON brestore_missing_path (Path)";
2194         $self->dbh_do($req);
2195     }
2196 }
2197
2198 # Recursive function to calculate the visibility of each directory in the cache
2199 # tree Working with references to save time and memory
2200 # For each directory, we want to propagate it's visible jobids onto it's
2201 # parents directory.
2202 # A tree is visible if
2203 # - it's been in a backup pointed by the CurrentJobIds
2204 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2205 # In the second case, the directory is visible but has no metadata.
2206 # We symbolize this with lstat = 1 for this jobid in the cache.
2207
2208 # Input : reference directory
2209 # Output : visibility of this dir. Has to know visibility of all subdirs
2210 # to know it's visibility, hence the recursing.
2211 sub list_visible
2212 {
2213     my ($refdir)=@_;
2214         
2215     my %visibility;
2216     # Get the subdirs array references list
2217     my @list_ref_subdirs;
2218     while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2219     {
2220         push @list_ref_subdirs,($ref_subdir);
2221     }
2222
2223     # Now lets recurse over these subdirs and retrieve the reference of a hash
2224     # containing the jobs where they are visible
2225     foreach my $ref_subdir (@list_ref_subdirs)
2226     {
2227         my $ref_list_jobs = list_visible($ref_subdir);
2228         foreach my $jobid (keys %$ref_list_jobs)
2229         {
2230             $visibility{$jobid}=1;
2231         }
2232     }
2233
2234     # Ok. Now, we've got the list of those jobs.  We are going to update our
2235     # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2236     # the lstat for the known jobids. Put 1 in the new elements...  But first,
2237     # let's store the current jobids
2238     my @known_jobids;
2239     foreach my $jobid (keys %{$refdir->[1]})
2240     {
2241         push @known_jobids,($jobid);
2242     }
2243     
2244     # Add the new jobs
2245     foreach my $jobid (keys %visibility)
2246     {
2247         next if ($refdir->[1]->{$jobid});
2248         $refdir->[1]->{$jobid} = 1;
2249     }
2250     # Add the known_jobids to %visibility
2251     foreach my $jobid (@known_jobids)
2252     {
2253         $visibility{$jobid}=1;
2254     }
2255     return \%visibility;
2256 }
2257
2258 # Returns the list of media required for a list of jobids.
2259 # Input : dbh, jobid1, jobid2...
2260 # Output : reference to array of (joibd, inchanger)
2261 sub get_required_media_from_jobid
2262 {
2263     my ($dbh, @jobids)=@_;
2264     my $inclause = join(',',@jobids);
2265     my $query = "
2266 SELECT DISTINCT JobMedia.MediaId, Media.InChanger 
2267 FROM JobMedia, Media 
2268 WHERE JobMedia.MediaId=Media.MediaId 
2269 AND JobId In ($inclause)
2270 ORDER BY MediaId";
2271     my $result = $dbh->selectall_arrayref($query);
2272     return $result;
2273 }
2274
2275 # Returns the fileindex from dirname and jobid.
2276 # Input : dbh, dirname, jobid
2277 # Output : fileindex
2278 sub get_fileindex_from_dir_jobid
2279 {
2280     my ($dbh, $dirname, $jobid)=@_;
2281     my $query;
2282     $query = "SELECT File.FileIndex
2283                 FROM File, Filename, Path
2284                 WHERE File.FilenameId = Filename.FilenameId
2285                 AND File.PathId = Path.PathId
2286                 AND Filename.Name = ''
2287                 AND Path.Path = '$dirname'
2288                 AND File.JobId = '$jobid'
2289                 ";
2290                 
2291     print STDERR $query,"\n" if $debug;
2292     my $result = $dbh->selectall_arrayref($query);
2293     return $result->[0]->[0];
2294 }
2295
2296 # Returns the fileindex from filename and jobid.
2297 # Input : dbh, filename, jobid
2298 # Output : fileindex
2299 sub get_fileindex_from_file_jobid
2300 {
2301     my ($dbh, $filename, $jobid)=@_;
2302     
2303     my @dirs = split(/\//, $filename);
2304     $filename=pop(@dirs);
2305     my $dirname = join('/', @dirs) . '/';
2306     
2307     
2308     my $query;
2309     $query = 
2310 "SELECT File.FileIndex
2311  FROM File, Filename, Path
2312  WHERE File.FilenameId = Filename.FilenameId
2313    AND File.PathId = Path.PathId
2314    AND Filename.Name = '$filename'
2315    AND Path.Path = '$dirname'
2316    AND File.JobId = '$jobid'";
2317                 
2318     print STDERR $query,"\n" if $debug;
2319     my $result = $dbh->selectall_arrayref($query);
2320     return $result->[0]->[0];
2321 }
2322
2323
2324 # Returns list of versions of a file that could be restored
2325 # returns an array of 
2326 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2327 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2328 # and volname and md5
2329 # and of course, there will be only one jobid in the array of jobids...
2330 sub get_all_file_versions
2331 {
2332     my ($dbh,$path,$file,$client,$see_all)=@_;
2333     
2334     defined $see_all or $see_all=0;
2335     
2336     my @versions;
2337     my $query;
2338     $query = 
2339 "SELECT File.JobId, File.FileIndex, File.Lstat, 
2340         File.Md5, Media.VolumeName, Media.InChanger
2341  FROM File, Filename, Path, Job, Client, JobMedia, Media
2342  WHERE File.FilenameId = Filename.FilenameId
2343    AND File.PathId=Path.PathId
2344    AND File.JobId = Job.JobId
2345    AND Job.ClientId = Client.ClientId
2346    AND Job.JobId = JobMedia.JobId
2347    AND File.FileIndex >= JobMedia.FirstIndex
2348    AND File.FileIndex <= JobMedia.LastIndex
2349    AND JobMedia.MediaId = Media.MediaId
2350    AND Path.Path = '$path'
2351    AND Filename.Name = '$file'
2352    AND Client.Name = '$client'";
2353         
2354     print STDERR $query if $debug;
2355         
2356     my $result = $dbh->selectall_arrayref($query);
2357         
2358     foreach my $refrow (@$result)
2359     {
2360         my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2361         my @attribs = parse_lstat($lstat);
2362         my $mtime = array_attrib('st_mtime',\@attribs);
2363         my $size = array_attrib('st_size',\@attribs);
2364                 
2365         my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2366                     $inchanger, $md5, $volname);
2367         push @versions, (\@list);
2368     }
2369         
2370     # We have the list of all versions of this file.
2371     # We'll sort it by mtime desc, size, md5, inchanger desc
2372     # the rest of the algorithm will be simpler
2373     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2374     @versions = sort { $b->[4] <=> $a->[4] 
2375                     || $a->[5] <=> $b->[5] 
2376                     || $a->[7] cmp $a->[7] 
2377                     || $b->[6] <=> $a->[6]} @versions;
2378         
2379     my @good_versions;
2380     my %allready_seen_by_mtime;
2381     my %allready_seen_by_md5;
2382     # Now we should create a new array with only the interesting records
2383     foreach my $ref (@versions)
2384     {   
2385         if ($ref->[7])
2386         {
2387             # The file has a md5. We compare his md5 to other known md5...
2388             # We take size into account. It may happen that 2 files
2389             # have the same md5sum and are different. size is a supplementary
2390             # criterion
2391             
2392             # If we allready have a (better) version
2393             next if ( (not $see_all) 
2394                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}); 
2395
2396             # we never met this one before...
2397             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2398         }
2399         # Even if it has a md5, we should also work with mtimes
2400         # We allready have a (better) version
2401         next if ( (not $see_all)
2402                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]}); 
2403         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2404         
2405         # We reached there. The file hasn't been seen.
2406         push @good_versions,($ref);
2407     }
2408         
2409     # To be nice with the user, we re-sort good_versions by
2410     # inchanger desc, mtime desc
2411     @good_versions = sort { $b->[4] <=> $a->[4] 
2412                          || $b->[2] <=> $a->[2]} @good_versions;
2413         
2414     return @good_versions;
2415 }
2416
2417 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2418 # This sub creates a BSR from the information in the restore_list
2419 # Returns the BSR as a string
2420 sub create_filelist
2421 {
2422         my $self = shift;
2423         my %mediainfos;
2424         # This query gets all jobid/jobmedia/media combination.
2425         my $query = "
2426 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile, 
2427        JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2428        JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex, 
2429        Media.Volumename, Media.MediaType
2430 FROM Job, JobMedia, Media
2431 WHERE Job.JobId = JobMedia.JobId
2432   AND JobMedia.MediaId = Media.MediaId
2433   ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2434         
2435
2436         my $result = $self->dbh_selectall_arrayref($query);
2437
2438         # We will store everything hashed by jobid.
2439
2440         foreach my $refrow (@$result)
2441         {
2442                 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2443                 $firstindex, $lastindex, $startblock, $endblock,
2444                 $volindex, $volumename, $mediatype) = @{$refrow};
2445
2446                 # We just have to deal with the case where starfile != endfile
2447                 # In this case, we concatenate both, for the bsr
2448                 if ($startfile != $endfile) { 
2449                       $startfile = $startfile . '-' . $endfile;
2450                 }
2451
2452                 my @tmparray = 
2453                 ($jobid, $volsessionid, $volsessiontime, $startfile, 
2454                 $firstindex, $lastindex, $startblock .'-'. $endblock,
2455                 $volindex, $volumename, $mediatype);
2456                 
2457                 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2458         }
2459
2460         
2461         # reminder : restore_list looks like this : 
2462         # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2463         
2464         # Here, we retrieve every file/dir that could be in the restore
2465         # We do as simple as possible for the SQL engine (no crazy joins,
2466         # no pseudo join (>= FirstIndex ...), etc ...
2467         # We do a SQL union of all the files/dirs specified in the restore_list
2468         my @select_queries;
2469         foreach my $entry (@{$self->{restore_list}->{data}})
2470         {
2471                 if ($entry->[2] eq 'dir')
2472                 {
2473                         my $dir = unpack('u', $entry->[0]);
2474                         my $inclause = $entry->[3]; #curjobids
2475
2476                         my $query = 
2477 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2478   FROM File, Path, Filename
2479   WHERE Path.PathId = File.PathId
2480   AND File.FilenameId = Filename.FilenameId
2481   AND Path.Path LIKE '$dir%'
2482   AND File.JobId IN ($inclause) )";
2483                         push @select_queries,($query);
2484                 }
2485                 else
2486                 {
2487                         # It's a file. Great, we allready have most 
2488                         # of what is needed. Simple and efficient query
2489                         my $file = unpack('u', $entry->[0]);
2490                         my @file = split '/',$file;
2491                         $file = pop @file;
2492                         my $dir = join('/',@file);
2493                         
2494                         my $jobid = $entry->[1];
2495                         my $fileindex = $entry->[7];
2496                         my $inclause = $entry->[3]; # curjobids
2497                         my $query = 
2498 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2499   FROM File, Path, Filename
2500   WHERE Path.PathId = File.PathId
2501   AND File.FilenameId = Filename.FilenameId
2502   AND Path.Path = '$dir/'
2503   AND Filename.Name = '$file'
2504   AND File.JobId = $jobid)";
2505                         push @select_queries,($query);
2506                 }
2507         }
2508         $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2509
2510         print STDERR $query,"\n" if $debug;
2511         
2512         #Now we run the query and parse the result...
2513         # there may be a lot of records, so we better be efficient
2514         # We use the bind column method, working with references...
2515
2516         my $sth = $self->dbh_prepare($query);
2517         $sth->execute;
2518
2519         my ($path,$name,$fileindex,$jobid);
2520         $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2521         
2522         # The temp place we're going to save all file
2523         # list to before the real list
2524         my @temp_list;
2525
2526         RECORD_LOOP:
2527         while ($sth->fetchrow_arrayref())
2528         {
2529                 # This may look dumb, but we're going to do a join by ourselves,
2530                 # to save memory and avoid sending a complex query to mysql
2531                 my $complete_path = $path . $name;
2532                 my $is_dir = 0;
2533                 
2534                 if ( $name eq '')
2535                 {
2536                         $is_dir = 1;
2537                 }
2538                 
2539                 # Remove trailing slash (normalize file and dir name)
2540                 $complete_path =~ s/\/$//;
2541                 
2542                 # Let's find the ref(s) for the %mediainfo element(s) 
2543                 # containing the data for this file
2544                 # There can be several matches. It is the pseudo join.
2545                 my $med_idx=0;
2546                 my $max_elt=@{$mediainfos{$jobid}}-1;
2547                 MEDIA_LOOP:
2548                 while($med_idx <= $max_elt)
2549                 {
2550                         my $ref = $mediainfos{$jobid}->[$med_idx];
2551                         # First, can we get rid of the first elements of the
2552                         # array ? (if they don't contain valuable records
2553                         # anymore
2554                         if ($fileindex > $ref->[5])
2555                         {
2556                                 # It seems we don't need anymore
2557                                 # this entry in %mediainfo (the input data
2558                                 # is sorted...)
2559                                 # We get rid of it.
2560                                 shift @{$mediainfos{$jobid}};
2561                                 $max_elt--;
2562                                 next MEDIA_LOOP;
2563                         }
2564                         # We will do work on this elt. We can ++
2565                         # $med_idx for next loop
2566                         $med_idx++;
2567
2568                         # %mediainfo row looks like : 
2569                         # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2570                         # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2571                         # MediaType)
2572                         
2573                         # We are in range. We store and continue looping
2574                         # in the medias
2575                         if ($fileindex >= $ref->[4])
2576                         {
2577                                 my @data = ($complete_path,$is_dir,
2578                                             $fileindex,$ref);
2579                                 push @temp_list,(\@data);
2580                                 next MEDIA_LOOP;
2581                         }
2582                         
2583                         # We are not in range. No point in continuing looping
2584                         # We go to next record.
2585                         next RECORD_LOOP;
2586                 }
2587         }
2588         # Now we have the array.
2589         # We're going to sort it, by 
2590         # path, volsessiontime DESC (get the most recent file...)
2591         # The array rows look like this :
2592         # complete_path,is_dir,fileindex,
2593         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2594         #       LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2595         @temp_list = sort {$a->[0] cmp $b->[0]
2596                         || $b->[3]->[2] <=> $a->[3]->[2]
2597                           } @temp_list;
2598
2599         my @restore_list;
2600         my $prev_complete_path='////'; # Sure not to match
2601         my $prev_is_file=1;
2602         my $prev_jobid;
2603
2604         while (my $refrow = shift @temp_list)
2605         {
2606                 # For the sake of readability, we load $refrow 
2607                 # contents in real scalars
2608                 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2609                 my $jobid= $refother->[0]; # We don't need the rest...
2610
2611                 # We skip this entry.
2612                 # We allready have a newer one and this 
2613                 # isn't a continuation of the same file
2614                 next if ($complete_path eq $prev_complete_path 
2615                          and $jobid != $prev_jobid);
2616                 
2617                 
2618                 if ($prev_is_file 
2619                     and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2620                 {
2621                         # We would be recursing inside a file.
2622                         # Just what we don't want (dir replaced by file
2623                         # between two backups
2624                         next;
2625                 }
2626                 elsif ($is_dir)
2627                 {
2628                         # It is a directory
2629                         push @restore_list,($refrow);
2630                         
2631                         $prev_complete_path = $complete_path;
2632                         $prev_jobid = $jobid;
2633                         $prev_is_file = 0;
2634                 }
2635                 else
2636                 {
2637                         # It is a file
2638                         push @restore_list,($refrow);
2639                         
2640                         $prev_complete_path = $complete_path;
2641                         $prev_jobid = $jobid;
2642                         $prev_is_file = 1;
2643                 }
2644         }
2645         # We get rid of @temp_list... save memory
2646         @temp_list=();
2647
2648         # Ok everything is in the list. Let's sort it again in another way.
2649         # This time it will be in the bsr file order
2650
2651         # we sort the results by 
2652         # volsessiontime, volsessionid, volindex, fileindex 
2653         # to get all files in right order...
2654         # Reminder : The array rows look like this :
2655         # complete_path,is_dir,fileindex,
2656         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2657         #       StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2658
2659         @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2] 
2660                            || $a->[3]->[1] <=> $b->[3]->[1] 
2661                            || $a->[3]->[7] <=> $b->[3]->[7] 
2662                            || $a->[2] <=> $b->[2] } 
2663                                 @restore_list;
2664
2665         # Now that everything is ready, we create the bsr
2666         my $prev_fileindex=-1;
2667         my $prev_volsessionid=-1;
2668         my $prev_volsessiontime=-1;
2669         my $prev_volumename=-1;
2670         my $prev_volfile=-1;
2671         my $prev_mediatype;
2672         my $prev_volblocks;
2673         my $count=0;
2674         my $first_of_current_range=0;
2675         my @fileindex_ranges;
2676         my $bsr='';
2677
2678         foreach my $refrow (@restore_list)
2679         {
2680                 my (undef,undef,$fileindex,$refother)=@{$refrow};
2681                 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2682                     $volblocks,undef,$volumename,$mediatype)=@{$refother};
2683                 
2684                 # We can specifiy the number of files in each section of the
2685                 # bsr to speedup restore (bacula can then jump over the
2686                 # end of tape files.
2687                 $count++;
2688                 
2689                 
2690                 if ($prev_volumename eq '-1')
2691                 {
2692                         # We only have to start the new range...
2693                         $first_of_current_range=$fileindex;
2694                 }
2695                 elsif ($prev_volsessionid != $volsessionid 
2696                        or $prev_volsessiontime != $volsessiontime 
2697                        or $prev_volumename ne $volumename 
2698                        or $prev_volfile ne $volfile)
2699                 {
2700                         # We have to create a new section in the bsr...
2701                         # We print the previous one ... 
2702                         # (before that, save the current range ...)
2703                         if ($first_of_current_range != $prev_fileindex)
2704                         {
2705                                 # we are in a range
2706                                 push @fileindex_ranges,
2707                                     ("$first_of_current_range-$prev_fileindex");
2708                         }
2709                         else
2710                         {
2711                                  # We are out of a range,
2712                                  # but there is only one element in the range
2713                                 push @fileindex_ranges,
2714                                     ("$first_of_current_range");
2715                         }
2716                         
2717                         $bsr.=print_bsr_section(\@fileindex_ranges,
2718                                                 $prev_volsessionid,
2719                                                 $prev_volsessiontime,
2720                                                 $prev_volumename,
2721                                                 $prev_volfile,
2722                                                 $prev_mediatype,
2723                                                 $prev_volblocks,
2724                                                 $count-1);
2725                         $count=1;
2726                         # Reset for next loop
2727                         @fileindex_ranges=();
2728                         $first_of_current_range=$fileindex;
2729                 }
2730                 elsif ($fileindex-1 != $prev_fileindex)
2731                 {
2732                         # End of a range of fileindexes
2733                         if ($first_of_current_range != $prev_fileindex)
2734                         {
2735                                 #we are in a range
2736                                 push @fileindex_ranges,
2737                                     ("$first_of_current_range-$prev_fileindex");
2738                         }
2739                         else
2740                         {
2741                                  # We are out of a range,
2742                                  # but there is only one element in the range
2743                                 push @fileindex_ranges,
2744                                     ("$first_of_current_range");
2745                         }
2746                         $first_of_current_range=$fileindex;
2747                 }
2748                 $prev_fileindex=$fileindex;
2749                 $prev_volsessionid = $volsessionid;
2750                 $prev_volsessiontime = $volsessiontime;
2751                 $prev_volumename = $volumename;
2752                 $prev_volfile=$volfile;
2753                 $prev_mediatype=$mediatype;
2754                 $prev_volblocks=$volblocks;
2755
2756         }
2757
2758         # Ok, we're out of the loop. Alas, there's still the last record ...
2759         if ($first_of_current_range != $prev_fileindex)
2760         {
2761                 # we are in a range
2762                 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2763                 
2764         }
2765         else
2766         {
2767                 # We are out of a range,
2768                 # but there is only one element in the range
2769                 push @fileindex_ranges,("$first_of_current_range");
2770                 
2771         }
2772         $bsr.=print_bsr_section(\@fileindex_ranges,
2773                                 $prev_volsessionid,
2774                                 $prev_volsessiontime,
2775                                 $prev_volumename,
2776                                 $prev_volfile,
2777                                 $prev_mediatype,
2778                                 $prev_volblocks,
2779                                 $count);
2780         
2781         return $bsr;
2782 }
2783
2784 sub print_bsr_section
2785 {
2786     my ($ref_fileindex_ranges,$volsessionid,
2787         $volsessiontime,$volumename,$volfile,
2788         $mediatype,$volblocks,$count)=@_;
2789     
2790     my $bsr='';
2791     $bsr .= "Volume=\"$volumename\"\n";
2792     $bsr .= "MediaType=\"$mediatype\"\n";
2793     $bsr .= "VolSessionId=$volsessionid\n";
2794     $bsr .= "VolSessionTime=$volsessiontime\n";
2795     $bsr .= "VolFile=$volfile\n";
2796     $bsr .= "VolBlock=$volblocks\n";
2797     
2798     foreach my $range (@{$ref_fileindex_ranges})
2799     {
2800         $bsr .= "FileIndex=$range\n";
2801     }
2802     
2803     $bsr .= "Count=$count\n";
2804     return $bsr;
2805 }
2806
2807 # This function estimates the size to be restored for an entry of the restore
2808 # list
2809 # In : self,reference to the entry
2810 # Out : size in bytes, number of files
2811 sub estimate_restore_size
2812 {
2813     # reminder : restore_list looks like this : 
2814     # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2815     my $self=shift;
2816     my ($entry)=@_;
2817     my $query;
2818     if ($entry->[2] eq 'dir')
2819     {
2820         my $dir = unpack('u', $entry->[0]);
2821         my $inclause = $entry->[3]; #curjobids
2822         $query = 
2823 "SELECT Path.Path, File.FilenameId, File.LStat
2824   FROM File, Path, Job
2825   WHERE Path.PathId = File.PathId
2826   AND File.JobId = Job.JobId
2827   AND Path.Path LIKE '$dir%'
2828   AND File.JobId IN ($inclause)
2829   ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2830     }
2831     else
2832     {
2833         # It's a file. Great, we allready have most 
2834         # of what is needed. Simple and efficient query
2835         my $file = unpack('u', $entry->[0]);
2836         my @file = split '/',$file;
2837         $file = pop @file;
2838         my $dir = join('/',@file);
2839         
2840         my $jobid = $entry->[1];
2841         my $fileindex = $entry->[7];
2842         my $inclause = $entry->[3]; # curjobids
2843         $query = 
2844 "SELECT Path.Path, File.FilenameId, File.Lstat
2845   FROM File, Path, Filename
2846   WHERE Path.PathId = File.PathId
2847   AND Path.Path = '$dir/'
2848   AND Filename.Name = '$file'
2849   AND File.JobId = $jobid
2850   AND Filename.FilenameId = File.FilenameId";
2851     }
2852
2853     print STDERR $query,"\n" if $debug;
2854     my ($path,$nameid,$lstat);
2855     my $sth = $self->dbh_prepare($query);
2856     $sth->execute;
2857     $sth->bind_columns(\$path,\$nameid,\$lstat);
2858     my $old_path='';
2859     my $old_nameid='';
2860     my $total_size=0;
2861     my $total_files=0;
2862
2863     refresh_screen();
2864
2865     my $rcount=0;
2866     # We fetch all rows
2867     while ($sth->fetchrow_arrayref())
2868     {
2869         # Only the latest version of a file
2870         next if ($nameid eq $old_nameid and $path eq $old_path);
2871
2872         if ($rcount > 15000) {
2873             refresh_screen();
2874             $rcount=0;
2875         } else {
2876             $rcount++;
2877         }
2878
2879         # We get the size of this file
2880         my $size=lstat_attrib($lstat,'st_size');
2881         $total_size += $size;
2882         $total_files++;
2883         $old_path=$path;
2884         $old_nameid=$nameid;
2885     }
2886     return ($total_size,$total_files);
2887 }
2888
2889 sub update_brestore_table
2890 {
2891     my ($self, @jobs) = @_;
2892     my $dbh = $self->{dbh};
2893
2894     foreach my $job (sort {$a <=> $b} @jobs)
2895     {
2896         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
2897         my $retour = $self->dbh_selectrow_arrayref($query);
2898         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
2899
2900         print STDERR "Inserting path records for JobId $job\n";
2901         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId) 
2902                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
2903
2904         $self->dbh_do($query);
2905
2906         # Now we have to do the directory recursion stuff to determine missing visibility
2907         # We try to avoid recursion, to be as fast as possible
2908         # We also only work on not allready hierarchised directories...
2909
2910         print STDERR "Creating missing recursion paths for $job\n";
2911
2912         $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility 
2913                   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
2914                   LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
2915                   WHERE brestore_pathvisibility.JobId = $job
2916                   AND brestore_pathhierarchy.PathId IS NULL
2917                   ORDER BY Path";
2918
2919         my $sth = $self->dbh_prepare($query);
2920         $sth->execute();
2921         my $pathid; my $path;
2922         $sth->bind_columns(\$pathid,\$path);
2923         
2924         while ($sth->fetch)
2925         {
2926             $self->build_path_hierarchy($path,$pathid);
2927         }
2928         $sth->finish();
2929
2930         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
2931         # This query gives all parent pathids for a given jobid that aren't stored.
2932         # It has to be called until no record is updated ...
2933         $query = "
2934         INSERT INTO brestore_pathvisibility (PathId, JobId) (
2935         SELECT a.PathId,$job
2936         FROM
2937                 (SELECT DISTINCT h.PPathId AS PathId
2938                 FROM brestore_pathhierarchy AS h
2939                 JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
2940                 WHERE p.JobId=$job) AS a
2941                 LEFT JOIN
2942                 (SELECT PathId
2943                 FROM brestore_pathvisibility
2944                 WHERE JobId=$job) AS b
2945                 ON (a.PathId = b.PathId)
2946         WHERE b.PathId IS NULL)";
2947         print STDERR $query,"\n" if ($debug);
2948         my $rows_affected;
2949         while (($rows_affected = $dbh->do($query)) and ($rows_affected !~ /^0/))
2950         {
2951             print STDERR "Recursively adding $rows_affected records from $job\n";
2952         }
2953         # Job's done
2954         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
2955         $dbh->do($query);
2956     }
2957 }
2958
2959 sub cleanup_brestore_table
2960 {
2961     my ($self) = @_;
2962     my $dbh = $self->{dbh};
2963
2964     my $query = "SELECT JobId from brestore_knownjobid";
2965     my @jobs = @{$dbh->selectall_arrayref($query)};
2966
2967     foreach my $jobentry (@jobs)
2968     {
2969         my $job = $jobentry->[0];
2970         $query = "SELECT FileId from File WHERE JobId = $job LIMIT 1";
2971         my $result = $dbh->selectall_arrayref($query);
2972         if (scalar(@{$result}))
2973         {
2974             # There are still files for this jobid
2975             print STDERR "$job still exists. Not cleaning...\n";
2976
2977         } else {
2978                 $query = "DELETE FROM brestore_pathvisibility WHERE JobId = $job";
2979                 $dbh->do($query);
2980                 $query = "DELETE FROM brestore_knownjobid WHERE JobId = $job";
2981                 $dbh->do($query);
2982         }
2983     }
2984 }
2985
2986 sub build_path_hierarchy
2987 {
2988     my ($self, $path,$pathid)=@_;
2989     # Does the ppathid exist for this ? we use a memory cache...
2990     # In order to avoid the full loop, we consider that if a dir is allready in the
2991     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
2992     while ($path ne '')
2993     {
2994         #print STDERR "$path\n" if $debug;
2995         if (! $self->{cache_ppathid}->{$pathid})
2996         {
2997             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
2998             my $sth2 = $self->{dbh}->prepare_cached($query);
2999             $sth2->execute($pathid);
3000             # Do we have a result ?
3001             if (my $refrow = $sth2->fetchrow_arrayref)
3002             {
3003                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
3004                 $sth2->finish();
3005                 # This dir was in the db ...
3006                 # It means we can leave, the tree has allready been built for
3007                 # this dir
3008                 return 1;
3009             } else {
3010                 $sth2->finish();
3011                 # We have to create the record ...
3012                 # What's the current p_path ?
3013                 my $ppath = parent_dir($path);
3014                 my $ppathid = $self->return_pathid_from_path($ppath);
3015                 $self->{cache_ppathid}->{$pathid}= $ppathid;
3016                 
3017                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
3018                 $sth2 = $self->{dbh}->prepare_cached($query);
3019                 $sth2->execute($pathid,$ppathid);
3020                 $sth2->finish();
3021                 $path = $ppath;
3022                 $pathid = $ppathid;
3023             }
3024         } else {
3025            # It's allready in the cache.
3026            # We can leave, no time to waste here, all the parent dirs have allready
3027            # been done
3028            return 1;
3029         }
3030     }
3031     return 1;
3032 }
3033
3034 sub return_pathid_from_path
3035 {
3036     my ($self, $path) = @_;
3037     my $query = "SELECT PathId FROM Path WHERE Path = ?
3038                  UNION
3039                  SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3040     #print STDERR $query,"\n" if $debug;
3041     my $sth = $self->{dbh}->prepare_cached($query);
3042     $sth->execute($path,$path);
3043     my $result =$sth->fetchrow_arrayref();
3044     $sth->finish();
3045     if (defined $result)
3046     {
3047         return $result->[0];
3048
3049     } else {
3050         # A bit dirty : we insert into path AND missing_path, to be sure
3051         # we aren't deleted by a purge. We still need to insert into path to get
3052         # the pathid, because of mysql
3053         $query = "INSERT INTO Path (Path) VALUES (?)";
3054         #print STDERR $query,"\n" if $debug;
3055         $sth = $self->{dbh}->prepare_cached($query);
3056         $sth->execute($path);
3057         $sth->finish();
3058         
3059         $query = " INSERT INTO brestore_missing_path (PathId,Path)
3060                    SELECT PathId,Path FROM Path WHERE Path = ?";
3061         #print STDERR $query,"\n" if $debug;
3062         $sth = $self->{dbh}->prepare_cached($query);
3063         $sth->execute($path);
3064         $sth->finish();
3065         $query = " DELETE FROM Path WHERE Path = ?";
3066         #print STDERR $query,"\n" if $debug;
3067         $sth = $self->{dbh}->prepare_cached($query);
3068         $sth->execute($path);
3069         $sth->finish();
3070         $query = "SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3071         #print STDERR $query,"\n" if $debug;
3072         $sth = $self->{dbh}->prepare_cached($query);
3073         $sth->execute($path);
3074         $result = $sth->fetchrow_arrayref();
3075         $sth->finish();
3076         return $result->[0];
3077     }
3078 }
3079
3080 sub parent_dir
3081 {
3082     my ($path) = @_;
3083     # Root Unix case :
3084     if ($path eq '/')
3085     {
3086         return '';
3087     }
3088     # Root Windows case :
3089     if ($path =~ /^[a-z]+:\/$/i)
3090     {
3091         return '';
3092     }
3093     # Split
3094     my @tmp = split('/',$path);
3095     # We remove the last ...
3096     pop @tmp;
3097     my $tmp = join ('/',@tmp) . '/';
3098     return $tmp;
3099 }
3100
3101 # Get metadata
3102 {
3103     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
3104                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
3105                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
3106                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
3107                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
3108                           'data_stream' => 15);;
3109     sub array_attrib
3110     {
3111         my ($attrib,$ref_attrib)=@_;
3112         return $ref_attrib->[$attrib_name_id{$attrib}];
3113     }
3114         
3115     sub file_attrib
3116     {   # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
3117
3118         my ($file, $attrib)=@_;
3119         
3120         if (defined $attrib_name_id{$attrib}) {
3121
3122             my @d = split(' ', $file->[2]) ; # TODO : cache this
3123             
3124             return from_base64($d[$attrib_name_id{$attrib}]);
3125
3126         } elsif ($attrib eq 'jobid') {
3127
3128             return $file->[3];
3129
3130         } elsif ($attrib eq 'name') {
3131
3132             return $file->[1];
3133             
3134         } else  {
3135             die "Attribute not known : $attrib.\n";
3136         }
3137     }
3138
3139     # Return the jobid or attribute asked for a dir
3140     sub dir_attrib
3141     {
3142         my ($self,$dir,$attrib)=@_;
3143         
3144         my @dir = split('/',$dir,-1);
3145         my $refdir=$self->{dirtree}->{$self->current_client};
3146         
3147         if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
3148         {
3149             die "Attribute not known : $attrib.\n";
3150         }
3151         # Find the leaf
3152         foreach my $subdir (@dir)
3153         {
3154             $refdir = $refdir->[0]->{$subdir};
3155         }
3156         
3157         # $refdir is now the reference to the dir's array
3158         # Is the a jobid in @CurrentJobIds where the lstat is
3159         # defined (we'll search in reverse order)
3160         foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
3161         {
3162             if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
3163             {
3164                 if ($attrib eq 'jobid')
3165                 {
3166                     return $jobid;
3167                 }
3168                 else
3169                 {
3170                     my @attribs = parse_lstat($refdir->[1]->{$jobid});
3171                     return $attribs[$attrib_name_id{$attrib}+1];
3172                 }
3173             }
3174         }
3175
3176         return 0; # We cannot get a good attribute.
3177                   # This directory is here for the sake of visibility
3178     }
3179     
3180     sub lstat_attrib
3181     {
3182         my ($lstat,$attrib)=@_;
3183         if ($lstat and defined $attrib_name_id{$attrib}) 
3184         {
3185             my @d = split(' ', $lstat) ; # TODO : cache this
3186             return from_base64($d[$attrib_name_id{$attrib}]);
3187         }
3188         return 0;
3189     }
3190 }
3191
3192 {
3193     # Base 64 functions, directly from recover.pl.
3194     # Thanks to
3195     # Karl Hakimian <hakimian@aha.com>
3196     # This section is also under GPL v2 or later.
3197     my @base64_digits;
3198     my @base64_map;
3199     my $is_init=0;
3200     sub init_base64
3201     {
3202         @base64_digits = (
3203         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
3204         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
3205         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
3206         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
3207         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
3208                           );
3209         @base64_map = (0) x 128;
3210         
3211         for (my $i=0; $i<64; $i++) {
3212             $base64_map[ord($base64_digits[$i])] = $i;
3213         }
3214         $is_init = 1;
3215     }
3216
3217     sub from_base64 {
3218         if(not $is_init)
3219         {
3220             init_base64();
3221         }
3222         my $where = shift;
3223         my $val = 0;
3224         my $i = 0;
3225         my $neg = 0;
3226         
3227         if (substr($where, 0, 1) eq '-') {
3228             $neg = 1;
3229             $where = substr($where, 1);
3230         }
3231         
3232         while ($where ne '') {
3233             $val *= 64;
3234             my $d = substr($where, 0, 1);
3235             $val += $base64_map[ord(substr($where, 0, 1))];
3236             $where = substr($where, 1);
3237         }
3238         
3239         return $val;
3240     }
3241
3242     sub parse_lstat {
3243         my ($lstat)=@_;
3244         my @attribs = split(' ',$lstat);
3245         foreach my $element (@attribs)
3246         {
3247             $element = from_base64($element);
3248         }
3249         return @attribs;
3250     }
3251 }
3252
3253
3254 1;
3255
3256 ################################################################
3257
3258 package Batch;
3259 use base qw/DlgResto/;
3260
3261 sub new
3262 {
3263     my ($class, $conf) = @_;
3264     my $self = bless {info => $conf}, $class;
3265
3266     $self->{dbh} = $conf->{dbh};
3267
3268     return $self;
3269 }
3270
3271 sub update_cache
3272 {
3273     my ($self) = @_;
3274
3275     my $query = "SELECT JobId from Job WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) order by JobId";
3276     my $jobs = $self->dbh_selectall_arrayref($query);
3277
3278     $self->update_brestore_table(map { $_->[0] } @$jobs);
3279 }
3280
3281 1;
3282
3283 package main;
3284
3285 use Getopt::Long ;
3286
3287 sub HELP_MESSAGE
3288 {
3289     print STDERR "Usage: $0 [--conf=brestore.conf] [--batch] [--debug]\n";
3290     exit 1;
3291 }
3292
3293 my $file_conf = "$ENV{HOME}/.brestore.conf" ;
3294 my $batch_mod;
3295
3296 GetOptions("conf=s"   => \$file_conf,
3297            "batch"    => \$batch_mod,
3298            "debug"    => \$debug,
3299            "help"     => \&HELP_MESSAGE) ;
3300
3301 my $p = new Pref($file_conf);
3302
3303 if (! -f $file_conf) {
3304     $p->write_config();
3305 }
3306
3307 if ($batch_mod) {
3308     my $b = new Batch($p);
3309     if ($p->connect_db()) {
3310         $b->set_dbh($p->{dbh});
3311         $b->update_cache();
3312     }
3313     exit (0);
3314 }
3315
3316 $glade_file = $p->{glade_file};
3317
3318 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
3319     if (-f "$path/$glade_file") {
3320         $glade_file = "$path/$glade_file" ;
3321         last;
3322     }
3323 }
3324
3325 # gtk have lots of warning on stderr
3326 if ($^O eq 'MSWin32')
3327 {
3328     close(STDERR);
3329     open(STDERR, ">stderr.log");
3330 }
3331
3332 Gtk2->init();
3333
3334 if ( -f $glade_file) {
3335     my $w = new DlgResto($p);
3336
3337 } else {
3338     my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close', 
3339 "Can't find your brestore.glade (glade_file => '$glade_file')
3340 Please, edit your $file_conf to setup it." );
3341  
3342     $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3343     $widget->run;
3344     exit 1;
3345 }
3346
3347 Gtk2->main; # Start Gtk2 main loop      
3348
3349 # that's it!
3350
3351 exit 0;
3352
3353
3354 __END__
3355
3356 TODO : 
3357
3358
3359 # Code pour trier les colonnes    
3360     my $mod = $fileview->get_model();
3361     $mod->set_default_sort_func(sub {
3362             my ($model, $item1, $item2) = @_;
3363             my $a = $model->get($item1, 1);  # récupération de la valeur de la 2ème 
3364             my $b = $model->get($item2, 1);  # colonne (indice 1)
3365             return $a cmp $b;
3366         }
3367     );
3368     
3369     $fileview->set_headers_clickable(1);
3370     my $col = $fileview->get_column(1);    # la colonne NOM, colonne numéro 2
3371     $col->signal_connect('clicked', sub {
3372             my ($colonne, $model) = @_;
3373             $model->set_sort_column_id (1, 'ascending');
3374         },
3375         $mod
3376     );