]> git.sur5r.net Git - bacula/bacula/blob - gui/brestore/brestore.pl
fb6178ceef2ede9b0e73e8980e87a3015bca8f1a
[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     print STDERR "Cleaning path visibility\n";
2401     
2402     my $nb = $self->dbh_do("
2403   DELETE FROM brestore_pathvisibility
2404       WHERE NOT EXISTS 
2405    (SELECT 1 FROM Job WHERE JobId=brestore_pathvisibility.JobId)");
2406
2407     print STDERR "$nb rows affected\n";
2408     print STDERR "Cleaning known jobid\n";
2409
2410     $nb = $self->dbh_do("
2411   DELETE FROM brestore_knownjobid
2412       WHERE NOT EXISTS 
2413    (SELECT 1 FROM Job WHERE JobId=brestore_knownjobid.JobId)");
2414
2415     print STDERR "$nb rows affected\n";
2416
2417     $self->{conf}->{dbh}->commit();
2418 }
2419
2420 sub get_root
2421 {
2422     my ($self, $dir) = @_;
2423     return $self->get_pathid('');
2424 }
2425
2426 sub ch_dir
2427 {
2428     my ($self, $pathid) = @_;
2429     $self->{cwdid} = $pathid;
2430 }
2431
2432 sub up_dir
2433 {
2434     my ($self) = @_ ;
2435     my $query = "
2436   SELECT PPathId 
2437     FROM brestore_pathhierarchy 
2438    WHERE PathId IN ($self->{cwdid}) ";
2439
2440     my $all = $self->dbh_selectall_arrayref($query);
2441     return unless ($all);       # already at root
2442
2443     my $dir = join(',', map { $_->[0] } @$all);
2444     if ($dir) {
2445         $self->ch_dir($dir);
2446     }
2447 }
2448
2449 sub pwd
2450 {
2451     my ($self) = @_;
2452     return $self->get_path($self->{cwdid});
2453 }
2454
2455 sub get_path
2456 {
2457     my ($self, $pathid) = @_;
2458     $self->debug("Call with pathid = $pathid");
2459     my $query = 
2460         "SELECT Path FROM Path WHERE PathId IN (?)";
2461
2462     my $sth = $self->dbh_prepare($query);
2463     $sth->execute($pathid);
2464     my $result = $sth->fetchrow_arrayref();
2465     $sth->finish();
2466     return $result->[0];    
2467 }
2468
2469 sub set_curjobids
2470 {
2471     my ($self, @jobids) = @_;
2472     $self->{curjobids} = join(',', @jobids);
2473     $self->update_brestore_table(@jobids);
2474 }
2475
2476 sub ls_files
2477 {
2478     my ($self) = @_;
2479
2480     return undef unless ($self->{curjobids});
2481
2482     my $inclause   = $self->{curjobids};
2483     my $inlistpath = $self->{cwdid};
2484
2485     my $query = 
2486 "SELECT File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
2487  FROM
2488         (SELECT Filename.Name, max(File.FileId) as id
2489          FROM File, Filename
2490          WHERE File.FilenameId = Filename.FilenameId
2491            AND Filename.Name != ''
2492            AND File.PathId IN ($inlistpath)
2493            AND File.JobId IN ($inclause)
2494          GROUP BY Filename.Name
2495          ORDER BY Filename.Name) AS listfiles,
2496 File
2497 WHERE File.FileId = listfiles.id";
2498         
2499     $self->debug($query);
2500     my $result = $self->dbh_selectall_arrayref($query);
2501     $self->debug($result);
2502         
2503     return $result;
2504 }
2505
2506 # return ($dirid,$dir_basename,$lstat,$jobid)
2507 sub ls_dirs
2508 {
2509     my ($self) = @_;
2510
2511     return undef unless ($self->{curjobids});
2512
2513     my $pathid = $self->{cwdid};
2514     my $jobclause = $self->{curjobids};
2515
2516     # Let's retrieve the list of the visible dirs in this dir ...
2517     # First, I need the empty filenameid to locate efficiently the dirs in the file table
2518     my $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
2519     my $sth = $self->dbh_prepare($query);
2520     $sth->execute();
2521     my $result = $sth->fetchrow_arrayref();
2522     $sth->finish();
2523     my $dir_filenameid = $result->[0];
2524      
2525     # Then we get all the dir entries from File ...
2526     $query = "
2527 SELECT PathId, Path, JobId, Lstat FROM (
2528     
2529     SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
2530            listfile1.JobId, listfile1.Lstat
2531     FROM (
2532         SELECT DISTINCT brestore_pathhierarchy1.PathId
2533         FROM brestore_pathhierarchy AS brestore_pathhierarchy1
2534         JOIN Path AS Path2
2535             ON (brestore_pathhierarchy1.PathId = Path2.PathId)
2536         JOIN brestore_pathvisibility AS brestore_pathvisibility1
2537             ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
2538         WHERE brestore_pathhierarchy1.PPathId = $pathid
2539         AND brestore_pathvisibility1.jobid IN ($jobclause)) AS listpath1
2540     JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
2541     LEFT JOIN (
2542         SELECT File1.PathId, File1.JobId, File1.Lstat FROM File AS File1
2543         WHERE File1.FilenameId = $dir_filenameid
2544         AND File1.JobId IN ($jobclause)) AS listfile1
2545         ON (listpath1.PathId = listfile1.PathId)
2546      ) AS A ORDER BY 2,3 DESC
2547 ";
2548     $self->debug($query);
2549     $sth=$self->dbh_prepare($query);
2550     $sth->execute();
2551     $result = $sth->fetchall_arrayref();
2552     my @return_list;
2553     my $prev_dir='';
2554     foreach my $refrow (@{$result})
2555     {
2556         my $dirid = $refrow->[0];
2557         my $dir = $refrow->[1];
2558         my $lstat = $refrow->[3];
2559         my $jobid = $refrow->[2] || 0;
2560         next if ($dirid eq $prev_dir);
2561         # We have to clean up this dirname ... we only want it's 'basename'
2562         my $return_value;
2563         if ($dir ne '/')
2564         {
2565             my @temp = split ('/',$dir);
2566             $return_value = pop @temp;
2567         }
2568         else
2569         {
2570             $return_value = '/';
2571         }
2572         my @return_array = ($dirid,$return_value,$lstat,$jobid);
2573         push @return_list,(\@return_array);
2574         $prev_dir = $dirid;
2575     }
2576     $self->debug(\@return_list);
2577     return \@return_list;    
2578 }
2579
2580 # Returns the list of media required for a list of jobids.
2581 # Input : self, jobid1, jobid2...
2582 # Output : reference to array of (joibd, inchanger)
2583 sub get_required_media_from_jobid
2584 {
2585     my ($self, @jobids)=@_;
2586     my $inclause = join(',',@jobids);
2587     my $query = "
2588 SELECT DISTINCT JobMedia.MediaId, Media.InChanger 
2589 FROM JobMedia, Media 
2590 WHERE JobMedia.MediaId=Media.MediaId 
2591 AND JobId In ($inclause)
2592 ORDER BY MediaId";
2593     my $result = $self->dbh_selectall_arrayref($query);
2594     return $result;
2595 }
2596
2597 # Returns the fileindex from dirname and jobid.
2598 # Input : self, dirid, jobid
2599 # Output : fileindex
2600 sub get_fileindex_from_dir_jobid
2601 {
2602     my ($self, $dirid, $jobid)=@_;
2603     my $query;
2604     $query = "SELECT File.FileIndex
2605                 FROM File, Filename
2606                 WHERE File.FilenameId = Filename.FilenameId
2607                 AND File.PathId = $dirid
2608                 AND Filename.Name = ''
2609                 AND File.JobId = '$jobid'
2610                 ";
2611                 
2612     $self->debug($query);
2613     my $result = $self->dbh_selectall_arrayref($query);
2614     return $result->[0]->[0];
2615 }
2616
2617 # Returns the fileindex from filename and jobid.
2618 # Input : self, dirid, filenameid, jobid
2619 # Output : fileindex
2620 sub get_fileindex_from_file_jobid
2621 {
2622     my ($self, $dirid, $filenameid, $jobid)=@_;
2623     
2624     my $query;
2625     $query = 
2626 "SELECT File.FileIndex
2627  FROM File
2628  WHERE File.PathId = $dirid
2629    AND File.FilenameId = $filenameid
2630    AND File.JobId = $jobid";
2631                 
2632     $self->debug($query);
2633     my $result = $self->dbh_selectall_arrayref($query);
2634     return $result->[0]->[0];
2635 }
2636
2637 # This function estimates the size to be restored for an entry of the restore
2638 # list
2639 # In : self,reference to the entry
2640 # Out : size in bytes, number of files
2641 sub estimate_restore_size
2642 {
2643     # reminder : restore_list looks like this : 
2644     # ($pid,$fid,$name,$jobid,'file',$curjobids, 
2645     #  undef, undef, undef, $dirfileindex);
2646     my ($self, $entry, $refresh) = @_;
2647     my $query;
2648     if ($entry->[4] eq 'dir')
2649     {
2650         my $dir = $entry->[0];
2651
2652         my $inclause = $entry->[5]; #curjobids
2653         $query = 
2654 "SELECT Path.Path, File.FilenameId, File.LStat
2655   FROM File, Path, Job
2656   WHERE Path.PathId = File.PathId
2657   AND File.JobId = Job.JobId
2658   AND Path.Path LIKE 
2659         (SELECT " . $self->dbh_strcat('Path',"'\%'") . " FROM Path WHERE PathId IN ($dir)
2660         )
2661   AND File.JobId IN ($inclause)
2662   ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2663     }
2664     else
2665     {
2666         # It's a file. Great, we allready have most 
2667         # of what is needed. Simple and efficient query
2668         my $dir = $entry->[0];
2669         my $fileid = $entry->[1];
2670         
2671         my $jobid = $entry->[3];
2672         my $fileindex = $entry->[9];
2673         my $inclause = $entry->[5]; # curjobids
2674         $query = 
2675 "SELECT Path.Path, File.FilenameId, File.Lstat
2676   FROM File, Path
2677   WHERE Path.PathId = File.PathId
2678   AND Path.PathId = $dir
2679   AND File.FilenameId = $fileid
2680   AND File.JobId = $jobid";
2681     }
2682
2683     my ($path,$nameid,$lstat);
2684     my $sth = $self->dbh_prepare($query);
2685     $sth->execute;
2686     $sth->bind_columns(\$path,\$nameid,\$lstat);
2687     my $old_path='';
2688     my $old_nameid='';
2689     my $total_size=0;
2690     my $total_files=0;
2691
2692     &$refresh();
2693
2694     my $rcount=0;
2695     # We fetch all rows
2696     while ($sth->fetchrow_arrayref())
2697     {
2698         # Only the latest version of a file
2699         next if ($nameid eq $old_nameid and $path eq $old_path);
2700
2701         if ($rcount > 15000) {
2702             &$refresh();
2703             $rcount=0;
2704         } else {
2705             $rcount++;
2706         }
2707
2708         # We get the size of this file
2709         my $size=lstat_attrib($lstat,'st_size');
2710         $total_size += $size;
2711         $total_files++;
2712         $old_path=$path;
2713         $old_nameid=$nameid;
2714     }
2715
2716     return ($total_size,$total_files);
2717 }
2718
2719 # Returns list of versions of a file that could be restored
2720 # returns an array of 
2721 # ('FILE:',jobid,fileindex,mtime,size,inchanger,md5,volname)
2722 # there will be only one jobid in the array of jobids...
2723 sub get_all_file_versions
2724 {
2725     my ($self,$pathid,$fileid,$client,$see_all)=@_;
2726     
2727     defined $see_all or $see_all=0;
2728     
2729     my @versions;
2730     my $query;
2731     $query = 
2732 "SELECT File.JobId, File.FileIndex, File.Lstat, 
2733         File.Md5, Media.VolumeName, Media.InChanger
2734  FROM File, Job, Client, JobMedia, Media
2735  WHERE File.FilenameId = $fileid
2736    AND File.PathId=$pathid
2737    AND File.JobId = Job.JobId
2738    AND Job.ClientId = Client.ClientId
2739    AND Job.JobId = JobMedia.JobId
2740    AND File.FileIndex >= JobMedia.FirstIndex
2741    AND File.FileIndex <= JobMedia.LastIndex
2742    AND JobMedia.MediaId = Media.MediaId
2743    AND Client.Name = '$client'";
2744         
2745     $self->debug($query);
2746         
2747     my $result = $self->dbh_selectall_arrayref($query);
2748         
2749     foreach my $refrow (@$result)
2750     {
2751         my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2752         my @attribs = parse_lstat($lstat);
2753         my $mtime = array_attrib('st_mtime',\@attribs);
2754         my $size = array_attrib('st_size',\@attribs);
2755                 
2756         my @list = ('FILE:',$pathid,$fileid,$jobid,
2757                     $fileindex, $mtime, $size, $inchanger,
2758                     $md5, $volname);
2759         push @versions, (\@list);
2760     }
2761         
2762     # We have the list of all versions of this file.
2763     # We'll sort it by mtime desc, size, md5, inchanger desc
2764     # the rest of the algorithm will be simpler
2765     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2766     @versions = sort { $b->[5] <=> $a->[5] 
2767                     || $a->[6] <=> $b->[6] 
2768                     || $a->[8] cmp $a->[8] 
2769                     || $b->[7] <=> $a->[7]} @versions;
2770
2771         
2772     my @good_versions;
2773     my %allready_seen_by_mtime;
2774     my %allready_seen_by_md5;
2775     # Now we should create a new array with only the interesting records
2776     foreach my $ref (@versions)
2777     {   
2778         if ($ref->[8])
2779         {
2780             # The file has a md5. We compare his md5 to other known md5...
2781             # We take size into account. It may happen that 2 files
2782             # have the same md5sum and are different. size is a supplementary
2783             # criterion
2784             
2785             # If we allready have a (better) version
2786             next if ( (not $see_all) 
2787                       and $allready_seen_by_md5{$ref->[8] .'-'. $ref->[6]}); 
2788
2789             # we never met this one before...
2790             $allready_seen_by_md5{$ref->[8] .'-'. $ref->[6]}=1;
2791         }
2792         # Even if it has a md5, we should also work with mtimes
2793         # We allready have a (better) version
2794         next if ( (not $see_all)
2795                   and $allready_seen_by_mtime{$ref->[5] .'-'. $ref->[6]}); 
2796         $allready_seen_by_mtime{$ref->[5] .'-'. $ref->[6] . '-' . $ref->[8]}=1;
2797         
2798         # We reached there. The file hasn't been seen.
2799         push @good_versions,($ref);
2800     }
2801         
2802     # To be nice with the user, we re-sort good_versions by
2803     # inchanger desc, mtime desc
2804     @good_versions = sort { $b->[5] <=> $a->[5] 
2805                          || $b->[3] <=> $a->[3]} @good_versions;
2806         
2807     return @good_versions;
2808 }
2809
2810
2811 sub update_brestore_table
2812 {
2813     my ($self, @jobs) = @_;
2814
2815     $self->debug(\@jobs);
2816
2817     foreach my $job (sort {$a <=> $b} @jobs)
2818     {
2819         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
2820         my $retour = $self->dbh_selectrow_arrayref($query);
2821         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
2822
2823         print STDERR "Inserting path records for JobId $job\n";
2824         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId) 
2825                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
2826
2827         $self->dbh_do($query);
2828
2829         # Now we have to do the directory recursion stuff to determine missing visibility
2830         # We try to avoid recursion, to be as fast as possible
2831         # We also only work on not allready hierarchised directories...
2832
2833         print STDERR "Creating missing recursion paths for $job\n";
2834
2835         $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility 
2836                   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
2837                   LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
2838                   WHERE brestore_pathvisibility.JobId = $job
2839                   AND brestore_pathhierarchy.PathId IS NULL
2840                   ORDER BY Path";
2841
2842         my $sth = $self->dbh_prepare($query);
2843         $sth->execute();
2844         my $pathid; my $path;
2845         $sth->bind_columns(\$pathid,\$path);
2846         
2847         while ($sth->fetch)
2848         {
2849             $self->build_path_hierarchy($path,$pathid);
2850         }
2851         $sth->finish();
2852
2853         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
2854         # This query gives all parent pathids for a given jobid that aren't stored.
2855         # It has to be called until no record is updated ...
2856         $query = "
2857         INSERT INTO brestore_pathvisibility (PathId, JobId) (
2858         SELECT a.PathId,$job
2859         FROM
2860                 (SELECT DISTINCT h.PPathId AS PathId
2861                 FROM brestore_pathhierarchy AS h
2862                 JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
2863                 WHERE p.JobId=$job) AS a
2864                 LEFT JOIN
2865                 (SELECT PathId
2866                 FROM brestore_pathvisibility
2867                 WHERE JobId=$job) AS b
2868                 ON (a.PathId = b.PathId)
2869         WHERE b.PathId IS NULL)";
2870
2871         my $rows_affected;
2872         while (($rows_affected = $self->dbh_do($query)) and ($rows_affected !~ /^0/))
2873         {
2874             print STDERR "Recursively adding $rows_affected records from $job\n";
2875         }
2876         # Job's done
2877         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
2878         $self->dbh_do($query);
2879     }
2880 }
2881
2882 sub parent_dir
2883 {
2884     my ($path) = @_;
2885     # Root Unix case :
2886     if ($path eq '/')
2887     {
2888         return '';
2889     }
2890     # Root Windows case :
2891     if ($path =~ /^[a-z]+:\/$/i)
2892     {
2893         return '';
2894     }
2895     # Split
2896     my @tmp = split('/',$path);
2897     # We remove the last ...
2898     pop @tmp;
2899     my $tmp = join ('/',@tmp) . '/';
2900     return $tmp;
2901 }
2902
2903 sub build_path_hierarchy
2904 {
2905     my ($self, $path,$pathid)=@_;
2906     # Does the ppathid exist for this ? we use a memory cache...
2907     # In order to avoid the full loop, we consider that if a dir is allready in the
2908     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
2909     while ($path ne '')
2910     {
2911         if (! $self->{cache_ppathid}->{$pathid})
2912         {
2913             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
2914             my $sth2 = $self->{conf}->{dbh}->prepare_cached($query);
2915             $sth2->execute($pathid);
2916             # Do we have a result ?
2917             if (my $refrow = $sth2->fetchrow_arrayref)
2918             {
2919                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
2920                 $sth2->finish();
2921                 # This dir was in the db ...
2922                 # It means we can leave, the tree has allready been built for
2923                 # this dir
2924                 return 1;
2925             } else {
2926                 $sth2->finish();
2927                 # We have to create the record ...
2928                 # What's the current p_path ?
2929                 my $ppath = parent_dir($path);
2930                 my $ppathid = $self->return_pathid_from_path($ppath);
2931                 $self->{cache_ppathid}->{$pathid}= $ppathid;
2932                 
2933                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
2934                 $sth2 = $self->{conf}->{dbh}->prepare_cached($query);
2935                 $sth2->execute($pathid,$ppathid);
2936                 $sth2->finish();
2937                 $path = $ppath;
2938                 $pathid = $ppathid;
2939             }
2940         } else {
2941            # It's allready in the cache.
2942            # We can leave, no time to waste here, all the parent dirs have allready
2943            # been done
2944            return 1;
2945         }
2946     }
2947     return 1;
2948 }
2949
2950
2951 sub return_pathid_from_path
2952 {
2953     my ($self, $path) = @_;
2954     my $query = "SELECT PathId FROM Path WHERE Path = ?";
2955
2956     #print STDERR $query,"\n" if $debug;
2957     my $sth = $self->{conf}->{dbh}->prepare_cached($query);
2958     $sth->execute($path);
2959     my $result =$sth->fetchrow_arrayref();
2960     $sth->finish();
2961     if (defined $result)
2962     {
2963         return $result->[0];
2964
2965     } else {
2966         # A bit dirty : we insert into path, and we have to be sure
2967         # we aren't deleted by a purge. We still need to insert into path to get
2968         # the pathid, because of mysql
2969         $query = "INSERT INTO Path (Path) VALUES (?)";
2970         #print STDERR $query,"\n" if $debug;
2971         $sth = $self->{conf}->{dbh}->prepare_cached($query);
2972         $sth->execute($path);
2973         $sth->finish();
2974         
2975         $query = "SELECT PathId FROM Path WHERE Path = ?";
2976         #print STDERR $query,"\n" if $debug;
2977         $sth = $self->{conf}->{dbh}->prepare_cached($query);
2978         $sth->execute($path);
2979         $result = $sth->fetchrow_arrayref();
2980         $sth->finish();
2981         return $result->[0];
2982     }
2983 }
2984
2985
2986 sub create_brestore_tables
2987 {
2988     my ($self) = @_;
2989     my $ret = 0;
2990     my $verif = "SELECT 1 FROM brestore_knownjobid LIMIT 1";
2991
2992     unless ($self->dbh_do($verif)) {
2993         $ret=1;
2994
2995         my $req = "
2996     CREATE TABLE brestore_knownjobid
2997     (
2998      JobId int4 NOT NULL,
2999      CONSTRAINT brestore_knownjobid_pkey PRIMARY KEY (JobId)
3000     )";
3001         $self->dbh_do($req);
3002     }
3003     
3004     $verif = "SELECT 1 FROM brestore_pathhierarchy LIMIT 1";
3005     unless ($self->dbh_do($verif)) {
3006         $ret=1;
3007         my $req = "
3008    CREATE TABLE brestore_pathhierarchy
3009    (
3010      PathId int4 NOT NULL,
3011      PPathId int4 NOT NULL,
3012      CONSTRAINT brestore_pathhierarchy_pkey PRIMARY KEY (PathId)
3013    )";
3014         $self->dbh_do($req);
3015
3016
3017         $req = "CREATE INDEX brestore_pathhierarchy_ppathid 
3018                           ON brestore_pathhierarchy (PPathId)";
3019         $self->dbh_do($req);
3020     }
3021     
3022     $verif = "SELECT 1 FROM brestore_pathvisibility LIMIT 1";
3023     unless ($self->dbh_do($verif)) {
3024         $ret=1;
3025         my $req = "
3026     CREATE TABLE brestore_pathvisibility
3027     (
3028       PathId int4 NOT NULL,
3029       JobId int4 NOT NULL,
3030       Size int8 DEFAULT 0,
3031       Files int4 DEFAULT 0,
3032       CONSTRAINT brestore_pathvisibility_pkey PRIMARY KEY (JobId, PathId)
3033     )";
3034         $self->dbh_do($req);
3035
3036         $req = "CREATE INDEX brestore_pathvisibility_jobid
3037                           ON brestore_pathvisibility (JobId)";
3038         $self->dbh_do($req);
3039     }
3040     return $ret;
3041 }
3042
3043 # Get metadata
3044 {
3045     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
3046                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
3047                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
3048                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
3049                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
3050                           'data_stream' => 15);;
3051     sub array_attrib
3052     {
3053         my ($attrib,$ref_attrib)=@_;
3054         return $ref_attrib->[$attrib_name_id{$attrib}];
3055     }
3056         
3057     sub file_attrib
3058     {   # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
3059
3060         my ($file, $attrib)=@_;
3061         
3062         if (defined $attrib_name_id{$attrib}) {
3063
3064             my @d = split(' ', $file->[3]) ; # TODO : cache this
3065             
3066             return from_base64($d[$attrib_name_id{$attrib}]);
3067
3068         } elsif ($attrib eq 'jobid') {
3069
3070             return $file->[4];
3071
3072         } elsif ($attrib eq 'name') {
3073
3074             return $file->[2];
3075             
3076         } else  {
3077             die "Attribute not known : $attrib.\n";
3078         }
3079     }
3080     
3081     sub lstat_attrib
3082     {
3083         my ($lstat,$attrib)=@_;
3084         if ($lstat and defined $attrib_name_id{$attrib}) 
3085         {
3086             my @d = split(' ', $lstat) ; # TODO : cache this
3087             return from_base64($d[$attrib_name_id{$attrib}]);
3088         }
3089         return 0;
3090     }
3091 }
3092
3093 {
3094     # Base 64 functions, directly from recover.pl.
3095     # Thanks to
3096     # Karl Hakimian <hakimian@aha.com>
3097     # This section is also under GPL v2 or later.
3098     my @base64_digits;
3099     my @base64_map;
3100     my $is_init=0;
3101     sub init_base64
3102     {
3103         @base64_digits = (
3104         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
3105         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
3106         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
3107         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
3108         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
3109                           );
3110         @base64_map = (0) x 128;
3111         
3112         for (my $i=0; $i<64; $i++) {
3113             $base64_map[ord($base64_digits[$i])] = $i;
3114         }
3115         $is_init = 1;
3116     }
3117
3118     sub from_base64 {
3119         if(not $is_init)
3120         {
3121             init_base64();
3122         }
3123         my $where = shift;
3124         my $val = 0;
3125         my $i = 0;
3126         my $neg = 0;
3127         
3128         if (substr($where, 0, 1) eq '-') {
3129             $neg = 1;
3130             $where = substr($where, 1);
3131         }
3132         
3133         while ($where ne '') {
3134             $val *= 64;
3135             my $d = substr($where, 0, 1);
3136             $val += $base64_map[ord(substr($where, 0, 1))];
3137             $where = substr($where, 1);
3138         }
3139         
3140         return $val;
3141     }
3142
3143     sub parse_lstat {
3144         my ($lstat)=@_;
3145         my @attribs = split(' ',$lstat);
3146         foreach my $element (@attribs)
3147         {
3148             $element = from_base64($element);
3149         }
3150         return @attribs;
3151     }
3152 }
3153
3154 1;
3155
3156 ################################################################
3157 package BwebConsole;
3158 use LWP::UserAgent;
3159 use HTTP::Request::Common;
3160
3161 sub new
3162 {
3163     my ($class, %arg) = @_;
3164
3165     my $self = bless {
3166         pref => $arg{pref},     # Pref object
3167         timeout => $arg{timeout} || 20,
3168         debug   => $arg{debug} || 0,
3169         'list_job'     => '',
3170         'list_client'  => '',
3171         'list_fileset' => '',
3172         'list_storage' => '',
3173         'run'          => '',
3174     };
3175
3176     return $self;
3177 }
3178
3179 sub prepare
3180 {
3181     my ($self, @what) = @_;
3182     my $ua = LWP::UserAgent->new();
3183     $ua->agent("Brestore/$VERSION");
3184     my $req = POST($self->{pref}->{bconsole},
3185                    Content_Type => 'form-data',
3186                    Content => [ map { (action => $_) } @what ]);
3187     #$req->authorization_basic('eric', 'test');
3188
3189     my $res = $ua->request($req);
3190
3191     if ($res->is_success) {
3192         foreach my $l (split(/\n/, $res->content)) {
3193             my ($k, $c) = split(/=/,$l,2);
3194             $self->{$k} = $c;
3195         }
3196     } else {
3197         $self->{error} = "Can't connect to bweb : " . $res->status_line;
3198         new DlgWarn($self->{error});
3199     }
3200 }
3201
3202 sub run
3203 {
3204     my ($self, %arg) = @_;
3205
3206     my $ua = LWP::UserAgent->new();
3207     $ua->agent("Brestore/$VERSION");
3208     my $req = POST($self->{pref}->{bconsole},
3209                    Content_Type => 'form-data',
3210                    Content => [ job     => $arg{job},
3211                                 client  => $arg{client},
3212                                 storage => $arg{storage} || '',
3213                                 fileset => $arg{fileset} || '',
3214                                 where   => $arg{where}   || '',
3215                                 regexwhere  => $arg{regexwhere}  || '',
3216                                 priority=> $arg{prio}    || '',
3217                                 replace => $arg{replace},
3218                                 action  => 'run',
3219                                 timeout => 10,
3220                                 bootstrap => [$arg{bootstrap}],
3221                                 ]);
3222     #$req->authorization_basic('eric', 'test');
3223
3224     my $res = $ua->request($req);
3225
3226     if ($res->is_success) {
3227         foreach my $l (split(/\n/, $res->content)) {
3228             my ($k, $c) = split(/=/,$l,2);
3229             $self->{$k} = $c;
3230         }
3231     } 
3232
3233     if (!$self->{run}) {
3234         new DlgWarn("Can't connect to bweb : " . $res->status_line);
3235     } 
3236
3237     unlink($arg{bootstrap});
3238
3239     return $self->{run};
3240 }
3241
3242 sub list_job
3243 {
3244     my ($self) = @_;
3245     return sort split(/;/, $self->{'list_job'});
3246 }
3247
3248 sub list_fileset
3249 {
3250     my ($self) = @_;
3251     return sort split(/;/, $self->{'list_fileset'});
3252 }
3253
3254 sub list_storage
3255 {
3256     my ($self) = @_;
3257     return sort split(/;/, $self->{'list_storage'});
3258 }
3259 sub list_client
3260 {
3261     my ($self) = @_;
3262     return sort split(/;/, $self->{'list_client'});
3263 }
3264
3265 1;
3266 ################################################################
3267
3268 package main;
3269
3270 use Getopt::Long ;
3271
3272 sub HELP_MESSAGE
3273 {
3274     print STDERR "Usage: $0 [--conf=brestore.conf] [--batch] [--debug]\n";
3275     exit 1;
3276 }
3277
3278 my $file_conf = (exists $ENV{HOME})? "$ENV{HOME}/.brestore.conf" : undef ;
3279 my $batch_mod;
3280
3281 GetOptions("conf=s"   => \$file_conf,
3282            "batch"    => \$batch_mod,
3283            "debug"    => \$debug,
3284            "help"     => \&HELP_MESSAGE) ;
3285
3286 if (! defined $file_conf) {
3287     print STDERR "Could not detect default config and no config file specified\n";
3288     HELP_MESSAGE();
3289 }
3290
3291 my $p = new Pref($file_conf);
3292
3293 if (! -f $file_conf) {
3294     $p->write_config();
3295 }
3296
3297 if ($batch_mod) {
3298     my $vfs = new Bvfs(conf => $p);
3299     if ($p->connect_db()) {
3300         if ($vfs->create_brestore_tables()) {
3301             print "Creating brestore tables\n";
3302         }
3303         $vfs->update_cache();
3304     }
3305     exit (0);
3306 }
3307
3308 $glade_file = $p->{glade_file} || $glade_file;
3309
3310 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
3311     if (-f "$path/$glade_file") {
3312         $glade_file = "$path/$glade_file" ;
3313         last;
3314     }
3315 }
3316
3317 # gtk have lots of warning on stderr
3318 if ($^O eq 'MSWin32')
3319 {
3320     close(STDERR);
3321     open(STDERR, ">stderr.log");
3322 }
3323
3324 Gtk2->init();
3325
3326 if ( -f $glade_file) {
3327     my $w = new DlgResto($p);
3328
3329 } else {
3330     my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close', 
3331 "Can't find your brestore.glade (glade_file => '$glade_file')
3332 Please, edit your $file_conf to setup it." );
3333  
3334     $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3335     $widget->run;
3336     exit 1;
3337 }
3338
3339 Gtk2->main; # Start Gtk2 main loop      
3340
3341 # that's it!
3342
3343 exit 0;
3344
3345 __END__
3346 package main;
3347
3348 my $p = new Pref("$ENV{HOME}/.brestore.conf");
3349 $p->{debug} = 1;
3350 $p->connect_db() || print $p->{error};
3351
3352 my $bvfs = new Bvfs(conf => $p);
3353
3354 $bvfs->debug($bvfs->get_root());
3355 $bvfs->ch_dir($bvfs->get_root());
3356 $bvfs->up_dir();
3357 $bvfs->set_curjobids(268,178,282,281,279);
3358 $bvfs->ls_files();
3359 my $dirs = $bvfs->ls_dirs();
3360 $bvfs->ch_dir(123496);
3361 $dirs = $bvfs->ls_dirs();
3362 $bvfs->ls_files();
3363 map { $bvfs->debug($_) } $bvfs->get_all_file_versions($bvfs->{cwdid},312433, "exw3srv3", 1);