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