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