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