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