]> git.sur5r.net Git - bacula/bacula/blob - gui/brestore/brestore.pl
f3d7b7b500f5119a1e04b0c426eba97064a3aacf
[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       CONSTRAINT brestore_pathvisibility_pkey PRIMARY KEY (JobId, PathId)
2185     )";
2186         $self->dbh_do($req);
2187
2188         $req = "CREATE INDEX brestore_pathvisibility_jobid
2189                           ON brestore_pathvisibility (JobId)";
2190         $self->dbh_do($req);
2191     }
2192     
2193     $verif = "SELECT 1 FROM brestore_missing_path LIMIT 1";
2194     unless ($self->dbh_do($verif)) {
2195         my $req = "
2196     CREATE TABLE brestore_missing_path
2197     (
2198       PathId int4 NOT NULL,
2199       Path text NOT NULL,
2200       CONSTRAINT brestore_missing_path_pkey PRIMARY KEY (PathId)
2201     )";
2202         $self->dbh_do($req);
2203
2204         $req = "CREATE INDEX brestore_missing_path_path
2205                           ON brestore_missing_path (Path)";
2206         $self->dbh_do($req);
2207     }
2208 }
2209
2210 # Recursive function to calculate the visibility of each directory in the cache
2211 # tree Working with references to save time and memory
2212 # For each directory, we want to propagate it's visible jobids onto it's
2213 # parents directory.
2214 # A tree is visible if
2215 # - it's been in a backup pointed by the CurrentJobIds
2216 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2217 # In the second case, the directory is visible but has no metadata.
2218 # We symbolize this with lstat = 1 for this jobid in the cache.
2219
2220 # Input : reference directory
2221 # Output : visibility of this dir. Has to know visibility of all subdirs
2222 # to know it's visibility, hence the recursing.
2223 sub list_visible
2224 {
2225     my ($refdir)=@_;
2226         
2227     my %visibility;
2228     # Get the subdirs array references list
2229     my @list_ref_subdirs;
2230     while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2231     {
2232         push @list_ref_subdirs,($ref_subdir);
2233     }
2234
2235     # Now lets recurse over these subdirs and retrieve the reference of a hash
2236     # containing the jobs where they are visible
2237     foreach my $ref_subdir (@list_ref_subdirs)
2238     {
2239         my $ref_list_jobs = list_visible($ref_subdir);
2240         foreach my $jobid (keys %$ref_list_jobs)
2241         {
2242             $visibility{$jobid}=1;
2243         }
2244     }
2245
2246     # Ok. Now, we've got the list of those jobs.  We are going to update our
2247     # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2248     # the lstat for the known jobids. Put 1 in the new elements...  But first,
2249     # let's store the current jobids
2250     my @known_jobids;
2251     foreach my $jobid (keys %{$refdir->[1]})
2252     {
2253         push @known_jobids,($jobid);
2254     }
2255     
2256     # Add the new jobs
2257     foreach my $jobid (keys %visibility)
2258     {
2259         next if ($refdir->[1]->{$jobid});
2260         $refdir->[1]->{$jobid} = 1;
2261     }
2262     # Add the known_jobids to %visibility
2263     foreach my $jobid (@known_jobids)
2264     {
2265         $visibility{$jobid}=1;
2266     }
2267     return \%visibility;
2268 }
2269
2270 # Returns the list of media required for a list of jobids.
2271 # Input : dbh, jobid1, jobid2...
2272 # Output : reference to array of (joibd, inchanger)
2273 sub get_required_media_from_jobid
2274 {
2275     my ($dbh, @jobids)=@_;
2276     my $inclause = join(',',@jobids);
2277     my $query = "
2278 SELECT DISTINCT JobMedia.MediaId, Media.InChanger 
2279 FROM JobMedia, Media 
2280 WHERE JobMedia.MediaId=Media.MediaId 
2281 AND JobId In ($inclause)
2282 ORDER BY MediaId";
2283     my $result = $dbh->selectall_arrayref($query);
2284     return $result;
2285 }
2286
2287 # Returns the fileindex from dirname and jobid.
2288 # Input : dbh, dirname, jobid
2289 # Output : fileindex
2290 sub get_fileindex_from_dir_jobid
2291 {
2292     my ($dbh, $dirname, $jobid)=@_;
2293     my $query;
2294     $query = "SELECT File.FileIndex
2295                 FROM File, Filename, Path
2296                 WHERE File.FilenameId = Filename.FilenameId
2297                 AND File.PathId = Path.PathId
2298                 AND Filename.Name = ''
2299                 AND Path.Path = '$dirname'
2300                 AND File.JobId = '$jobid'
2301                 ";
2302                 
2303     print STDERR $query,"\n" if $debug;
2304     my $result = $dbh->selectall_arrayref($query);
2305     return $result->[0]->[0];
2306 }
2307
2308 # Returns the fileindex from filename and jobid.
2309 # Input : dbh, filename, jobid
2310 # Output : fileindex
2311 sub get_fileindex_from_file_jobid
2312 {
2313     my ($dbh, $filename, $jobid)=@_;
2314     
2315     my @dirs = split(/\//, $filename);
2316     $filename=pop(@dirs);
2317     my $dirname = join('/', @dirs) . '/';
2318     
2319     
2320     my $query;
2321     $query = 
2322 "SELECT File.FileIndex
2323  FROM File, Filename, Path
2324  WHERE File.FilenameId = Filename.FilenameId
2325    AND File.PathId = Path.PathId
2326    AND Filename.Name = '$filename'
2327    AND Path.Path = '$dirname'
2328    AND File.JobId = '$jobid'";
2329                 
2330     print STDERR $query,"\n" if $debug;
2331     my $result = $dbh->selectall_arrayref($query);
2332     return $result->[0]->[0];
2333 }
2334
2335
2336 # Returns list of versions of a file that could be restored
2337 # returns an array of 
2338 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2339 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2340 # and volname and md5
2341 # and of course, there will be only one jobid in the array of jobids...
2342 sub get_all_file_versions
2343 {
2344     my ($dbh,$path,$file,$client,$see_all)=@_;
2345     
2346     defined $see_all or $see_all=0;
2347     
2348     my @versions;
2349     my $query;
2350     $query = 
2351 "SELECT File.JobId, File.FileIndex, File.Lstat, 
2352         File.Md5, Media.VolumeName, Media.InChanger
2353  FROM File, Filename, Path, Job, Client, JobMedia, Media
2354  WHERE File.FilenameId = Filename.FilenameId
2355    AND File.PathId=Path.PathId
2356    AND File.JobId = Job.JobId
2357    AND Job.ClientId = Client.ClientId
2358    AND Job.JobId = JobMedia.JobId
2359    AND File.FileIndex >= JobMedia.FirstIndex
2360    AND File.FileIndex <= JobMedia.LastIndex
2361    AND JobMedia.MediaId = Media.MediaId
2362    AND Path.Path = '$path'
2363    AND Filename.Name = '$file'
2364    AND Client.Name = '$client'";
2365         
2366     print STDERR $query if $debug;
2367         
2368     my $result = $dbh->selectall_arrayref($query);
2369         
2370     foreach my $refrow (@$result)
2371     {
2372         my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2373         my @attribs = parse_lstat($lstat);
2374         my $mtime = array_attrib('st_mtime',\@attribs);
2375         my $size = array_attrib('st_size',\@attribs);
2376                 
2377         my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2378                     $inchanger, $md5, $volname);
2379         push @versions, (\@list);
2380     }
2381         
2382     # We have the list of all versions of this file.
2383     # We'll sort it by mtime desc, size, md5, inchanger desc
2384     # the rest of the algorithm will be simpler
2385     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2386     @versions = sort { $b->[4] <=> $a->[4] 
2387                     || $a->[5] <=> $b->[5] 
2388                     || $a->[7] cmp $a->[7] 
2389                     || $b->[6] <=> $a->[6]} @versions;
2390         
2391     my @good_versions;
2392     my %allready_seen_by_mtime;
2393     my %allready_seen_by_md5;
2394     # Now we should create a new array with only the interesting records
2395     foreach my $ref (@versions)
2396     {   
2397         if ($ref->[7])
2398         {
2399             # The file has a md5. We compare his md5 to other known md5...
2400             # We take size into account. It may happen that 2 files
2401             # have the same md5sum and are different. size is a supplementary
2402             # criterion
2403             
2404             # If we allready have a (better) version
2405             next if ( (not $see_all) 
2406                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}); 
2407
2408             # we never met this one before...
2409             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2410         }
2411         # Even if it has a md5, we should also work with mtimes
2412         # We allready have a (better) version
2413         next if ( (not $see_all)
2414                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]}); 
2415         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2416         
2417         # We reached there. The file hasn't been seen.
2418         push @good_versions,($ref);
2419     }
2420         
2421     # To be nice with the user, we re-sort good_versions by
2422     # inchanger desc, mtime desc
2423     @good_versions = sort { $b->[4] <=> $a->[4] 
2424                          || $b->[2] <=> $a->[2]} @good_versions;
2425         
2426     return @good_versions;
2427 }
2428
2429 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2430 # This sub creates a BSR from the information in the restore_list
2431 # Returns the BSR as a string
2432 sub create_filelist
2433 {
2434         my $self = shift;
2435         my %mediainfos;
2436         # This query gets all jobid/jobmedia/media combination.
2437         my $query = "
2438 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile, 
2439        JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2440        JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex, 
2441        Media.Volumename, Media.MediaType
2442 FROM Job, JobMedia, Media
2443 WHERE Job.JobId = JobMedia.JobId
2444   AND JobMedia.MediaId = Media.MediaId
2445   ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2446         
2447
2448         my $result = $self->dbh_selectall_arrayref($query);
2449
2450         # We will store everything hashed by jobid.
2451
2452         foreach my $refrow (@$result)
2453         {
2454                 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2455                 $firstindex, $lastindex, $startblock, $endblock,
2456                 $volindex, $volumename, $mediatype) = @{$refrow};
2457
2458                 # We just have to deal with the case where starfile != endfile
2459                 # In this case, we concatenate both, for the bsr
2460                 if ($startfile != $endfile) { 
2461                       $startfile = $startfile . '-' . $endfile;
2462                 }
2463
2464                 my @tmparray = 
2465                 ($jobid, $volsessionid, $volsessiontime, $startfile, 
2466                 $firstindex, $lastindex, $startblock .'-'. $endblock,
2467                 $volindex, $volumename, $mediatype);
2468                 
2469                 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2470         }
2471
2472         
2473         # reminder : restore_list looks like this : 
2474         # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2475         
2476         # Here, we retrieve every file/dir that could be in the restore
2477         # We do as simple as possible for the SQL engine (no crazy joins,
2478         # no pseudo join (>= FirstIndex ...), etc ...
2479         # We do a SQL union of all the files/dirs specified in the restore_list
2480         my @select_queries;
2481         foreach my $entry (@{$self->{restore_list}->{data}})
2482         {
2483                 if ($entry->[2] eq 'dir')
2484                 {
2485                         my $dir = unpack('u', $entry->[0]);
2486                         my $inclause = $entry->[3]; #curjobids
2487
2488                         my $query = 
2489 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2490   FROM File, Path, Filename
2491   WHERE Path.PathId = File.PathId
2492   AND File.FilenameId = Filename.FilenameId
2493   AND Path.Path LIKE '$dir%'
2494   AND File.JobId IN ($inclause) )";
2495                         push @select_queries,($query);
2496                 }
2497                 else
2498                 {
2499                         # It's a file. Great, we allready have most 
2500                         # of what is needed. Simple and efficient query
2501                         my $file = unpack('u', $entry->[0]);
2502                         my @file = split '/',$file;
2503                         $file = pop @file;
2504                         my $dir = join('/',@file);
2505                         
2506                         my $jobid = $entry->[1];
2507                         my $fileindex = $entry->[7];
2508                         my $inclause = $entry->[3]; # curjobids
2509                         my $query = 
2510 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2511   FROM File, Path, Filename
2512   WHERE Path.PathId = File.PathId
2513   AND File.FilenameId = Filename.FilenameId
2514   AND Path.Path = '$dir/'
2515   AND Filename.Name = '$file'
2516   AND File.JobId = $jobid)";
2517                         push @select_queries,($query);
2518                 }
2519         }
2520         $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2521
2522         print STDERR $query,"\n" if $debug;
2523         
2524         #Now we run the query and parse the result...
2525         # there may be a lot of records, so we better be efficient
2526         # We use the bind column method, working with references...
2527
2528         my $sth = $self->dbh_prepare($query);
2529         $sth->execute;
2530
2531         my ($path,$name,$fileindex,$jobid);
2532         $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2533         
2534         # The temp place we're going to save all file
2535         # list to before the real list
2536         my @temp_list;
2537
2538         RECORD_LOOP:
2539         while ($sth->fetchrow_arrayref())
2540         {
2541                 # This may look dumb, but we're going to do a join by ourselves,
2542                 # to save memory and avoid sending a complex query to mysql
2543                 my $complete_path = $path . $name;
2544                 my $is_dir = 0;
2545                 
2546                 if ( $name eq '')
2547                 {
2548                         $is_dir = 1;
2549                 }
2550                 
2551                 # Remove trailing slash (normalize file and dir name)
2552                 $complete_path =~ s/\/$//;
2553                 
2554                 # Let's find the ref(s) for the %mediainfo element(s) 
2555                 # containing the data for this file
2556                 # There can be several matches. It is the pseudo join.
2557                 my $med_idx=0;
2558                 my $max_elt=@{$mediainfos{$jobid}}-1;
2559                 MEDIA_LOOP:
2560                 while($med_idx <= $max_elt)
2561                 {
2562                         my $ref = $mediainfos{$jobid}->[$med_idx];
2563                         # First, can we get rid of the first elements of the
2564                         # array ? (if they don't contain valuable records
2565                         # anymore
2566                         if ($fileindex > $ref->[5])
2567                         {
2568                                 # It seems we don't need anymore
2569                                 # this entry in %mediainfo (the input data
2570                                 # is sorted...)
2571                                 # We get rid of it.
2572                                 shift @{$mediainfos{$jobid}};
2573                                 $max_elt--;
2574                                 next MEDIA_LOOP;
2575                         }
2576                         # We will do work on this elt. We can ++
2577                         # $med_idx for next loop
2578                         $med_idx++;
2579
2580                         # %mediainfo row looks like : 
2581                         # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2582                         # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2583                         # MediaType)
2584                         
2585                         # We are in range. We store and continue looping
2586                         # in the medias
2587                         if ($fileindex >= $ref->[4])
2588                         {
2589                                 my @data = ($complete_path,$is_dir,
2590                                             $fileindex,$ref);
2591                                 push @temp_list,(\@data);
2592                                 next MEDIA_LOOP;
2593                         }
2594                         
2595                         # We are not in range. No point in continuing looping
2596                         # We go to next record.
2597                         next RECORD_LOOP;
2598                 }
2599         }
2600         # Now we have the array.
2601         # We're going to sort it, by 
2602         # path, volsessiontime DESC (get the most recent file...)
2603         # The array rows look like this :
2604         # complete_path,is_dir,fileindex,
2605         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2606         #       LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2607         @temp_list = sort {$a->[0] cmp $b->[0]
2608                         || $b->[3]->[2] <=> $a->[3]->[2]
2609                           } @temp_list;
2610
2611         my @restore_list;
2612         my $prev_complete_path='////'; # Sure not to match
2613         my $prev_is_file=1;
2614         my $prev_jobid;
2615
2616         while (my $refrow = shift @temp_list)
2617         {
2618                 # For the sake of readability, we load $refrow 
2619                 # contents in real scalars
2620                 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2621                 my $jobid= $refother->[0]; # We don't need the rest...
2622
2623                 # We skip this entry.
2624                 # We allready have a newer one and this 
2625                 # isn't a continuation of the same file
2626                 next if ($complete_path eq $prev_complete_path 
2627                          and $jobid != $prev_jobid);
2628                 
2629                 
2630                 if ($prev_is_file 
2631                     and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2632                 {
2633                         # We would be recursing inside a file.
2634                         # Just what we don't want (dir replaced by file
2635                         # between two backups
2636                         next;
2637                 }
2638                 elsif ($is_dir)
2639                 {
2640                         # It is a directory
2641                         push @restore_list,($refrow);
2642                         
2643                         $prev_complete_path = $complete_path;
2644                         $prev_jobid = $jobid;
2645                         $prev_is_file = 0;
2646                 }
2647                 else
2648                 {
2649                         # It is a file
2650                         push @restore_list,($refrow);
2651                         
2652                         $prev_complete_path = $complete_path;
2653                         $prev_jobid = $jobid;
2654                         $prev_is_file = 1;
2655                 }
2656         }
2657         # We get rid of @temp_list... save memory
2658         @temp_list=();
2659
2660         # Ok everything is in the list. Let's sort it again in another way.
2661         # This time it will be in the bsr file order
2662
2663         # we sort the results by 
2664         # volsessiontime, volsessionid, volindex, fileindex 
2665         # to get all files in right order...
2666         # Reminder : The array rows look like this :
2667         # complete_path,is_dir,fileindex,
2668         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2669         #       StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2670
2671         @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2] 
2672                            || $a->[3]->[1] <=> $b->[3]->[1] 
2673                            || $a->[3]->[7] <=> $b->[3]->[7] 
2674                            || $a->[2] <=> $b->[2] } 
2675                                 @restore_list;
2676
2677         # Now that everything is ready, we create the bsr
2678         my $prev_fileindex=-1;
2679         my $prev_volsessionid=-1;
2680         my $prev_volsessiontime=-1;
2681         my $prev_volumename=-1;
2682         my $prev_volfile=-1;
2683         my $prev_mediatype;
2684         my $prev_volblocks;
2685         my $count=0;
2686         my $first_of_current_range=0;
2687         my @fileindex_ranges;
2688         my $bsr='';
2689
2690         foreach my $refrow (@restore_list)
2691         {
2692                 my (undef,undef,$fileindex,$refother)=@{$refrow};
2693                 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2694                     $volblocks,undef,$volumename,$mediatype)=@{$refother};
2695                 
2696                 # We can specifiy the number of files in each section of the
2697                 # bsr to speedup restore (bacula can then jump over the
2698                 # end of tape files.
2699                 $count++;
2700                 
2701                 
2702                 if ($prev_volumename eq '-1')
2703                 {
2704                         # We only have to start the new range...
2705                         $first_of_current_range=$fileindex;
2706                 }
2707                 elsif ($prev_volsessionid != $volsessionid 
2708                        or $prev_volsessiontime != $volsessiontime 
2709                        or $prev_volumename ne $volumename 
2710                        or $prev_volfile ne $volfile)
2711                 {
2712                         # We have to create a new section in the bsr...
2713                         # We print the previous one ... 
2714                         # (before that, save the current range ...)
2715                         if ($first_of_current_range != $prev_fileindex)
2716                         {
2717                                 # we are in a range
2718                                 push @fileindex_ranges,
2719                                     ("$first_of_current_range-$prev_fileindex");
2720                         }
2721                         else
2722                         {
2723                                  # We are out of a range,
2724                                  # but there is only one element in the range
2725                                 push @fileindex_ranges,
2726                                     ("$first_of_current_range");
2727                         }
2728                         
2729                         $bsr.=print_bsr_section(\@fileindex_ranges,
2730                                                 $prev_volsessionid,
2731                                                 $prev_volsessiontime,
2732                                                 $prev_volumename,
2733                                                 $prev_volfile,
2734                                                 $prev_mediatype,
2735                                                 $prev_volblocks,
2736                                                 $count-1);
2737                         $count=1;
2738                         # Reset for next loop
2739                         @fileindex_ranges=();
2740                         $first_of_current_range=$fileindex;
2741                 }
2742                 elsif ($fileindex-1 != $prev_fileindex)
2743                 {
2744                         # End of a range of fileindexes
2745                         if ($first_of_current_range != $prev_fileindex)
2746                         {
2747                                 #we are in a range
2748                                 push @fileindex_ranges,
2749                                     ("$first_of_current_range-$prev_fileindex");
2750                         }
2751                         else
2752                         {
2753                                  # We are out of a range,
2754                                  # but there is only one element in the range
2755                                 push @fileindex_ranges,
2756                                     ("$first_of_current_range");
2757                         }
2758                         $first_of_current_range=$fileindex;
2759                 }
2760                 $prev_fileindex=$fileindex;
2761                 $prev_volsessionid = $volsessionid;
2762                 $prev_volsessiontime = $volsessiontime;
2763                 $prev_volumename = $volumename;
2764                 $prev_volfile=$volfile;
2765                 $prev_mediatype=$mediatype;
2766                 $prev_volblocks=$volblocks;
2767
2768         }
2769
2770         # Ok, we're out of the loop. Alas, there's still the last record ...
2771         if ($first_of_current_range != $prev_fileindex)
2772         {
2773                 # we are in a range
2774                 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2775                 
2776         }
2777         else
2778         {
2779                 # We are out of a range,
2780                 # but there is only one element in the range
2781                 push @fileindex_ranges,("$first_of_current_range");
2782                 
2783         }
2784         $bsr.=print_bsr_section(\@fileindex_ranges,
2785                                 $prev_volsessionid,
2786                                 $prev_volsessiontime,
2787                                 $prev_volumename,
2788                                 $prev_volfile,
2789                                 $prev_mediatype,
2790                                 $prev_volblocks,
2791                                 $count);
2792         
2793         return $bsr;
2794 }
2795
2796 sub print_bsr_section
2797 {
2798     my ($ref_fileindex_ranges,$volsessionid,
2799         $volsessiontime,$volumename,$volfile,
2800         $mediatype,$volblocks,$count)=@_;
2801     
2802     my $bsr='';
2803     $bsr .= "Volume=\"$volumename\"\n";
2804     $bsr .= "MediaType=\"$mediatype\"\n";
2805     $bsr .= "VolSessionId=$volsessionid\n";
2806     $bsr .= "VolSessionTime=$volsessiontime\n";
2807     $bsr .= "VolFile=$volfile\n";
2808     $bsr .= "VolBlock=$volblocks\n";
2809     
2810     foreach my $range (@{$ref_fileindex_ranges})
2811     {
2812         $bsr .= "FileIndex=$range\n";
2813     }
2814     
2815     $bsr .= "Count=$count\n";
2816     return $bsr;
2817 }
2818
2819 # This function estimates the size to be restored for an entry of the restore
2820 # list
2821 # In : self,reference to the entry
2822 # Out : size in bytes, number of files
2823 sub estimate_restore_size
2824 {
2825     # reminder : restore_list looks like this : 
2826     # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2827     my $self=shift;
2828     my ($entry)=@_;
2829     my $query;
2830     if ($entry->[2] eq 'dir')
2831     {
2832         my $dir = unpack('u', $entry->[0]);
2833         my $inclause = $entry->[3]; #curjobids
2834         $query = 
2835 "SELECT Path.Path, File.FilenameId, File.LStat
2836   FROM File, Path, Job
2837   WHERE Path.PathId = File.PathId
2838   AND File.JobId = Job.JobId
2839   AND Path.Path LIKE '$dir%'
2840   AND File.JobId IN ($inclause)
2841   ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2842     }
2843     else
2844     {
2845         # It's a file. Great, we allready have most 
2846         # of what is needed. Simple and efficient query
2847         my $file = unpack('u', $entry->[0]);
2848         my @file = split '/',$file;
2849         $file = pop @file;
2850         my $dir = join('/',@file);
2851         
2852         my $jobid = $entry->[1];
2853         my $fileindex = $entry->[7];
2854         my $inclause = $entry->[3]; # curjobids
2855         $query = 
2856 "SELECT Path.Path, File.FilenameId, File.Lstat
2857   FROM File, Path, Filename
2858   WHERE Path.PathId = File.PathId
2859   AND Path.Path = '$dir/'
2860   AND Filename.Name = '$file'
2861   AND File.JobId = $jobid
2862   AND Filename.FilenameId = File.FilenameId";
2863     }
2864
2865     print STDERR $query,"\n" if $debug;
2866     my ($path,$nameid,$lstat);
2867     my $sth = $self->dbh_prepare($query);
2868     $sth->execute;
2869     $sth->bind_columns(\$path,\$nameid,\$lstat);
2870     my $old_path='';
2871     my $old_nameid='';
2872     my $total_size=0;
2873     my $total_files=0;
2874
2875     refresh_screen();
2876
2877     my $rcount=0;
2878     # We fetch all rows
2879     while ($sth->fetchrow_arrayref())
2880     {
2881         # Only the latest version of a file
2882         next if ($nameid eq $old_nameid and $path eq $old_path);
2883
2884         if ($rcount > 15000) {
2885             refresh_screen();
2886             $rcount=0;
2887         } else {
2888             $rcount++;
2889         }
2890
2891         # We get the size of this file
2892         my $size=lstat_attrib($lstat,'st_size');
2893         $total_size += $size;
2894         $total_files++;
2895         $old_path=$path;
2896         $old_nameid=$nameid;
2897     }
2898     return ($total_size,$total_files);
2899 }
2900
2901 sub update_brestore_table
2902 {
2903     my ($self, @jobs) = @_;
2904     my $dbh = $self->{dbh};
2905
2906     foreach my $job (sort {$a <=> $b} @jobs)
2907     {
2908         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
2909         my $retour = $self->dbh_selectrow_arrayref($query);
2910         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
2911
2912         print STDERR "Inserting path records for JobId $job\n";
2913         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId) 
2914                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
2915
2916         $self->dbh_do($query);
2917
2918         # Now we have to do the directory recursion stuff to determine missing visibility
2919         # We try to avoid recursion, to be as fast as possible
2920         # We also only work on not allready hierarchised directories...
2921
2922         print STDERR "Creating missing recursion paths for $job\n";
2923
2924         $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility 
2925                   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
2926                   LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
2927                   WHERE brestore_pathvisibility.JobId = $job
2928                   AND brestore_pathhierarchy.PathId IS NULL
2929                   ORDER BY Path";
2930
2931         my $sth = $self->dbh_prepare($query);
2932         $sth->execute();
2933         my $pathid; my $path;
2934         $sth->bind_columns(\$pathid,\$path);
2935         
2936         while ($sth->fetch)
2937         {
2938             $self->build_path_hierarchy($path,$pathid);
2939         }
2940         $sth->finish();
2941
2942         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
2943         # This query gives all parent pathids for a given jobid that aren't stored.
2944         # It has to be called until no record is updated ...
2945         $query = "
2946         INSERT INTO brestore_pathvisibility (PathId, JobId) (
2947         SELECT a.PathId,$job
2948         FROM
2949                 (SELECT DISTINCT h.PPathId AS PathId
2950                 FROM brestore_pathhierarchy AS h
2951                 JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
2952                 WHERE p.JobId=$job) AS a
2953                 LEFT JOIN
2954                 (SELECT PathId
2955                 FROM brestore_pathvisibility
2956                 WHERE JobId=$job) AS b
2957                 ON (a.PathId = b.PathId)
2958         WHERE b.PathId IS NULL)";
2959         print STDERR $query,"\n" if ($debug);
2960         my $rows_affected;
2961         while (($rows_affected = $dbh->do($query)) and ($rows_affected !~ /^0/))
2962         {
2963             print STDERR "Recursively adding $rows_affected records from $job\n";
2964         }
2965         # Job's done
2966         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
2967         $dbh->do($query);
2968     }
2969 }
2970
2971 sub cleanup_brestore_table
2972 {
2973     my ($self) = @_;
2974     my $dbh = $self->{dbh};
2975
2976     my $query = "SELECT JobId from brestore_knownjobid";
2977     my @jobs = @{$dbh->selectall_arrayref($query)};
2978
2979     foreach my $jobentry (@jobs)
2980     {
2981         my $job = $jobentry->[0];
2982         $query = "SELECT FileId from File WHERE JobId = $job LIMIT 1";
2983         my $result = $dbh->selectall_arrayref($query);
2984         if (scalar(@{$result}))
2985         {
2986             # There are still files for this jobid
2987             print STDERR "$job still exists. Not cleaning...\n";
2988
2989         } else {
2990                 $query = "DELETE FROM brestore_pathvisibility WHERE JobId = $job";
2991                 $dbh->do($query);
2992                 $query = "DELETE FROM brestore_knownjobid WHERE JobId = $job";
2993                 $dbh->do($query);
2994         }
2995     }
2996 }
2997
2998 sub build_path_hierarchy
2999 {
3000     my ($self, $path,$pathid)=@_;
3001     # Does the ppathid exist for this ? we use a memory cache...
3002     # In order to avoid the full loop, we consider that if a dir is allready in the
3003     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
3004     while ($path ne '')
3005     {
3006         #print STDERR "$path\n" if $debug;
3007         if (! $self->{cache_ppathid}->{$pathid})
3008         {
3009             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
3010             my $sth2 = $self->{dbh}->prepare_cached($query);
3011             $sth2->execute($pathid);
3012             # Do we have a result ?
3013             if (my $refrow = $sth2->fetchrow_arrayref)
3014             {
3015                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
3016                 $sth2->finish();
3017                 # This dir was in the db ...
3018                 # It means we can leave, the tree has allready been built for
3019                 # this dir
3020                 return 1;
3021             } else {
3022                 $sth2->finish();
3023                 # We have to create the record ...
3024                 # What's the current p_path ?
3025                 my $ppath = parent_dir($path);
3026                 my $ppathid = $self->return_pathid_from_path($ppath);
3027                 $self->{cache_ppathid}->{$pathid}= $ppathid;
3028                 
3029                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
3030                 $sth2 = $self->{dbh}->prepare_cached($query);
3031                 $sth2->execute($pathid,$ppathid);
3032                 $sth2->finish();
3033                 $path = $ppath;
3034                 $pathid = $ppathid;
3035             }
3036         } else {
3037            # It's allready in the cache.
3038            # We can leave, no time to waste here, all the parent dirs have allready
3039            # been done
3040            return 1;
3041         }
3042     }
3043     return 1;
3044 }
3045
3046 sub return_pathid_from_path
3047 {
3048     my ($self, $path) = @_;
3049     my $query = "SELECT PathId FROM Path WHERE Path = ?
3050                  UNION
3051                  SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3052     #print STDERR $query,"\n" if $debug;
3053     my $sth = $self->{dbh}->prepare_cached($query);
3054     $sth->execute($path,$path);
3055     my $result =$sth->fetchrow_arrayref();
3056     $sth->finish();
3057     if (defined $result)
3058     {
3059         return $result->[0];
3060
3061     } else {
3062         # A bit dirty : we insert into path AND missing_path, to be sure
3063         # we aren't deleted by a purge. We still need to insert into path to get
3064         # the pathid, because of mysql
3065         $query = "INSERT INTO Path (Path) VALUES (?)";
3066         #print STDERR $query,"\n" if $debug;
3067         $sth = $self->{dbh}->prepare_cached($query);
3068         $sth->execute($path);
3069         $sth->finish();
3070         
3071         $query = " INSERT INTO brestore_missing_path (PathId,Path)
3072                    SELECT PathId,Path FROM Path WHERE Path = ?";
3073         #print STDERR $query,"\n" if $debug;
3074         $sth = $self->{dbh}->prepare_cached($query);
3075         $sth->execute($path);
3076         $sth->finish();
3077         $query = " DELETE FROM Path WHERE Path = ?";
3078         #print STDERR $query,"\n" if $debug;
3079         $sth = $self->{dbh}->prepare_cached($query);
3080         $sth->execute($path);
3081         $sth->finish();
3082         $query = "SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3083         #print STDERR $query,"\n" if $debug;
3084         $sth = $self->{dbh}->prepare_cached($query);
3085         $sth->execute($path);
3086         $result = $sth->fetchrow_arrayref();
3087         $sth->finish();
3088         return $result->[0];
3089     }
3090 }
3091
3092 sub parent_dir
3093 {
3094     my ($path) = @_;
3095     # Root Unix case :
3096     if ($path eq '/')
3097     {
3098         return '';
3099     }
3100     # Root Windows case :
3101     if ($path =~ /^[a-z]+:\/$/i)
3102     {
3103         return '';
3104     }
3105     # Split
3106     my @tmp = split('/',$path);
3107     # We remove the last ...
3108     pop @tmp;
3109     my $tmp = join ('/',@tmp) . '/';
3110     return $tmp;
3111 }
3112
3113 # Get metadata
3114 {
3115     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
3116                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
3117                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
3118                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
3119                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
3120                           'data_stream' => 15);;
3121     sub array_attrib
3122     {
3123         my ($attrib,$ref_attrib)=@_;
3124         return $ref_attrib->[$attrib_name_id{$attrib}];
3125     }
3126         
3127     sub file_attrib
3128     {   # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
3129
3130         my ($file, $attrib)=@_;
3131         
3132         if (defined $attrib_name_id{$attrib}) {
3133
3134             my @d = split(' ', $file->[2]) ; # TODO : cache this
3135             
3136             return from_base64($d[$attrib_name_id{$attrib}]);
3137
3138         } elsif ($attrib eq 'jobid') {
3139
3140             return $file->[3];
3141
3142         } elsif ($attrib eq 'name') {
3143
3144             return $file->[1];
3145             
3146         } else  {
3147             die "Attribute not known : $attrib.\n";
3148         }
3149     }
3150
3151     # Return the jobid or attribute asked for a dir
3152     sub dir_attrib
3153     {
3154         my ($self,$dir,$attrib)=@_;
3155         
3156         my @dir = split('/',$dir,-1);
3157         my $refdir=$self->{dirtree}->{$self->current_client};
3158         
3159         if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
3160         {
3161             die "Attribute not known : $attrib.\n";
3162         }
3163         # Find the leaf
3164         foreach my $subdir (@dir)
3165         {
3166             $refdir = $refdir->[0]->{$subdir};
3167         }
3168         
3169         # $refdir is now the reference to the dir's array
3170         # Is the a jobid in @CurrentJobIds where the lstat is
3171         # defined (we'll search in reverse order)
3172         foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
3173         {
3174             if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
3175             {
3176                 if ($attrib eq 'jobid')
3177                 {
3178                     return $jobid;
3179                 }
3180                 else
3181                 {
3182                     my @attribs = parse_lstat($refdir->[1]->{$jobid});
3183                     return $attribs[$attrib_name_id{$attrib}+1];
3184                 }
3185             }
3186         }
3187
3188         return 0; # We cannot get a good attribute.
3189                   # This directory is here for the sake of visibility
3190     }
3191     
3192     sub lstat_attrib
3193     {
3194         my ($lstat,$attrib)=@_;
3195         if ($lstat and defined $attrib_name_id{$attrib}) 
3196         {
3197             my @d = split(' ', $lstat) ; # TODO : cache this
3198             return from_base64($d[$attrib_name_id{$attrib}]);
3199         }
3200         return 0;
3201     }
3202 }
3203
3204 {
3205     # Base 64 functions, directly from recover.pl.
3206     # Thanks to
3207     # Karl Hakimian <hakimian@aha.com>
3208     # This section is also under GPL v2 or later.
3209     my @base64_digits;
3210     my @base64_map;
3211     my $is_init=0;
3212     sub init_base64
3213     {
3214         @base64_digits = (
3215         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
3216         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
3217         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
3218         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
3219         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
3220                           );
3221         @base64_map = (0) x 128;
3222         
3223         for (my $i=0; $i<64; $i++) {
3224             $base64_map[ord($base64_digits[$i])] = $i;
3225         }
3226         $is_init = 1;
3227     }
3228
3229     sub from_base64 {
3230         if(not $is_init)
3231         {
3232             init_base64();
3233         }
3234         my $where = shift;
3235         my $val = 0;
3236         my $i = 0;
3237         my $neg = 0;
3238         
3239         if (substr($where, 0, 1) eq '-') {
3240             $neg = 1;
3241             $where = substr($where, 1);
3242         }
3243         
3244         while ($where ne '') {
3245             $val *= 64;
3246             my $d = substr($where, 0, 1);
3247             $val += $base64_map[ord(substr($where, 0, 1))];
3248             $where = substr($where, 1);
3249         }
3250         
3251         return $val;
3252     }
3253
3254     sub parse_lstat {
3255         my ($lstat)=@_;
3256         my @attribs = split(' ',$lstat);
3257         foreach my $element (@attribs)
3258         {
3259             $element = from_base64($element);
3260         }
3261         return @attribs;
3262     }
3263 }
3264
3265
3266 1;
3267
3268 ################################################################
3269
3270 package Batch;
3271 use base qw/DlgResto/;
3272
3273 sub new
3274 {
3275     my ($class, $conf) = @_;
3276     my $self = bless {info => $conf}, $class;
3277
3278     $self->{dbh} = $conf->{dbh};
3279
3280     return $self;
3281 }
3282
3283 sub update_cache
3284 {
3285     my ($self) = @_;
3286
3287     my $query = "SELECT JobId from Job WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) order by JobId";
3288     my $jobs = $self->dbh_selectall_arrayref($query);
3289
3290     $self->update_brestore_table(map { $_->[0] } @$jobs);
3291 }
3292
3293 1;
3294
3295 package main;
3296
3297 use Getopt::Long ;
3298
3299 sub HELP_MESSAGE
3300 {
3301     print STDERR "Usage: $0 [--conf=brestore.conf] [--batch] [--debug]\n";
3302     exit 1;
3303 }
3304
3305 my $file_conf = "$ENV{HOME}/.brestore.conf" ;
3306 my $batch_mod;
3307
3308 GetOptions("conf=s"   => \$file_conf,
3309            "batch"    => \$batch_mod,
3310            "debug"    => \$debug,
3311            "help"     => \&HELP_MESSAGE) ;
3312
3313 my $p = new Pref($file_conf);
3314
3315 if (! -f $file_conf) {
3316     $p->write_config();
3317 }
3318
3319 if ($batch_mod) {
3320     my $b = new Batch($p);
3321     if ($p->connect_db()) {
3322         $b->set_dbh($p->{dbh});
3323         $b->update_cache();
3324     }
3325     exit (0);
3326 }
3327
3328 $glade_file = $p->{glade_file};
3329
3330 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
3331     if (-f "$path/$glade_file") {
3332         $glade_file = "$path/$glade_file" ;
3333         last;
3334     }
3335 }
3336
3337 # gtk have lots of warning on stderr
3338 if ($^O eq 'MSWin32')
3339 {
3340     close(STDERR);
3341     open(STDERR, ">stderr.log");
3342 }
3343
3344 Gtk2->init();
3345
3346 if ( -f $glade_file) {
3347     my $w = new DlgResto($p);
3348
3349 } else {
3350     my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close', 
3351 "Can't find your brestore.glade (glade_file => '$glade_file')
3352 Please, edit your $file_conf to setup it." );
3353  
3354     $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3355     $widget->run;
3356     exit 1;
3357 }
3358
3359 Gtk2->main; # Start Gtk2 main loop      
3360
3361 # that's it!
3362
3363 exit 0;
3364
3365
3366 __END__
3367
3368 TODO : 
3369
3370
3371 # Code pour trier les colonnes    
3372     my $mod = $fileview->get_model();
3373     $mod->set_default_sort_func(sub {
3374             my ($model, $item1, $item2) = @_;
3375             my $a = $model->get($item1, 1);  # récupération de la valeur de la 2ème 
3376             my $b = $model->get($item2, 1);  # colonne (indice 1)
3377             return $a cmp $b;
3378         }
3379     );
3380     
3381     $fileview->set_headers_clickable(1);
3382     my $col = $fileview->get_column(1);    # la colonne NOM, colonne numéro 2
3383     $col->signal_connect('clicked', sub {
3384             my ($colonne, $model) = @_;
3385             $model->set_sort_column_id (1, 'ascending');
3386         },
3387         $mod
3388     );