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