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