]> git.sur5r.net Git - bacula/bacula/blob - gui/brestore/brestore.pl
e019f7af66706e4a5a6fa0912b3811fb4e09f072
[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         )
1959   AND File.JobId IN ($inclause) )";
1960                         push @select_queries,($query);
1961                 }
1962                 else
1963                 {
1964                         # It's a file. Great, we allready have most 
1965                         # of what is needed. Simple and efficient query
1966                         my $dir = $entry->[0];
1967                         my $file = $entry->[1];
1968                         
1969                         my $jobid = $entry->[3];
1970                         my $fileindex = $entry->[9];
1971                         my $inclause = $entry->[5]; # curjobids
1972                         my $query = 
1973 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
1974   FROM File,Path,Filename
1975   WHERE File.PathId = $dir
1976   AND File.PathId = Path.PathId
1977   AND File.FilenameId = $file
1978   AND File.FilenameId = Filename.FilenameId
1979   AND File.JobId = $jobid
1980  )
1981 ";
1982                         push @select_queries,($query);
1983                 }
1984         }
1985         $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
1986
1987         #Now we run the query and parse the result...
1988         # there may be a lot of records, so we better be efficient
1989         # We use the bind column method, working with references...
1990
1991         my $sth = $self->dbh_prepare($query);
1992         $sth->execute;
1993
1994         my ($path,$name,$fileindex,$jobid);
1995         $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
1996         
1997         # The temp place we're going to save all file
1998         # list to before the real list
1999         my @temp_list;
2000
2001         RECORD_LOOP:
2002         while ($sth->fetchrow_arrayref())
2003         {
2004                 # This may look dumb, but we're going to do a join by ourselves,
2005                 # to save memory and avoid sending a complex query to mysql
2006                 my $complete_path = $path . $name;
2007                 my $is_dir = 0;
2008                 
2009                 if ( $name eq '')
2010                 {
2011                         $is_dir = 1;
2012                 }
2013                 
2014                 # Remove trailing slash (normalize file and dir name)
2015                 $complete_path =~ s/\/$//;
2016                 
2017                 # Let's find the ref(s) for the %mediainfo element(s) 
2018                 # containing the data for this file
2019                 # There can be several matches. It is the pseudo join.
2020                 my $med_idx=0;
2021                 my $max_elt=@{$mediainfos{$jobid}}-1;
2022                 MEDIA_LOOP:
2023                 while($med_idx <= $max_elt)
2024                 {
2025                         my $ref = $mediainfos{$jobid}->[$med_idx];
2026                         # First, can we get rid of the first elements of the
2027                         # array ? (if they don't contain valuable records
2028                         # anymore
2029                         if ($fileindex > $ref->[5])
2030                         {
2031                                 # It seems we don't need anymore
2032                                 # this entry in %mediainfo (the input data
2033                                 # is sorted...)
2034                                 # We get rid of it.
2035                                 shift @{$mediainfos{$jobid}};
2036                                 $max_elt--;
2037                                 next MEDIA_LOOP;
2038                         }
2039                         # We will do work on this elt. We can ++
2040                         # $med_idx for next loop
2041                         $med_idx++;
2042
2043                         # %mediainfo row looks like : 
2044                         # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2045                         # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2046                         # MediaType)
2047                         
2048                         # We are in range. We store and continue looping
2049                         # in the medias
2050                         if ($fileindex >= $ref->[4])
2051                         {
2052                                 my @data = ($complete_path,$is_dir,
2053                                             $fileindex,$ref);
2054                                 push @temp_list,(\@data);
2055                                 next MEDIA_LOOP;
2056                         }
2057                         
2058                         # We are not in range. No point in continuing looping
2059                         # We go to next record.
2060                         next RECORD_LOOP;
2061                 }
2062         }
2063         # Now we have the array.
2064         # We're going to sort it, by 
2065         # path, volsessiontime DESC (get the most recent file...)
2066         # The array rows look like this :
2067         # complete_path,is_dir,fileindex,
2068         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2069         #       LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2070         @temp_list = sort {$a->[0] cmp $b->[0]
2071                         || $b->[3]->[2] <=> $a->[3]->[2]
2072                           } @temp_list;
2073
2074         my @restore_list;
2075         my $prev_complete_path='////'; # Sure not to match
2076         my $prev_is_file=1;
2077         my $prev_jobid;
2078
2079         while (my $refrow = shift @temp_list)
2080         {
2081                 # For the sake of readability, we load $refrow 
2082                 # contents in real scalars
2083                 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2084                 my $jobid= $refother->[0]; # We don't need the rest...
2085
2086                 # We skip this entry.
2087                 # We allready have a newer one and this 
2088                 # isn't a continuation of the same file
2089                 next if ($complete_path eq $prev_complete_path 
2090                          and $jobid != $prev_jobid);
2091                 
2092                 
2093                 if ($prev_is_file 
2094                     and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2095                 {
2096                         # We would be recursing inside a file.
2097                         # Just what we don't want (dir replaced by file
2098                         # between two backups
2099                         next;
2100                 }
2101                 elsif ($is_dir)
2102                 {
2103                         # It is a directory
2104                         push @restore_list,($refrow);
2105                         
2106                         $prev_complete_path = $complete_path;
2107                         $prev_jobid = $jobid;
2108                         $prev_is_file = 0;
2109                 }
2110                 else
2111                 {
2112                         # It is a file
2113                         push @restore_list,($refrow);
2114                         
2115                         $prev_complete_path = $complete_path;
2116                         $prev_jobid = $jobid;
2117                         $prev_is_file = 1;
2118                 }
2119         }
2120         # We get rid of @temp_list... save memory
2121         @temp_list=();
2122
2123         # Ok everything is in the list. Let's sort it again in another way.
2124         # This time it will be in the bsr file order
2125
2126         # we sort the results by 
2127         # volsessiontime, volsessionid, volindex, fileindex 
2128         # to get all files in right order...
2129         # Reminder : The array rows look like this :
2130         # complete_path,is_dir,fileindex,
2131         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2132         #       StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2133
2134         @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2] 
2135                            || $a->[3]->[1] <=> $b->[3]->[1] 
2136                            || $a->[3]->[7] <=> $b->[3]->[7] 
2137                            || $a->[2] <=> $b->[2] } 
2138                                 @restore_list;
2139
2140         # Now that everything is ready, we create the bsr
2141         my $prev_fileindex=-1;
2142         my $prev_volsessionid=-1;
2143         my $prev_volsessiontime=-1;
2144         my $prev_volumename=-1;
2145         my $prev_volfile=-1;
2146         my $prev_mediatype;
2147         my $prev_volblocks;
2148         my $count=0;
2149         my $first_of_current_range=0;
2150         my @fileindex_ranges;
2151         my $bsr='';
2152
2153         foreach my $refrow (@restore_list)
2154         {
2155                 my (undef,undef,$fileindex,$refother)=@{$refrow};
2156                 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2157                     $volblocks,undef,$volumename,$mediatype)=@{$refother};
2158                 
2159                 # We can specifiy the number of files in each section of the
2160                 # bsr to speedup restore (bacula can then jump over the
2161                 # end of tape files.
2162                 $count++;
2163                 
2164                 
2165                 if ($prev_volumename eq '-1')
2166                 {
2167                         # We only have to start the new range...
2168                         $first_of_current_range=$fileindex;
2169                 }
2170                 elsif ($prev_volsessionid != $volsessionid 
2171                        or $prev_volsessiontime != $volsessiontime 
2172                        or $prev_volumename ne $volumename 
2173                        or $prev_volfile ne $volfile)
2174                 {
2175                         # We have to create a new section in the bsr...
2176                         # We print the previous one ... 
2177                         # (before that, save the current range ...)
2178                         if ($first_of_current_range != $prev_fileindex)
2179                         {
2180                                 # we are in a range
2181                                 push @fileindex_ranges,
2182                                     ("$first_of_current_range-$prev_fileindex");
2183                         }
2184                         else
2185                         {
2186                                  # We are out of a range,
2187                                  # but there is only one element in the range
2188                                 push @fileindex_ranges,
2189                                     ("$first_of_current_range");
2190                         }
2191                         
2192                         $bsr.=print_bsr_section(\@fileindex_ranges,
2193                                                 $prev_volsessionid,
2194                                                 $prev_volsessiontime,
2195                                                 $prev_volumename,
2196                                                 $prev_volfile,
2197                                                 $prev_mediatype,
2198                                                 $prev_volblocks,
2199                                                 $count-1);
2200                         $count=1;
2201                         # Reset for next loop
2202                         @fileindex_ranges=();
2203                         $first_of_current_range=$fileindex;
2204                 }
2205                 elsif ($fileindex-1 != $prev_fileindex)
2206                 {
2207                         # End of a range of fileindexes
2208                         if ($first_of_current_range != $prev_fileindex)
2209                         {
2210                                 #we are in a range
2211                                 push @fileindex_ranges,
2212                                     ("$first_of_current_range-$prev_fileindex");
2213                         }
2214                         else
2215                         {
2216                                  # We are out of a range,
2217                                  # but there is only one element in the range
2218                                 push @fileindex_ranges,
2219                                     ("$first_of_current_range");
2220                         }
2221                         $first_of_current_range=$fileindex;
2222                 }
2223                 $prev_fileindex=$fileindex;
2224                 $prev_volsessionid = $volsessionid;
2225                 $prev_volsessiontime = $volsessiontime;
2226                 $prev_volumename = $volumename;
2227                 $prev_volfile=$volfile;
2228                 $prev_mediatype=$mediatype;
2229                 $prev_volblocks=$volblocks;
2230
2231         }
2232
2233         # Ok, we're out of the loop. Alas, there's still the last record ...
2234         if ($first_of_current_range != $prev_fileindex)
2235         {
2236                 # we are in a range
2237                 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2238                 
2239         }
2240         else
2241         {
2242                 # We are out of a range,
2243                 # but there is only one element in the range
2244                 push @fileindex_ranges,("$first_of_current_range");
2245                 
2246         }
2247         $bsr.=print_bsr_section(\@fileindex_ranges,
2248                                 $prev_volsessionid,
2249                                 $prev_volsessiontime,
2250                                 $prev_volumename,
2251                                 $prev_volfile,
2252                                 $prev_mediatype,
2253                                 $prev_volblocks,
2254                                 $count);
2255         
2256         return $bsr;
2257 }
2258
2259 sub print_bsr_section
2260 {
2261     my ($ref_fileindex_ranges,$volsessionid,
2262         $volsessiontime,$volumename,$volfile,
2263         $mediatype,$volblocks,$count)=@_;
2264     
2265     my $bsr='';
2266     $bsr .= "Volume=\"$volumename\"\n";
2267     $bsr .= "MediaType=\"$mediatype\"\n";
2268     $bsr .= "VolSessionId=$volsessionid\n";
2269     $bsr .= "VolSessionTime=$volsessiontime\n";
2270     $bsr .= "VolFile=$volfile\n";
2271     $bsr .= "VolBlock=$volblocks\n";
2272     
2273     foreach my $range (@{$ref_fileindex_ranges})
2274     {
2275         $bsr .= "FileIndex=$range\n";
2276     }
2277     
2278     $bsr .= "Count=$count\n";
2279     return $bsr;
2280 }
2281
2282 1;
2283
2284 ################################################################
2285
2286 package Bvfs;
2287 use base qw/Bbase/;
2288
2289 sub get_pathid
2290 {
2291     my ($self, $dir) = @_;
2292     my $query = 
2293         "SELECT PathId FROM Path WHERE Path = ?";
2294     my $sth = $self->dbh_prepare($query);
2295     $sth->execute($dir);
2296     my $result = $sth->fetchall_arrayref();
2297     $sth->finish();
2298     
2299     return join(',', map { $_->[0] } @$result);
2300 }
2301
2302
2303 sub update_cache
2304 {
2305     my ($self) = @_;
2306
2307     my $query = "SELECT JobId from Job WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) order by JobId";
2308     my $jobs = $self->dbh_selectall_arrayref($query);
2309
2310     $self->update_brestore_table(map { $_->[0] } @$jobs);
2311 }
2312
2313 sub get_root
2314 {
2315     my ($self, $dir) = @_;
2316     return $self->get_pathid('');
2317 }
2318
2319 sub ch_dir
2320 {
2321     my ($self, $pathid) = @_;
2322     $self->{cwd} = $pathid;
2323 }
2324
2325 sub up_dir
2326 {
2327     my ($self) = @_ ;
2328     my $query = 
2329   "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId IN ($self->{cwd}) ";
2330
2331     my $all = $self->dbh_selectall_arrayref($query);
2332     return unless ($all);       # already at root
2333
2334     my $dir = join(',', map { $_->[0] } @$all);
2335     if ($dir) {
2336         $self->{cwd} = $dir;
2337     }
2338 }
2339
2340 sub pwd
2341 {
2342     my ($self) = @_;
2343     return $self->get_path($self->{cwd});
2344 }
2345
2346 sub get_path
2347 {
2348     my ($self, $pathid) = @_;
2349     $self->debug("Call with pathid = $pathid");
2350     my $query = 
2351         "SELECT Path FROM Path WHERE PathId IN (?)";
2352
2353     my $sth = $self->dbh_prepare($query);
2354     $sth->execute($pathid);
2355     my $result = $sth->fetchrow_arrayref();
2356     $sth->finish();
2357     return $result->[0];    
2358 }
2359
2360 sub set_curjobids
2361 {
2362     my ($self, @jobids) = @_;
2363     $self->{curjobids} = join(',', @jobids);
2364     $self->update_brestore_table(@jobids);
2365 }
2366
2367 sub ls_files
2368 {
2369     my ($self) = @_;
2370
2371     return undef unless ($self->{curjobids});
2372
2373     my $inclause   = $self->{curjobids};
2374     my $inlistpath = $self->{cwd};
2375
2376     my $query = 
2377 "SELECT File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
2378  FROM
2379         (SELECT Filename.Name, max(File.FileId) as id
2380          FROM File, Filename
2381          WHERE File.FilenameId = Filename.FilenameId
2382            AND Filename.Name != ''
2383            AND File.PathId IN ($inlistpath)
2384            AND File.JobId IN ($inclause)
2385          GROUP BY Filename.Name
2386          ORDER BY Filename.Name) AS listfiles,
2387 File
2388 WHERE File.FileId = listfiles.id";
2389         
2390     $self->debug($query);
2391     my $result = $self->dbh_selectall_arrayref($query);
2392     $self->debug($result);
2393         
2394     return $result;
2395 }
2396
2397 # return ($dirid,$dir_basename,$lstat,$jobid)
2398 sub ls_dirs
2399 {
2400     my ($self) = @_;
2401
2402     return undef unless ($self->{curjobids});
2403
2404     my $pathid = $self->{cwd};
2405     my $jobclause = $self->{curjobids};
2406
2407     # Let's retrieve the list of the visible dirs in this dir ...
2408     # First, I need the empty filenameid to locate efficiently the dirs in the file table
2409     my $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
2410     my $sth = $self->dbh_prepare($query);
2411     $sth->execute();
2412     my $result = $sth->fetchrow_arrayref();
2413     $sth->finish();
2414     my $dir_filenameid = $result->[0];
2415      
2416     # Then we get all the dir entries from File ...
2417     $query = "
2418 SELECT PathId, Path, JobId, Lstat FROM (
2419     
2420     SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
2421            listfile1.JobId, listfile1.Lstat
2422     FROM (
2423         SELECT DISTINCT brestore_pathhierarchy1.PathId
2424         FROM brestore_pathhierarchy AS brestore_pathhierarchy1
2425         JOIN Path AS Path2
2426             ON (brestore_pathhierarchy1.PathId = Path2.PathId)
2427         JOIN brestore_pathvisibility AS brestore_pathvisibility1
2428             ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
2429         WHERE brestore_pathhierarchy1.PPathId = $pathid
2430         AND brestore_pathvisibility1.jobid IN ($jobclause)) AS listpath1
2431     JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
2432     LEFT JOIN (
2433         SELECT File1.PathId, File1.JobId, File1.Lstat FROM File AS File1
2434         WHERE File1.FilenameId = $dir_filenameid
2435         AND File1.JobId IN ($jobclause)) AS listfile1
2436         ON (listpath1.PathId = listfile1.PathId)
2437      ) AS A ORDER BY 2,3 DESC
2438 ";
2439     $self->debug($query);
2440     $sth=$self->dbh_prepare($query);
2441     $sth->execute();
2442     $result = $sth->fetchall_arrayref();
2443     my @return_list;
2444     my $prev_dir='';
2445     foreach my $refrow (@{$result})
2446     {
2447         my $dirid = $refrow->[0];
2448         my $dir = $refrow->[1];
2449         my $lstat = $refrow->[3];
2450         my $jobid = $refrow->[2] || 0;
2451         next if ($dirid eq $prev_dir);
2452         # We have to clean up this dirname ... we only want it's 'basename'
2453         my $return_value;
2454         if ($dir ne '/')
2455         {
2456             my @temp = split ('/',$dir);
2457             $return_value = pop @temp;
2458         }
2459         else
2460         {
2461             $return_value = '/';
2462         }
2463         my @return_array = ($dirid,$return_value,$lstat,$jobid);
2464         push @return_list,(\@return_array);
2465         $prev_dir = $dirid;
2466     }
2467     $self->debug(\@return_list);
2468     return \@return_list;    
2469 }
2470
2471 # Returns the list of media required for a list of jobids.
2472 # Input : self, jobid1, jobid2...
2473 # Output : reference to array of (joibd, inchanger)
2474 sub get_required_media_from_jobid
2475 {
2476     my ($self, @jobids)=@_;
2477     my $inclause = join(',',@jobids);
2478     my $query = "
2479 SELECT DISTINCT JobMedia.MediaId, Media.InChanger 
2480 FROM JobMedia, Media 
2481 WHERE JobMedia.MediaId=Media.MediaId 
2482 AND JobId In ($inclause)
2483 ORDER BY MediaId";
2484     my $result = $self->dbh_selectall_arrayref($query);
2485     return $result;
2486 }
2487
2488 # Returns the fileindex from dirname and jobid.
2489 # Input : self, dirid, jobid
2490 # Output : fileindex
2491 sub get_fileindex_from_dir_jobid
2492 {
2493     my ($self, $dirid, $jobid)=@_;
2494     my $query;
2495     $query = "SELECT File.FileIndex
2496                 FROM File, Filename
2497                 WHERE File.FilenameId = Filename.FilenameId
2498                 AND File.PathId = $dirid
2499                 AND Filename.Name = ''
2500                 AND File.JobId = '$jobid'
2501                 ";
2502                 
2503     $self->debug($query);
2504     my $result = $self->dbh_selectall_arrayref($query);
2505     return $result->[0]->[0];
2506 }
2507
2508 # Returns the fileindex from filename and jobid.
2509 # Input : self, dirid, filenameid, jobid
2510 # Output : fileindex
2511 sub get_fileindex_from_file_jobid
2512 {
2513     my ($self, $dirid, $filenameid, $jobid)=@_;
2514     
2515     my $query;
2516     $query = 
2517 "SELECT File.FileIndex
2518  FROM File
2519  WHERE File.PathId = $dirid
2520    AND File.FilenameId = $filenameid
2521    AND File.JobId = $jobid";
2522                 
2523     $self->debug($query);
2524     my $result = $self->dbh_selectall_arrayref($query);
2525     return $result->[0]->[0];
2526 }
2527
2528 # This function estimates the size to be restored for an entry of the restore
2529 # list
2530 # In : self,reference to the entry
2531 # Out : size in bytes, number of files
2532 sub estimate_restore_size
2533 {
2534     # reminder : restore_list looks like this : 
2535     # ($pid,$fid,$name,$jobid,'file',$curjobids, 
2536     #  undef, undef, undef, $dirfileindex);
2537     my ($self, $entry, $refresh) = @_;
2538     my $query;
2539     if ($entry->[4] eq 'dir')
2540     {
2541         my $dir = $entry->[0];
2542
2543         my $inclause = $entry->[5]; #curjobids
2544         $query = 
2545 "SELECT Path.Path, File.FilenameId, File.LStat
2546   FROM File, Path, Job
2547   WHERE Path.PathId = File.PathId
2548   AND File.JobId = Job.JobId
2549   AND Path.Path LIKE 
2550         (SELECT " . $self->dbh_strcat('Path',"'\%'") . " FROM Path WHERE PathId IN ($dir)
2551         )
2552   AND File.JobId IN ($inclause)
2553   ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2554     }
2555     else
2556     {
2557         # It's a file. Great, we allready have most 
2558         # of what is needed. Simple and efficient query
2559         my $dir = $entry->[0];
2560         my $fileid = $entry->[1];
2561         
2562         my $jobid = $entry->[3];
2563         my $fileindex = $entry->[9];
2564         my $inclause = $entry->[5]; # curjobids
2565         $query = 
2566 "SELECT Path.Path, File.FilenameId, File.Lstat
2567   FROM File, Path
2568   WHERE Path.PathId = File.PathId
2569   AND Path.PathId = $dir
2570   AND File.FilenameId = $fileid
2571   AND File.JobId = $jobid";
2572     }
2573
2574     my ($path,$nameid,$lstat);
2575     my $sth = $self->dbh_prepare($query);
2576     $sth->execute;
2577     $sth->bind_columns(\$path,\$nameid,\$lstat);
2578     my $old_path='';
2579     my $old_nameid='';
2580     my $total_size=0;
2581     my $total_files=0;
2582
2583     &$refresh();
2584
2585     my $rcount=0;
2586     # We fetch all rows
2587     while ($sth->fetchrow_arrayref())
2588     {
2589         # Only the latest version of a file
2590         next if ($nameid eq $old_nameid and $path eq $old_path);
2591
2592         if ($rcount > 15000) {
2593             &$refresh();
2594             $rcount=0;
2595         } else {
2596             $rcount++;
2597         }
2598
2599         # We get the size of this file
2600         my $size=lstat_attrib($lstat,'st_size');
2601         $total_size += $size;
2602         $total_files++;
2603         $old_path=$path;
2604         $old_nameid=$nameid;
2605     }
2606
2607     return ($total_size,$total_files);
2608 }
2609
2610 # Returns list of versions of a file that could be restored
2611 # returns an array of 
2612 # ('FILE:',jobid,fileindex,mtime,size,inchanger,md5,volname)
2613 # there will be only one jobid in the array of jobids...
2614 sub get_all_file_versions
2615 {
2616     my ($self,$pathid,$fileid,$client,$see_all)=@_;
2617     
2618     defined $see_all or $see_all=0;
2619     
2620     my @versions;
2621     my $query;
2622     $query = 
2623 "SELECT File.JobId, File.FileIndex, File.Lstat, 
2624         File.Md5, Media.VolumeName, Media.InChanger
2625  FROM File, Job, Client, JobMedia, Media
2626  WHERE File.FilenameId = $fileid
2627    AND File.PathId=$pathid
2628    AND File.JobId = Job.JobId
2629    AND Job.ClientId = Client.ClientId
2630    AND Job.JobId = JobMedia.JobId
2631    AND File.FileIndex >= JobMedia.FirstIndex
2632    AND File.FileIndex <= JobMedia.LastIndex
2633    AND JobMedia.MediaId = Media.MediaId
2634    AND Client.Name = '$client'";
2635         
2636     $self->debug($query);
2637         
2638     my $result = $self->dbh_selectall_arrayref($query);
2639         
2640     foreach my $refrow (@$result)
2641     {
2642         my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2643         my @attribs = parse_lstat($lstat);
2644         my $mtime = array_attrib('st_mtime',\@attribs);
2645         my $size = array_attrib('st_size',\@attribs);
2646                 
2647         my @list = ('FILE:',$pathid,$fileid,$jobid,
2648                     $fileindex, $mtime, $size, $inchanger,
2649                     $md5, $volname);
2650         push @versions, (\@list);
2651     }
2652         
2653     # We have the list of all versions of this file.
2654     # We'll sort it by mtime desc, size, md5, inchanger desc
2655     # the rest of the algorithm will be simpler
2656     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2657     @versions = sort { $b->[5] <=> $a->[5] 
2658                     || $a->[6] <=> $b->[6] 
2659                     || $a->[8] cmp $a->[8] 
2660                     || $b->[7] <=> $a->[7]} @versions;
2661
2662         
2663     my @good_versions;
2664     my %allready_seen_by_mtime;
2665     my %allready_seen_by_md5;
2666     # Now we should create a new array with only the interesting records
2667     foreach my $ref (@versions)
2668     {   
2669         if ($ref->[8])
2670         {
2671             # The file has a md5. We compare his md5 to other known md5...
2672             # We take size into account. It may happen that 2 files
2673             # have the same md5sum and are different. size is a supplementary
2674             # criterion
2675             
2676             # If we allready have a (better) version
2677             next if ( (not $see_all) 
2678                       and $allready_seen_by_md5{$ref->[8] .'-'. $ref->[6]}); 
2679
2680             # we never met this one before...
2681             $allready_seen_by_md5{$ref->[8] .'-'. $ref->[6]}=1;
2682         }
2683         # Even if it has a md5, we should also work with mtimes
2684         # We allready have a (better) version
2685         next if ( (not $see_all)
2686                   and $allready_seen_by_mtime{$ref->[5] .'-'. $ref->[6]}); 
2687         $allready_seen_by_mtime{$ref->[5] .'-'. $ref->[6] . '-' . $ref->[8]}=1;
2688         
2689         # We reached there. The file hasn't been seen.
2690         push @good_versions,($ref);
2691     }
2692         
2693     # To be nice with the user, we re-sort good_versions by
2694     # inchanger desc, mtime desc
2695     @good_versions = sort { $b->[5] <=> $a->[5] 
2696                          || $b->[3] <=> $a->[3]} @good_versions;
2697         
2698     return @good_versions;
2699 }
2700
2701
2702 sub update_brestore_table
2703 {
2704     my ($self, @jobs) = @_;
2705
2706     $self->debug(\@jobs);
2707
2708     foreach my $job (sort {$a <=> $b} @jobs)
2709     {
2710         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
2711         my $retour = $self->dbh_selectrow_arrayref($query);
2712         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
2713
2714         print STDERR "Inserting path records for JobId $job\n";
2715         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId) 
2716                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
2717
2718         $self->dbh_do($query);
2719
2720         # Now we have to do the directory recursion stuff to determine missing visibility
2721         # We try to avoid recursion, to be as fast as possible
2722         # We also only work on not allready hierarchised directories...
2723
2724         print STDERR "Creating missing recursion paths for $job\n";
2725
2726         $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility 
2727                   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
2728                   LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
2729                   WHERE brestore_pathvisibility.JobId = $job
2730                   AND brestore_pathhierarchy.PathId IS NULL
2731                   ORDER BY Path";
2732
2733         my $sth = $self->dbh_prepare($query);
2734         $sth->execute();
2735         my $pathid; my $path;
2736         $sth->bind_columns(\$pathid,\$path);
2737         
2738         while ($sth->fetch)
2739         {
2740             $self->build_path_hierarchy($path,$pathid);
2741         }
2742         $sth->finish();
2743
2744         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
2745         # This query gives all parent pathids for a given jobid that aren't stored.
2746         # It has to be called until no record is updated ...
2747         $query = "
2748         INSERT INTO brestore_pathvisibility (PathId, JobId) (
2749         SELECT a.PathId,$job
2750         FROM
2751                 (SELECT DISTINCT h.PPathId AS PathId
2752                 FROM brestore_pathhierarchy AS h
2753                 JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
2754                 WHERE p.JobId=$job) AS a
2755                 LEFT JOIN
2756                 (SELECT PathId
2757                 FROM brestore_pathvisibility
2758                 WHERE JobId=$job) AS b
2759                 ON (a.PathId = b.PathId)
2760         WHERE b.PathId IS NULL)";
2761
2762         my $rows_affected;
2763         while (($rows_affected = $self->dbh_do($query)) and ($rows_affected !~ /^0/))
2764         {
2765             print STDERR "Recursively adding $rows_affected records from $job\n";
2766         }
2767         # Job's done
2768         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
2769         $self->dbh_do($query);
2770     }
2771 }
2772
2773 sub cleanup_brestore_table
2774 {
2775     my ($self) = @_;
2776
2777     my $query = "SELECT JobId from brestore_knownjobid";
2778     my @jobs = @{$self->dbh_selectall_arrayref($query)};
2779
2780     foreach my $jobentry (@jobs)
2781     {
2782         my $job = $jobentry->[0];
2783         $query = "SELECT FileId from File WHERE JobId = $job LIMIT 1";
2784         my $result = $self->dbh_selectall_arrayref($query);
2785         if (scalar(@{$result}))
2786         {
2787             # There are still files for this jobid
2788             print STDERR "$job still exists. Not cleaning...\n";
2789
2790         } else {
2791                 $query = "DELETE FROM brestore_pathvisibility WHERE JobId = $job";
2792                 $self->dbh_do($query);
2793                 $query = "DELETE FROM brestore_knownjobid WHERE JobId = $job";
2794                 $self->dbh_do($query);
2795         }
2796     }
2797 }
2798
2799 sub parent_dir
2800 {
2801     my ($path) = @_;
2802     # Root Unix case :
2803     if ($path eq '/')
2804     {
2805         return '';
2806     }
2807     # Root Windows case :
2808     if ($path =~ /^[a-z]+:\/$/i)
2809     {
2810         return '';
2811     }
2812     # Split
2813     my @tmp = split('/',$path);
2814     # We remove the last ...
2815     pop @tmp;
2816     my $tmp = join ('/',@tmp) . '/';
2817     return $tmp;
2818 }
2819
2820 sub build_path_hierarchy
2821 {
2822     my ($self, $path,$pathid)=@_;
2823     # Does the ppathid exist for this ? we use a memory cache...
2824     # In order to avoid the full loop, we consider that if a dir is allready in the
2825     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
2826     while ($path ne '')
2827     {
2828         if (! $self->{cache_ppathid}->{$pathid})
2829         {
2830             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
2831             my $sth2 = $self->{conf}->{dbh}->prepare_cached($query);
2832             $sth2->execute($pathid);
2833             # Do we have a result ?
2834             if (my $refrow = $sth2->fetchrow_arrayref)
2835             {
2836                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
2837                 $sth2->finish();
2838                 # This dir was in the db ...
2839                 # It means we can leave, the tree has allready been built for
2840                 # this dir
2841                 return 1;
2842             } else {
2843                 $sth2->finish();
2844                 # We have to create the record ...
2845                 # What's the current p_path ?
2846                 my $ppath = parent_dir($path);
2847                 my $ppathid = $self->return_pathid_from_path($ppath);
2848                 $self->{cache_ppathid}->{$pathid}= $ppathid;
2849                 
2850                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
2851                 $sth2 = $self->{conf}->{dbh}->prepare_cached($query);
2852                 $sth2->execute($pathid,$ppathid);
2853                 $sth2->finish();
2854                 $path = $ppath;
2855                 $pathid = $ppathid;
2856             }
2857         } else {
2858            # It's allready in the cache.
2859            # We can leave, no time to waste here, all the parent dirs have allready
2860            # been done
2861            return 1;
2862         }
2863     }
2864     return 1;
2865 }
2866
2867
2868 sub return_pathid_from_path
2869 {
2870     my ($self, $path) = @_;
2871     my $query = "SELECT PathId FROM Path WHERE Path = ?";
2872
2873     #print STDERR $query,"\n" if $debug;
2874     my $sth = $self->{conf}->{dbh}->prepare_cached($query);
2875     $sth->execute($path);
2876     my $result =$sth->fetchrow_arrayref();
2877     $sth->finish();
2878     if (defined $result)
2879     {
2880         return $result->[0];
2881
2882     } else {
2883         # A bit dirty : we insert into path, and we have to be sure
2884         # we aren't deleted by a purge. We still need to insert into path to get
2885         # the pathid, because of mysql
2886         $query = "INSERT INTO Path (Path) VALUES (?)";
2887         #print STDERR $query,"\n" if $debug;
2888         $sth = $self->{conf}->{dbh}->prepare_cached($query);
2889         $sth->execute($path);
2890         $sth->finish();
2891         
2892         $query = "SELECT PathId FROM Path WHERE Path = ?";
2893         #print STDERR $query,"\n" if $debug;
2894         $sth = $self->{conf}->{dbh}->prepare_cached($query);
2895         $sth->execute($path);
2896         $result = $sth->fetchrow_arrayref();
2897         $sth->finish();
2898         return $result->[0];
2899     }
2900 }
2901
2902
2903 sub create_brestore_tables
2904 {
2905     my ($self) = @_;
2906
2907     my $verif = "SELECT 1 FROM brestore_knownjobid LIMIT 1";
2908
2909     unless ($self->dbh_do($verif)) {
2910         new DlgWarn("brestore can't find brestore_xxx tables on your database. I will create them.");
2911
2912         $self->{error} = "Creating internal brestore tables";
2913         my $req = "
2914     CREATE TABLE brestore_knownjobid
2915     (
2916      JobId int4 NOT NULL,
2917      CONSTRAINT brestore_knownjobid_pkey PRIMARY KEY (JobId)
2918     )";
2919         $self->dbh_do($req);
2920     }
2921     
2922     $verif = "SELECT 1 FROM brestore_pathhierarchy LIMIT 1";
2923     unless ($self->dbh_do($verif)) {
2924         my $req = "
2925    CREATE TABLE brestore_pathhierarchy
2926    (
2927      PathId int4 NOT NULL,
2928      PPathId int4 NOT NULL,
2929      CONSTRAINT brestore_pathhierarchy_pkey PRIMARY KEY (PathId)
2930    )";
2931         $self->dbh_do($req);
2932
2933
2934         $req = "CREATE INDEX brestore_pathhierarchy_ppathid 
2935                           ON brestore_pathhierarchy (PPathId)";
2936         $self->dbh_do($req);
2937     }
2938     
2939     $verif = "SELECT 1 FROM brestore_pathvisibility LIMIT 1";
2940     unless ($self->dbh_do($verif)) {
2941         my $req = "
2942     CREATE TABLE brestore_pathvisibility
2943     (
2944       PathId int4 NOT NULL,
2945       JobId int4 NOT NULL,
2946       Size int8 DEFAULT 0,
2947       Files int4 DEFAULT 0,
2948       CONSTRAINT brestore_pathvisibility_pkey PRIMARY KEY (JobId, PathId)
2949     )";
2950         $self->dbh_do($req);
2951
2952         $req = "CREATE INDEX brestore_pathvisibility_jobid
2953                           ON brestore_pathvisibility (JobId)";
2954         $self->dbh_do($req);
2955     }
2956 }
2957
2958 # Get metadata
2959 {
2960     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
2961                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
2962                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
2963                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
2964                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
2965                           'data_stream' => 15);;
2966     sub array_attrib
2967     {
2968         my ($attrib,$ref_attrib)=@_;
2969         return $ref_attrib->[$attrib_name_id{$attrib}];
2970     }
2971         
2972     sub file_attrib
2973     {   # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
2974
2975         my ($file, $attrib)=@_;
2976         
2977         if (defined $attrib_name_id{$attrib}) {
2978
2979             my @d = split(' ', $file->[3]) ; # TODO : cache this
2980             
2981             return from_base64($d[$attrib_name_id{$attrib}]);
2982
2983         } elsif ($attrib eq 'jobid') {
2984
2985             return $file->[4];
2986
2987         } elsif ($attrib eq 'name') {
2988
2989             return $file->[2];
2990             
2991         } else  {
2992             die "Attribute not known : $attrib.\n";
2993         }
2994     }
2995     
2996     sub lstat_attrib
2997     {
2998         my ($lstat,$attrib)=@_;
2999         if ($lstat and defined $attrib_name_id{$attrib}) 
3000         {
3001             my @d = split(' ', $lstat) ; # TODO : cache this
3002             return from_base64($d[$attrib_name_id{$attrib}]);
3003         }
3004         return 0;
3005     }
3006 }
3007
3008 {
3009     # Base 64 functions, directly from recover.pl.
3010     # Thanks to
3011     # Karl Hakimian <hakimian@aha.com>
3012     # This section is also under GPL v2 or later.
3013     my @base64_digits;
3014     my @base64_map;
3015     my $is_init=0;
3016     sub init_base64
3017     {
3018         @base64_digits = (
3019         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
3020         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
3021         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
3022         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
3023         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
3024                           );
3025         @base64_map = (0) x 128;
3026         
3027         for (my $i=0; $i<64; $i++) {
3028             $base64_map[ord($base64_digits[$i])] = $i;
3029         }
3030         $is_init = 1;
3031     }
3032
3033     sub from_base64 {
3034         if(not $is_init)
3035         {
3036             init_base64();
3037         }
3038         my $where = shift;
3039         my $val = 0;
3040         my $i = 0;
3041         my $neg = 0;
3042         
3043         if (substr($where, 0, 1) eq '-') {
3044             $neg = 1;
3045             $where = substr($where, 1);
3046         }
3047         
3048         while ($where ne '') {
3049             $val *= 64;
3050             my $d = substr($where, 0, 1);
3051             $val += $base64_map[ord(substr($where, 0, 1))];
3052             $where = substr($where, 1);
3053         }
3054         
3055         return $val;
3056     }
3057
3058     sub parse_lstat {
3059         my ($lstat)=@_;
3060         my @attribs = split(' ',$lstat);
3061         foreach my $element (@attribs)
3062         {
3063             $element = from_base64($element);
3064         }
3065         return @attribs;
3066     }
3067 }
3068
3069 1;
3070
3071 ################################################################
3072 package BwebConsole;
3073 use LWP::UserAgent;
3074 use HTTP::Request::Common;
3075
3076 sub new
3077 {
3078     my ($class, %arg) = @_;
3079
3080     my $self = bless {
3081         pref => $arg{pref},     # Pref object
3082         timeout => $arg{timeout} || 20,
3083         debug   => $arg{debug} || 0,
3084         'list_job'     => '',
3085         'list_client'  => '',
3086         'list_fileset' => '',
3087         'list_storage' => '',
3088         'run'          => '',
3089     };
3090
3091     return $self;
3092 }
3093
3094 sub prepare
3095 {
3096     my ($self, @what) = @_;
3097     my $ua = LWP::UserAgent->new();
3098     $ua->agent("Brestore/$VERSION");
3099     my $req = POST($self->{pref}->{bconsole},
3100                    Content_Type => 'form-data',
3101                    Content => [ map { (action => $_) } @what ]);
3102     #$req->authorization_basic('eric', 'test');
3103
3104     my $res = $ua->request($req);
3105
3106     if ($res->is_success) {
3107         foreach my $l (split(/\n/, $res->content)) {
3108             my ($k, $c) = split(/=/,$l,2);
3109             $self->{$k} = $c;
3110         }
3111     } else {
3112         $self->{error} = "Can't connect to bweb : " . $res->status_line;
3113         new DlgWarn($self->{error});
3114     }
3115 }
3116
3117 sub run
3118 {
3119     my ($self, %arg) = @_;
3120
3121     my $ua = LWP::UserAgent->new();
3122     $ua->agent("Brestore/$VERSION");
3123     my $req = POST($self->{pref}->{bconsole},
3124                    Content_Type => 'form-data',
3125                    Content => [ job     => $arg{job},
3126                                 client  => $arg{client},
3127                                 storage => $arg{storage} || '',
3128                                 fileset => $arg{fileset} || '',
3129                                 where   => $arg{where},
3130                                 replace => $arg{replace},
3131                                 priority=> $arg{prio}    || '',
3132                                 action  => 'run',
3133                                 timeout => 10,
3134                                 bootstrap => [$arg{bootstrap}],
3135                                 ]);
3136     #$req->authorization_basic('eric', 'test');
3137
3138     my $res = $ua->request($req);
3139
3140     if ($res->is_success) {
3141         foreach my $l (split(/\n/, $res->content)) {
3142             my ($k, $c) = split(/=/,$l,2);
3143             $self->{$k} = $c;
3144         }
3145     } 
3146
3147     if (!$self->{run}) {
3148         new DlgWarn("Can't connect to bweb : " . $res->status_line);
3149     } 
3150
3151     unlink($arg{bootstrap});
3152
3153     return $self->{run};
3154 }
3155
3156 sub list_job
3157 {
3158     my ($self) = @_;
3159     return sort split(/;/, $self->{'list_job'});
3160 }
3161
3162 sub list_fileset
3163 {
3164     my ($self) = @_;
3165     return sort split(/;/, $self->{'list_fileset'});
3166 }
3167
3168 sub list_storage
3169 {
3170     my ($self) = @_;
3171     return sort split(/;/, $self->{'list_storage'});
3172 }
3173 sub list_client
3174 {
3175     my ($self) = @_;
3176     return sort split(/;/, $self->{'list_client'});
3177 }
3178
3179 1;
3180
3181 package main;
3182
3183 use Getopt::Long ;
3184
3185 sub HELP_MESSAGE
3186 {
3187     print STDERR "Usage: $0 [--conf=brestore.conf] [--batch] [--debug]\n";
3188     exit 1;
3189 }
3190
3191 my $file_conf = "$ENV{HOME}/.brestore.conf" ;
3192 my $batch_mod;
3193
3194 GetOptions("conf=s"   => \$file_conf,
3195            "batch"    => \$batch_mod,
3196            "debug"    => \$debug,
3197            "help"     => \&HELP_MESSAGE) ;
3198
3199 my $p = new Pref($file_conf);
3200
3201 if (! -f $file_conf) {
3202     $p->write_config();
3203 }
3204
3205 if ($batch_mod) {
3206     my $vfs = new Bvfs(conf => $p);
3207     if ($p->connect_db()) {
3208         $vfs->update_cache();
3209     }
3210     exit (0);
3211 }
3212
3213 $glade_file = $p->{glade_file};
3214
3215 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
3216     if (-f "$path/$glade_file") {
3217         $glade_file = "$path/$glade_file" ;
3218         last;
3219     }
3220 }
3221
3222 # gtk have lots of warning on stderr
3223 if ($^O eq 'MSWin32')
3224 {
3225     close(STDERR);
3226     open(STDERR, ">stderr.log");
3227 }
3228
3229 Gtk2->init();
3230
3231 if ( -f $glade_file) {
3232     my $w = new DlgResto($p);
3233
3234 } else {
3235     my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close', 
3236 "Can't find your brestore.glade (glade_file => '$glade_file')
3237 Please, edit your $file_conf to setup it." );
3238  
3239     $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3240     $widget->run;
3241     exit 1;
3242 }
3243
3244 Gtk2->main; # Start Gtk2 main loop      
3245
3246 # that's it!
3247
3248 exit 0;
3249
3250 __END__
3251 package main;
3252
3253 my $p = new Pref("$ENV{HOME}/.brestore.conf");
3254 $p->{debug} = 1;
3255 $p->connect_db() || print $p->{error};
3256
3257 my $bvfs = new Bvfs(conf => $p);
3258
3259 $bvfs->debug($bvfs->get_root());
3260 $bvfs->ch_dir($bvfs->get_root());
3261 $bvfs->up_dir();
3262 $bvfs->set_curjobids(268,178,282,281,279);
3263 $bvfs->ls_files();
3264 my $dirs = $bvfs->ls_dirs();
3265 $bvfs->ch_dir(123496);
3266 $dirs = $bvfs->ls_dirs();
3267 $bvfs->ls_files();
3268 map { $bvfs->debug($_) } $bvfs->get_all_file_versions($bvfs->{cwd},312433, "exw3srv3", 1);