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