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