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