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