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