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