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