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