]> git.sur5r.net Git - bacula/bacula/blob - gui/brestore/brestore.pl
920 messages traduits, 90 traductions approximatives, 2003 messages non-traduits.
[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                                               'Nb Files'    => 'text', #8
886                                               'Size'        => 'text', #9
887                                               'size_b'      => 'hidden', #10
888                                               );
889
890     my @restore_list_target_table = ({'target' => 'STRING',
891                                       'flags' => [], 
892                                       'info' => 40 });  
893
894     $restore_list->enable_model_drag_dest(['copy'],@restore_list_target_table);
895     $restore_list->get_selection->set_mode('multiple');
896     
897     $widget = $glade->get_widget('infoview');
898     my $infoview = $self->{fileinfo} = Gtk2::SimpleList->new_from_treeview(
899                    $widget,
900                    'h_name'        => 'hidden',
901                    'h_jobid'       => 'hidden',
902                    'h_type'        => 'hidden',
903
904                    'InChanger'     => 'pixbuf',
905                    'Volume'        => 'text',
906                    'JobId'         => 'text',
907                    'Size'          => 'text',
908                    'Date'          => 'text',
909                    'MD5'           => 'text');
910
911     init_drag_drop($infoview);
912
913     $pref->connect_db() ||  $self->{dlg_pref}->display($self);
914
915     if ($pref->{dbh}) {
916         $self->{dbh} = $pref->{dbh};
917         $self->init_server_backup_combobox();
918     }
919 }
920
921 # set status bar informations
922 sub set_status
923 {
924     my ($self, $string) = @_;
925     my $context = $self->{status}->get_context_id('Main');
926     $self->{status}->push($context, $string);
927 }
928
929 sub on_time_select_changed
930 {
931     my ($self) = @_;
932 }
933
934 sub get_active_time
935 {
936     my ($self) = @_;
937     my $c = $self->{glade}->get_widget('combo_time');
938     return $c->get_active_text;
939 }
940
941 # This sub returns all clients declared in DB
942 sub get_all_clients
943 {
944     my $dbh = shift;
945     my $query = "SELECT Name FROM Client ORDER BY Name";
946     print $query,"\n" if $debug;
947     my $result = $dbh->selectall_arrayref($query);
948     my @return_array;
949     foreach my $refrow (@$result)
950     {
951         push @return_array,($refrow->[0]);
952     }
953     return @return_array;
954 }
955
956 sub get_wanted_job_status
957 {
958     my ($ok_only) = @_;
959
960     if ($ok_only) {
961         return "'T'";
962     } else {
963         return "'T', 'A', 'E'";
964     }
965 }
966
967 # This sub gives a full list of the EndTimes for a ClientId
968 # ( [ 'Date', 'FileSet', 'Type', 'Status', 'JobId'], 
969 #   ['Date', 'FileSet', 'Type', 'Status', 'JobId']..)
970 sub get_all_endtimes_for_job
971 {
972     my ($dbh, $client, $ok_only)=@_;
973     my $status = get_wanted_job_status($ok_only);
974     my $query = "
975  SELECT Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus, Job.JobId
976   FROM Job,Client,FileSet
977   WHERE Job.ClientId=Client.ClientId
978   AND Client.Name = '$client'
979   AND Job.Type = 'B'
980   AND JobStatus IN ($status)
981   AND Job.FileSetId = FileSet.FileSetId
982   ORDER BY EndTime desc";
983     print $query,"\n" if $debug;
984     my $result = $dbh->selectall_arrayref($query);
985
986     return @$result;
987 }
988
989
990 # init infoview widget
991 sub clear_infoview
992 {
993     my $self = shift;
994     @{$self->{fileinfo}->{data}} = ();
995 }
996
997 # init restore_list
998 sub on_clear_clicked
999 {
1000     my $self = shift;
1001     @{$self->{restore_list}->{data}} = ();
1002 }
1003
1004 sub on_estimate_clicked
1005 {
1006     my ($self) = @_;
1007
1008     my $size_total=0;
1009     my $nb_total=0;
1010
1011     # TODO : If we get here, things could get lenghty ... draw a popup window .
1012     my $widget = Gtk2::MessageDialog->new($self->{mainwin}, 
1013                                           'destroy-with-parent', 
1014                                           'info', 'close', 
1015                                           'Computing size...');
1016     $widget->show;
1017     refresh_screen();
1018
1019     my $title = "Computing size...\n";
1020     my $txt="";
1021     foreach my $entry (@{$self->{restore_list}->{data}})
1022     {
1023         unless ($entry->[9]) {
1024             my ($size, $nb) = $self->estimate_restore_size($entry);
1025             $entry->[10] = $size;
1026             $entry->[9] = human($size);
1027             $entry->[8] = $nb;
1028         }
1029
1030         my $name = unpack('u', $entry->[0]);
1031
1032         $txt .= "\n<i>$name</i> : " . $entry->[8] . " file(s)/" . $entry->[9] ;
1033         $widget->set_markup($title . $txt);
1034         
1035         $size_total+=$entry->[10];
1036         $nb_total+=$entry->[8];
1037         refresh_screen();
1038     }
1039     
1040     $txt .= "\n\n<b>Total</b> : $nb_total file(s)/" . human($size_total);
1041     $widget->set_markup("Size estimation :\n" . $txt);
1042     $widget->signal_connect ("response", sub { my $w=shift; $w->destroy();});
1043
1044     return 0;
1045 }
1046
1047 sub on_gen_bsr_clicked
1048 {
1049     my ($self) = @_;
1050     
1051     my @options = ("Choose a bsr file", $self->{mainwin}, 'save', 
1052                    'gtk-save','ok', 'gtk-cancel', 'cancel');
1053
1054     
1055     my $w = new Gtk2::FileChooserDialog ( @options );
1056     my $ok = 0;
1057     my $save;
1058     while (!$ok) {
1059         my $a = $w->run();
1060         if ($a eq 'cancel') {
1061             $ok = 1;
1062         }
1063
1064         if ($a eq 'ok') {
1065             my $f = $w->get_filename();
1066             if (-f $f) {
1067                 my $dlg = Gtk2::MessageDialog->new($self->{mainwin}, 
1068                                                    'destroy-with-parent', 
1069                                                    'warning', 'ok-cancel', 'This file already exists, do you want to overwrite it ?');
1070                 if ($dlg->run() eq 'ok') {
1071                     $save = $f;
1072                 }
1073                 $dlg->destroy();
1074             } else {
1075                 $save = $f;
1076             }
1077             $ok = 1;
1078         }
1079     }
1080
1081     $w->destroy();
1082     
1083     if ($save) {
1084         if (open(FP, ">$save")) {
1085             my $bsr = $self->create_filelist();
1086             print FP $bsr;
1087             close(FP);
1088             $self->set_status("Dumping BSR to $save ok");
1089         } else {
1090             $self->set_status("Can't dump BSR to $save: $!");
1091         }
1092     }
1093 }
1094
1095 use File::Temp qw/tempfile/;
1096
1097 sub on_go_button_clicked 
1098 {
1099     my $self = shift;
1100     my $bsr = $self->create_filelist();
1101     my ($fh, $filename) = tempfile();
1102     $fh->print($bsr);
1103     close($fh);
1104     chmod(0644, $filename);
1105
1106     print "Dumping BSR info to $filename\n"
1107         if ($debug);
1108
1109     # we get Volume list
1110     my %a = map { $_ => 1 } ($bsr =~ /Volume="(.+)"/g);
1111     my $vol = [ keys %a ] ;     # need only one occurrence of each volume
1112
1113     new DlgLaunch(pref     => $self->{pref},
1114                   volumes  => $vol,
1115                   bsr_file => $filename,
1116                   );
1117
1118 }
1119
1120 our $client_list_empty = 'Clients list'; 
1121 our %type_markup = ('F' => '<b>$label F</b>',
1122                     'D' => '$label D',
1123                     'I' => '$label I',
1124                     'B' => '<b>$label B</b>',
1125
1126                     'A' => '<span foreground=\"red\">$label</span>',
1127                     'T' => '$label',
1128                     'E' => '<span foreground=\"red\">$label</span>',
1129                     );
1130
1131 sub on_list_client_changed 
1132 {
1133     my ($self, $widget) = @_;
1134     return 0 unless defined $self->{fileview};
1135     my $dbh = $self->{dbh};
1136
1137     $self->{list_backup}->clear();
1138
1139     if ($self->current_client eq $client_list_empty) {
1140         return 0 ;
1141     }
1142
1143     my @endtimes=get_all_endtimes_for_job($dbh, 
1144                                           $self->current_client,
1145                                           $self->{pref}->{use_ok_bkp_only});
1146     foreach my $endtime (@endtimes)
1147     {
1148         my $i = $self->{list_backup}->append();
1149
1150         my $label = $endtime->[1] . " (" . $endtime->[4] . ")";
1151         eval "\$label = \"$type_markup{$endtime->[2]}\""; # job type
1152         eval "\$label = \"$type_markup{$endtime->[3]}\""; # job status
1153
1154         $self->{list_backup}->set($i, 
1155                                   0, $endtime->[0],
1156                                   1, $label,
1157                                   );
1158     }
1159     $self->{restore_backup_combobox}->set_active(0);
1160
1161     $self->{CurrentJobIds} = [
1162                               set_job_ids_for_date($dbh,
1163                                                    $self->current_client,
1164                                                    $self->current_date,
1165                                                    $self->{pref}->{use_ok_bkp_only})
1166                               ];
1167
1168     $self->ch_dir('');
1169
1170 #     undef $self->{dirtree};
1171     $self->refresh_fileview();
1172     0;
1173 }
1174
1175 sub fill_server_list
1176 {
1177     my ($dbh, $combo, $list) = @_;
1178
1179     my @clients=get_all_clients($dbh);
1180
1181     $list->clear();
1182     
1183     my $i = $list->append();
1184     $list->set($i, 0, $client_list_empty);
1185     
1186     foreach my $client (@clients)
1187     {
1188         $i = $list->append();
1189         $list->set($i, 0, $client);
1190     }
1191     $combo->set_active(0);
1192 }
1193
1194 sub init_server_backup_combobox
1195 {
1196     my $self = shift ;
1197     fill_server_list($self->{dbh}, 
1198                      $self->{client_combobox},
1199                      $self->{list_client}) ;
1200 }
1201
1202 #----------------------------------------------------------------------
1203 #Refreshes the file-view Redraws everything. The dir data is cached, the file
1204 #data isn't.  There is additionnal complexity for dirs (visibility problems),
1205 #so the @CurrentJobIds is not sufficient.
1206 sub refresh_fileview 
1207 {
1208     my ($self) = @_;
1209     my $fileview = $self->{fileview};
1210     my $client_combobox = $self->{client_combobox};
1211     my $cwd = $self->{cwd};
1212
1213     @{$fileview->{data}} = ();
1214
1215     $self->clear_infoview();
1216     
1217     my $client_name = $self->current_client;
1218
1219     if (!$client_name or ($client_name eq $client_list_empty)) {
1220         $self->set_status("Client list empty");
1221         return;
1222     }
1223
1224     my @dirs     = $self->list_dirs($cwd,$client_name);
1225     # [ [listfiles.id, listfiles.Name, File.LStat, File.JobId]..]
1226     my $files    = $self->list_files($cwd); 
1227     print "CWD : $cwd\n" if ($debug);
1228     
1229     my $file_count = 0 ;
1230     my $total_bytes = 0;
1231     
1232     # Add directories to view
1233     foreach my $dir (@dirs) {
1234         my $time = localtime($self->dir_attrib("$cwd/$dir",'st_mtime'));
1235         $total_bytes += 4096;
1236         $file_count++;
1237
1238         listview_push($fileview,
1239                       $dir,
1240                       $self->dir_attrib("$cwd/$dir",'jobid'),
1241                       'dir',
1242
1243                       $diricon, 
1244                       $dir, 
1245                       "4 Kb", 
1246                       $time);
1247     }
1248     
1249     # Add files to view 
1250     foreach my $file (@$files) 
1251     {
1252         my $size = file_attrib($file,'st_size');
1253         my $time = localtime(file_attrib($file,'st_mtime'));
1254         $total_bytes += $size;
1255         $file_count++;
1256         # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
1257
1258         listview_push($fileview,
1259                       $file->[1],
1260                       $file->[3],
1261                       'file',
1262                       
1263                       $fileicon, 
1264                       $file->[1], 
1265                       human($size), $time);
1266     }
1267     
1268     $self->set_status("$file_count files/" . human($total_bytes));
1269
1270     # set a decent default selection (makes keyboard nav easy)
1271     $fileview->select(0);
1272 }
1273
1274
1275 sub on_about_activate
1276 {
1277     DlgAbout::display();
1278 }
1279
1280 sub drag_set_info
1281 {
1282     my ($tree, $path, $data) = @_;
1283
1284     my @items = listview_get_all($tree) ;
1285     my @ret;
1286     foreach my $i (@items)
1287     {
1288         my @file_info = @{$i};
1289
1290         # doc ligne 93
1291         # Ok, we have a corner case :
1292         # path can be empty
1293         my $file;
1294         if ($path eq '')
1295         {
1296             $file = pack("u", $file_info[0]);
1297         }
1298         else
1299         {
1300                 $file = pack("u", $path . '/' . $file_info[0]);
1301         }
1302         push @ret, join(" ; ", $file, 
1303                         $file_info[1], # $jobid
1304                         $file_info[2], # $type
1305                         );
1306     }
1307
1308     my $data_get = join(" :: ", @ret);
1309     
1310     $data->set_text($data_get,-1);
1311 }
1312
1313 sub fileview_data_get
1314 {
1315     my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1316     drag_set_info($widget, $self->{cwd}, $data);
1317 }
1318
1319 sub fileinfo_data_get
1320 {
1321     my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1322     drag_set_info($widget, $self->{cwd}, $data);
1323 }
1324
1325 sub restore_list_data_received
1326 {
1327     my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_;
1328     my @ret;
1329
1330     if  ($info eq 40 || $info eq 0) # patch for display!=:0
1331     {
1332         foreach my $elt (split(/ :: /, $data->data()))
1333         {
1334             
1335             my ($file, $jobid, $type) = 
1336                 split(/ ; /, $elt);
1337             $file = unpack("u", $file);
1338             
1339             $self->add_selected_file_to_list($file, $jobid, $type);
1340         }
1341     }
1342 }
1343
1344 sub on_back_button_clicked {
1345     my $self = shift;
1346     $self->up_dir();
1347 }
1348 sub on_location_go_button_clicked 
1349 {
1350     my $self = shift; 
1351     $self->ch_dir($self->{location}->get_text());
1352 }
1353 sub on_quit_activate {Gtk2->main_quit;}
1354 sub on_preferences_activate
1355 {
1356     my $self = shift; 
1357     $self->{dlg_pref}->display($self) ;
1358 }
1359 sub on_main_delete_event {Gtk2->main_quit;}
1360 sub on_bweb_activate
1361 {
1362     my $self = shift; 
1363     $self->set_status("Open bweb on your browser");
1364     $self->{pref}->go_bweb('', "go on bweb");
1365 }
1366
1367 # Change to parent directory
1368 sub up_dir
1369 {
1370     my $self = shift ;
1371     if ($self->{cwd} eq '/')
1372     {
1373         $self->ch_dir('');
1374     }
1375     my @dirs = File::Spec->splitdir ($self->{cwd});
1376     pop @dirs;
1377     $self->ch_dir(File::Spec->catdir(@dirs));
1378 }
1379
1380 # Change the current working directory
1381 #   * Updates fileview, location, and selection
1382 #
1383 sub ch_dir 
1384 {
1385     my $self = shift;
1386     $self->{cwd} = shift;
1387     
1388     $self->refresh_fileview();
1389     $self->{location}->set_text($self->{cwd});
1390     
1391     1;
1392 }
1393
1394 # Handle dialog 'close' (window-decoration induced close)
1395 #   * Just hide the dialog, and tell Gtk not to do anything else
1396 #
1397 sub on_delete_event 
1398 {
1399     my ($self, $w) = @_;
1400     $w->hide; 
1401     Gtk2::main_quit();
1402     1; # consume this event!
1403 }
1404
1405 # Handle key presses in location text edit control
1406 #   * Translate a Return/Enter key into a 'Go' command
1407 #   * All other key presses left for GTK
1408 #
1409 sub on_location_entry_key_release_event 
1410 {
1411     my $self = shift;
1412     my $widget = shift;
1413     my $event = shift;
1414     
1415     my $keypress = $event->keyval;
1416     if ($keypress == $Gtk2::Gdk::Keysyms{KP_Enter} ||
1417         $keypress == $Gtk2::Gdk::Keysyms{Return}) 
1418     {
1419         $self->ch_dir($widget->get_text());
1420         
1421         return 1; # consume keypress
1422     }
1423
1424     return 0; # let gtk have the keypress
1425 }
1426
1427 sub on_fileview_key_press_event
1428 {
1429     my ($self, $widget, $event) = @_;
1430     return 0;
1431 }
1432
1433 sub listview_get_first
1434 {
1435     my ($list) = shift; 
1436     my @selected = $list->get_selected_indices();
1437     if (@selected > 0) {
1438         my ($name, @other) = @{$list->{data}->[$selected[0]]};
1439         return (unpack('u', $name), @other);
1440     } else {
1441         return undef;
1442     }
1443 }
1444
1445 sub listview_get_all
1446 {
1447     my ($list) = shift; 
1448
1449     my @selected = $list->get_selected_indices();
1450     my @ret;
1451     for my $i (@selected) {
1452         my ($name, @other) = @{$list->{data}->[$i]};
1453         push @ret, [unpack('u', $name), @other];
1454     } 
1455     return @ret;
1456 }
1457
1458
1459 sub listview_push
1460 {
1461     my ($list, $name, @other) = @_;
1462     push @{$list->{data}}, [pack('u', $name), @other];
1463 }
1464
1465 #----------------------------------------------------------------------
1466 # Handle keypress in file-view
1467 #   * Translates backspace into a 'cd ..' command 
1468 #   * All other key presses left for GTK
1469 #
1470 sub on_fileview_key_release_event 
1471 {
1472     my ($self, $widget, $event) = @_;
1473     if (not $event->keyval)
1474     {
1475         return 0;
1476     }
1477     if ($event->keyval == $Gtk2::Gdk::Keysyms{BackSpace}) {
1478         $self->up_dir();
1479         return 1; # eat keypress
1480     }
1481
1482     return 0; # let gtk have keypress
1483 }
1484
1485 sub on_forward_keypress
1486 {
1487     return 0;
1488 }
1489
1490 #----------------------------------------------------------------------
1491 # Handle double-click (or enter) on file-view
1492 #   * Translates into a 'cd <dir>' command
1493 #
1494 sub on_fileview_row_activated 
1495 {
1496     my ($self, $widget) = @_;
1497     
1498     my ($name, undef, $type, undef) = listview_get_first($widget);
1499
1500     if ($type eq 'dir')
1501     {
1502         if ($self->{cwd} eq '')
1503         {
1504                 $self->ch_dir($name);
1505         }
1506         elsif ($self->{cwd} eq '/')
1507         {
1508                 $self->ch_dir('/' . $name);
1509         }
1510         else
1511         {
1512                 $self->ch_dir($self->{cwd} . '/' . $name);
1513         }
1514
1515     } else {
1516         $self->fill_infoview($self->{cwd}, $name);
1517     }
1518     
1519     return 1; # consume event
1520 }
1521
1522 sub fill_infoview
1523 {
1524     my ($self, $path, $file) = @_;
1525     $self->clear_infoview();
1526     my @v = get_all_file_versions($self->{dbh}, 
1527                                   "$path/", 
1528                                   $file,
1529                                   $self->current_client,
1530                                   $self->{pref}->{see_all_versions});
1531     for my $ver (@v) {
1532         my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
1533             = @{$ver};
1534         my $icon = ($inchanger)?$yesicon:$noicon;
1535
1536         $mtime = localtime($mtime) ;
1537
1538         listview_push($self->{fileinfo},
1539                       $file, $jobid, 'file', 
1540                       $icon, $volname, $jobid, human($size), $mtime, $md5);
1541     }
1542 }
1543
1544 sub current_date
1545 {
1546     my $self = shift ;
1547     return $self->{restore_backup_combobox}->get_active_text;
1548 }
1549
1550 sub current_client
1551 {
1552     my $self = shift ;
1553     return $self->{client_combobox}->get_active_text;
1554 }
1555
1556 sub on_list_backups_changed 
1557 {
1558     my ($self, $widget) = @_;
1559     return 0 unless defined $self->{fileview};
1560
1561     $self->{CurrentJobIds} = [
1562                               set_job_ids_for_date($self->{dbh},
1563                                                    $self->current_client,
1564                                                    $self->current_date,
1565                                                    $self->{pref}->{use_ok_bkp_only})
1566                               ];
1567
1568     $self->refresh_fileview();
1569     0;
1570 }
1571
1572 sub on_restore_list_keypress
1573 {
1574     my ($self, $widget, $event) = @_;
1575     if ($event->keyval == $Gtk2::Gdk::Keysyms{Delete})
1576     {
1577         my @sel = $widget->get_selected_indices;
1578         foreach my $elt (reverse(sort {$a <=> $b} @sel))
1579         {
1580             splice @{$self->{restore_list}->{data}},$elt,1;
1581         }
1582     }
1583 }
1584
1585 sub on_fileview_button_press_event
1586 {
1587     my ($self,$widget,$event) = @_;
1588     if ($event->button == 3)
1589     {
1590         $self->on_right_click_filelist($widget,$event);
1591         return 1;
1592     }
1593     
1594     if ($event->button == 2)
1595     {
1596         $self->on_see_all_version();
1597         return 1;
1598     }
1599
1600     return 0;
1601 }
1602
1603 sub on_see_all_version
1604 {
1605     my ($self) = @_;
1606     
1607     my @lst = listview_get_all($self->{fileview});
1608
1609     for my $i (@lst) {
1610         my ($name, undef) = @{$i};
1611
1612         new DlgFileVersion($self->{dbh}, 
1613                            $self->current_client, 
1614                            $self->{cwd}, $name);
1615     }
1616 }
1617
1618 sub on_right_click_filelist
1619 {
1620     my ($self,$widget,$event) = @_;
1621     # I need to know what's selected
1622     my @sel = listview_get_all($self->{fileview});
1623     
1624     my $type = '';
1625
1626     if (@sel == 1) {
1627         $type = $sel[0]->[2];   # $type
1628     }
1629
1630     my $w;
1631
1632     if (@sel >=2 or $type eq 'dir')
1633     {
1634         # We have selected more than one or it is a directories
1635         $w = $self->{filelist_dir_menu};
1636     }
1637     else
1638     {
1639         $w = $self->{filelist_file_menu};
1640     }
1641     $w->popup(undef,
1642               undef,
1643               undef,
1644               undef,
1645               $event->button, $event->time);
1646 }
1647
1648 sub context_add_to_filelist
1649 {
1650     my ($self) = @_;
1651
1652     my @sel = listview_get_all($self->{fileview});
1653
1654     foreach my $i (@sel)
1655     {
1656         my ($file, $jobid, $type, undef) = @{$i};
1657         $file = $self->{cwd} . '/' . $file;
1658         $self->add_selected_file_to_list($file, $jobid, $type);
1659     }
1660 }
1661
1662 # Adds a file to the filelist
1663 sub add_selected_file_to_list
1664 {
1665     my ($self, $name, $jobid, $type)=@_;
1666
1667     my $dbh = $self->{dbh};
1668     my $restore_list = $self->{restore_list};
1669
1670     my $curjobids=join(',', @{$self->{CurrentJobIds}});
1671
1672     if ($type eq 'dir')
1673     {
1674         # dirty hack
1675         $name =~ s!^//+!/!;
1676
1677         if ($name and substr $name,-1 ne '/')
1678         {
1679                 $name .= '/'; # For bacula
1680         }
1681         my $dirfileindex = get_fileindex_from_dir_jobid($dbh,$name,$jobid);
1682         listview_push($restore_list, 
1683                       $name, $jobid, 'dir', $curjobids,
1684                       $diricon, $name,$curjobids,$dirfileindex);
1685     }
1686     elsif ($type eq 'file')
1687     {
1688         my $fileindex = get_fileindex_from_file_jobid($dbh,$name,$jobid);
1689
1690         listview_push($restore_list,
1691                       $name, $jobid, 'file', $curjobids,
1692                       $fileicon, $name, $jobid, $fileindex );
1693     }
1694 }
1695
1696 # TODO : we want be able to restore files from a bad ended backup
1697 # we have JobStatus IN ('T', 'A', 'E') and we must 
1698
1699 # Data acces subs from here. Interaction with SGBD and caching
1700
1701 # This sub retrieves the list of jobs corresponding to the jobs selected in the
1702 # GUI and stores them in @CurrentJobIds
1703 sub set_job_ids_for_date
1704 {
1705     my ($dbh, $client, $date, $only_ok)=@_;
1706
1707     if (!$client or !$date) {
1708         return ();
1709     }
1710     
1711     my $status = get_wanted_job_status($only_ok);
1712         
1713     # The algorithm : for a client, we get all the backups for each
1714     # fileset, in reverse order Then, for each fileset, we store the 'good'
1715     # incrementals and differentials until we have found a full so it goes
1716     # like this : store all incrementals until we have found a differential
1717     # or a full, then find the full #
1718
1719     my $query = "SELECT JobId, FileSet, Level, JobStatus
1720                 FROM Job, Client, FileSet
1721                 WHERE Job.ClientId = Client.ClientId
1722                 AND FileSet.FileSetId = Job.FileSetId
1723                 AND EndTime <= '$date'
1724                 AND Client.Name = '$client'
1725                 AND Type IN ('B')
1726                 AND JobStatus IN ($status)
1727                 ORDER BY FileSet, JobTDate DESC";
1728         
1729     print $query,"\n" if $debug;
1730     my @CurrentJobIds;
1731     my $result = $dbh->selectall_arrayref($query);
1732     my %progress;
1733     foreach my $refrow (@$result)
1734     {
1735         my $jobid = $refrow->[0];
1736         my $fileset = $refrow->[1];
1737         my $level = $refrow->[2];
1738                 
1739         defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
1740                 
1741         next if $progress{$fileset} eq 'F'; # It's over for this fileset...
1742                 
1743         if ($level eq 'I')
1744         {
1745             next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
1746             push @CurrentJobIds,($jobid);
1747         }
1748         elsif ($level eq 'D')
1749         {
1750             next if $progress{$fileset} eq 'D'; # We allready have a differential
1751             push @CurrentJobIds,($jobid);
1752         }
1753         elsif ($level eq 'F')
1754         {
1755             push @CurrentJobIds,($jobid);
1756         }
1757
1758         my $status = $refrow->[3] ;
1759         if ($status eq 'T') {              # good end of job
1760             $progress{$fileset} = $level;
1761         }
1762     }
1763     print Data::Dumper::Dumper(\@CurrentJobIds) if $debug;
1764
1765     return @CurrentJobIds;
1766 }
1767
1768 # Lists all directories contained inside a directory.
1769 # Uses the current dir, the client name, and CurrentJobIds for visibility.
1770 # Returns an array of dirs
1771 sub list_dirs
1772 {
1773     my ($self,$dir,$client)=@_;
1774     print "list_dirs($dir, $client)\n";
1775
1776     # Is data allready cached ?
1777     if (not $self->{dirtree}->{$client})
1778     {
1779         $self->cache_dirs($client);
1780     }
1781
1782     if ($dir ne '' and substr $dir,-1 ne '/')
1783     {
1784         $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1785     }
1786     # Here, the tree is cached in ram
1787     my @dir = split('/',$dir,-1);
1788     pop @dir; # We don't need the empty trailing element
1789     
1790     # We have to get the reference of the hash containing $dir contents
1791     # Get to the root
1792     my $refdir=$self->{dirtree}->{$client};
1793
1794     # Find the leaf
1795     foreach my $subdir (@dir)
1796     {
1797         if ($subdir eq '')
1798         {
1799                 $subdir = '/';
1800         }
1801         $refdir = $refdir->[0]->{$subdir};
1802     }
1803     
1804     # We reached the directory
1805     my @return_list;
1806   DIRLOOP:
1807     foreach my $dir (sort(keys %{$refdir->[0]}))
1808     {
1809         # We return the directory's content : only visible directories
1810         foreach my $jobid (reverse(sort(@{$self->{CurrentJobIds}})))
1811         {
1812             if (defined $refdir->[0]->{$dir}->[1]->{$jobid})
1813             {
1814                 my $dirname = $refdir->[0]->{$dir}->[2]; # The real dirname...
1815                 push @return_list,($dirname);
1816                 next DIRLOOP; # No need to waste more CPU cycles...
1817             }
1818         }
1819     }
1820     print "LIST DIR : ", Data::Dumper::Dumper(\@return_list),"\n";
1821     return @return_list;
1822 }
1823
1824
1825 # List all files in a directory. dir as parameter, CurrentJobIds for visibility
1826 # Returns an array of dirs
1827 sub list_files
1828 {
1829     my ($self, $dir)=@_;
1830     my $dbh = $self->{dbh};
1831
1832     my $empty = [];
1833
1834     print "list_files($dir)\n";
1835
1836     if ($dir ne '' and substr $dir,-1 ne '/')
1837     {
1838         $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1839     }
1840
1841     my $query = "SELECT Path.PathId FROM Path WHERE Path.Path = '$dir'";
1842     print $query,"\n" if $debug;
1843     my @list_pathid=();
1844     my $result = $dbh->selectall_arrayref($query);
1845     foreach my $refrow (@$result)
1846     {
1847         push @list_pathid,($refrow->[0]);
1848     }
1849         
1850     if  (@list_pathid == 0)
1851     {
1852         print "No pathid found for $dir\n" if $debug;
1853         return $empty;
1854     }
1855         
1856     my $inlistpath = join (',', @list_pathid);
1857     my $inclause = join (',', @{$self->{CurrentJobIds}});
1858     if ($inclause eq '')
1859     {
1860         return $empty;
1861     }
1862         
1863     $query = 
1864 "SELECT listfiles.id, listfiles.Name, File.LStat, File.JobId
1865  FROM
1866         (SELECT Filename.Name, max(File.FileId) as id
1867          FROM File, Filename
1868          WHERE File.FilenameId = Filename.FilenameId
1869            AND Filename.Name != ''
1870            AND File.PathId IN ($inlistpath)
1871            AND File.JobId IN ($inclause)
1872          GROUP BY Filename.Name
1873          ORDER BY Filename.Name) AS listfiles,
1874 File
1875 WHERE File.FileId = listfiles.id";
1876         
1877     print $query,"\n" if $debug;
1878     $result = $dbh->selectall_arrayref($query);
1879         
1880     return $result;
1881 }
1882
1883 sub refresh_screen
1884 {
1885     Gtk2->main_iteration while (Gtk2->events_pending);
1886 }
1887
1888 # For the dirs, because of the db schema, it's inefficient to get the
1889 # directories contained inside other directories (regexp match or tossing
1890 # lots of records...). So we load all the tree and cache it.  The data is 
1891 # stored in a structure of this form :
1892 # Each directory is an array. 
1893 # - In this array, the first element is a ref to next dir (hash) 
1894 # - The second element is a hash containing all jobids pointing
1895 # on an array containing their lstat (or 1 if this jobid is there because 
1896 # of dependencies)
1897 # - The third is the filename itself (it could get mangled because of 
1898 # the hashing...) 
1899
1900 # So it looks like this :
1901 # $reftree->[   { 'dir1' => $refdir1
1902 #                 'dir2' => $refdir2
1903 #               ......
1904 #               },
1905 #               { 'jobid1' => 'lstat1',
1906 #                 'jobid2' => 'lstat2',
1907 #                 'jobid3' => 1            # This one is here for "visibility"
1908 #               },
1909 #               'dirname'
1910 #          ]
1911
1912 # Client as a parameter
1913 # Returns an array of dirs
1914 sub cache_dirs
1915 {
1916     my ($self, $client) = @_;
1917     print "cache_dirs()\n";
1918
1919     $self->{dirtree}->{$client} = [];   # reset cache
1920     my $dbh = $self->{dbh};
1921
1922     # TODO : If we get here, things could get lenghty ... draw a popup window .
1923     my $widget = Gtk2::MessageDialog->new($self->{mainwin}, 
1924                                           'destroy-with-parent', 
1925                                           'info', 'none', 
1926                                           'Populating cache');
1927     $widget->show;
1928         
1929     # We have to build the tree, as it's the first time it is asked...
1930     
1931     
1932     # First, we only need the jobids of the selected server.
1933     # It's not the same as @CurrentJobIds (we need ALL the jobs)
1934     # We get the JobIds first in order to have the best execution
1935     # plan possible for the big query, with an in clause.
1936     my $query;
1937     my $status = get_wanted_job_status($self->{pref}->{use_ok_bkp_only});
1938     $query = 
1939 "SELECT JobId 
1940  FROM Job,Client
1941  WHERE Job.ClientId = Client.ClientId
1942    AND Client.Name = '$client'
1943    AND Job.JobStatus IN ($status)
1944    AND Job.Type = 'B'";
1945         
1946     print $query,"\n" if $debug;
1947     my $result = $dbh->selectall_arrayref($query);
1948     refresh_screen();
1949
1950     my @jobids;
1951     foreach my $record (@{$result})
1952     {
1953         push @jobids,($record->[0]);
1954     }
1955     my $inclause = join(',',@jobids);
1956     if ($inclause eq '')
1957     {
1958         $widget->destroy();
1959         $self->set_status("No previous backup found for $client");
1960         return ();
1961     }
1962
1963 # Then, still to help dear mysql, we'll retrieve the PathId from empty Path (directory entries...)
1964    my @dirids;
1965     $query =
1966 "SELECT Filename.FilenameId FROM Filename WHERE Filename.Name=''";
1967
1968     print $query,"\n" if $debug;
1969     $result = $dbh->selectall_arrayref($query);
1970     refresh_screen();
1971
1972     foreach my $record (@{$result})
1973     {
1974         push @dirids,$record->[0];
1975     }
1976     my $dirinclause = join(',',@dirids);
1977
1978    # This query is a bit complicated : 
1979    # whe need to find all dir entries that should be displayed, even
1980    # if the directory itself has no entry in File table (it means a file
1981    # is explicitely chosen in the backup configuration)
1982    # Here's what I wanted to do :
1983 #     $query = 
1984 # "
1985 # SELECT T1.Path, T2.Lstat, T2.JobId
1986 # FROM (    SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
1987 #     WHERE File.PathId = Path.PathId
1988 # AND File.JobId IN ($inclause)) AS T1
1989 # LEFT JOIN 
1990 #     (    SELECT File.Lstat, File.JobId, File.PathId FROM File
1991 #         WHERE File.FilenameId IN ($dirinclause)
1992 #         AND File.JobId IN ($inclause)) AS T2
1993 # ON (T1.PathId = T2.PathId)
1994 # ";            
1995     # It works perfectly with postgresql, but mysql doesn't seem to be able
1996     # to do the hash join correcty, so the performance sucks.
1997     # So it will be done in 4 steps :
1998     # o create T1 and T2 as temp tables
1999     # o create an index on T2.PathId
2000     # o do the query
2001     # o remove the temp tables
2002     $query = "
2003 CREATE TEMPORARY TABLE T1 AS
2004 SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
2005 WHERE File.PathId = Path.PathId
2006   AND File.JobId IN ($inclause)
2007 ";
2008     print $query,"\n" if $debug;
2009     $dbh->do($query);
2010     refresh_screen();
2011
2012     $query = "
2013 CREATE TEMPORARY TABLE T2 AS
2014 SELECT File.Lstat, File.JobId, File.PathId FROM File
2015 WHERE File.FilenameId IN ($dirinclause)
2016   AND File.JobId IN ($inclause)
2017 ";
2018     print $query,"\n" if $debug;
2019     $dbh->do($query);
2020     refresh_screen();
2021
2022     $query = "
2023 CREATE INDEX tmp2 ON T2(PathId)
2024 ";
2025     print $query,"\n" if $debug;
2026     $dbh->do($query);
2027     refresh_screen();
2028     
2029     $query = "
2030 SELECT T1.Path, T2.Lstat, T2.JobId
2031 FROM T1 LEFT JOIN T2
2032 ON (T1.PathId = T2.PathId)
2033 ";
2034     print $query,"\n" if $debug;
2035     $result = $dbh->selectall_arrayref($query);
2036     refresh_screen();
2037     
2038     my $rcount=0;
2039     foreach my $record (@{$result})
2040     {
2041         if ($rcount > 15000) {
2042             refresh_screen();
2043             $rcount=0;
2044         } else {
2045             $rcount++;
2046         }
2047         # Dirty hack to force the string encoding on perl... we don't
2048         # want implicit conversions
2049         my $path = pack "U0C*", unpack "C*",$record->[0];
2050         
2051         my @path = split('/',$path,-1);
2052         pop @path; # we don't need the trailing empty element
2053         my $lstat = $record->[1];
2054         my $jobid = $record->[2];
2055         
2056         # We're going to store all the data on the cache tree.
2057         # We find the leaf, then store data there
2058         my $reftree=$self->{dirtree}->{$client};
2059         foreach my $dir(@path)
2060         {
2061             if ($dir eq '')
2062             {
2063                 $dir = '/';
2064             }
2065             if (not defined($reftree->[0]->{$dir}))
2066             {
2067                 my @tmparray;
2068                 $reftree->[0]->{$dir}=\@tmparray;
2069             }
2070             $reftree=$reftree->[0]->{$dir};
2071             $reftree->[2]=$dir;
2072         }
2073         # We can now add the metadata for this dir ...
2074         
2075 #         $result = $dbh->selectall_arrayref($query);
2076         if ($lstat)
2077         {
2078             # contains something
2079             $reftree->[1]->{$jobid}=$lstat;
2080         }
2081         else
2082         {
2083             # We have a very special case here...
2084             # lstat is not defined.
2085             # it means the directory is there because a file has been
2086             # backuped. so the dir has no entry in File table.
2087             # That's a rare case, so we can afford to determine it's
2088             # visibility with a query
2089             my $select_path=$record->[0];
2090             $select_path=$dbh->quote($select_path); # gotta be careful
2091             my $query = "
2092 SELECT File.JobId
2093 FROM File, Path
2094 WHERE File.PathId = Path.PathId
2095 AND Path.Path = $select_path
2096 ";
2097             print $query,"\n" if $debug;
2098             my $result2 = $dbh->selectall_arrayref($query);
2099             foreach my $record (@{$result2})
2100             {
2101                 my $jobid=$record->[0];
2102                 $reftree->[1]->{$jobid}=1;
2103             }
2104         }
2105         
2106     }
2107     $query = "
2108 DROP TABLE T1;
2109 ";
2110     print $query,"\n" if $debug;
2111     $dbh->do($query);
2112     $query = "
2113 DROP TABLE T2;
2114 ";
2115     print $query,"\n" if $debug;
2116     $dbh->do($query);
2117
2118
2119     list_visible($self->{dirtree}->{$client});
2120     $widget->destroy();
2121
2122 #      print Data::Dumper::Dumper($self->{dirtree});
2123 }
2124
2125 # Recursive function to calculate the visibility of each directory in the cache
2126 # tree Working with references to save time and memory
2127 # For each directory, we want to propagate it's visible jobids onto it's
2128 # parents directory.
2129 # A tree is visible if
2130 # - it's been in a backup pointed by the CurrentJobIds
2131 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2132 # In the second case, the directory is visible but has no metadata.
2133 # We symbolize this with lstat = 1 for this jobid in the cache.
2134
2135 # Input : reference directory
2136 # Output : visibility of this dir. Has to know visibility of all subdirs
2137 # to know it's visibility, hence the recursing.
2138 sub list_visible
2139 {
2140     my ($refdir)=@_;
2141         
2142     my %visibility;
2143     # Get the subdirs array references list
2144     my @list_ref_subdirs;
2145     while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2146     {
2147         push @list_ref_subdirs,($ref_subdir);
2148     }
2149
2150     # Now lets recurse over these subdirs and retrieve the reference of a hash
2151     # containing the jobs where they are visible
2152     foreach my $ref_subdir (@list_ref_subdirs)
2153     {
2154         my $ref_list_jobs = list_visible($ref_subdir);
2155         foreach my $jobid (keys %$ref_list_jobs)
2156         {
2157             $visibility{$jobid}=1;
2158         }
2159     }
2160
2161     # Ok. Now, we've got the list of those jobs.  We are going to update our
2162     # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2163     # the lstat for the known jobids. Put 1 in the new elements...  But first,
2164     # let's store the current jobids
2165     my @known_jobids;
2166     foreach my $jobid (keys %{$refdir->[1]})
2167     {
2168         push @known_jobids,($jobid);
2169     }
2170     
2171     # Add the new jobs
2172     foreach my $jobid (keys %visibility)
2173     {
2174         next if ($refdir->[1]->{$jobid});
2175         $refdir->[1]->{$jobid} = 1;
2176     }
2177     # Add the known_jobids to %visibility
2178     foreach my $jobid (@known_jobids)
2179     {
2180         $visibility{$jobid}=1;
2181     }
2182     return \%visibility;
2183 }
2184
2185 # Returns the list of media required for a list of jobids.
2186 # Input : dbh, jobid1, jobid2...
2187 # Output : reference to array of (joibd, inchanger)
2188 sub get_required_media_from_jobid
2189 {
2190     my ($dbh, @jobids)=@_;
2191     my $inclause = join(',',@jobids);
2192     my $query = "
2193 SELECT DISTINCT JobMedia.MediaId, Media.InChanger 
2194 FROM JobMedia, Media 
2195 WHERE JobMedia.MediaId=Media.MediaId 
2196 AND JobId In ($inclause)
2197 ORDER BY MediaId";
2198     my $result = $dbh->selectall_arrayref($query);
2199     return $result;
2200 }
2201
2202 # Returns the fileindex from dirname and jobid.
2203 # Input : dbh, dirname, jobid
2204 # Output : fileindex
2205 sub get_fileindex_from_dir_jobid
2206 {
2207     my ($dbh, $dirname, $jobid)=@_;
2208     my $query;
2209     $query = "SELECT File.FileIndex
2210                 FROM File, Filename, Path
2211                 WHERE File.FilenameId = Filename.FilenameId
2212                 AND File.PathId = Path.PathId
2213                 AND Filename.Name = ''
2214                 AND Path.Path = '$dirname'
2215                 AND File.JobId = '$jobid'
2216                 ";
2217                 
2218     print $query,"\n" if $debug;
2219     my $result = $dbh->selectall_arrayref($query);
2220     return $result->[0]->[0];
2221 }
2222
2223 # Returns the fileindex from filename and jobid.
2224 # Input : dbh, filename, jobid
2225 # Output : fileindex
2226 sub get_fileindex_from_file_jobid
2227 {
2228     my ($dbh, $filename, $jobid)=@_;
2229     
2230     my @dirs = File::Spec->splitdir ($filename);
2231     $filename=pop(@dirs);
2232     my $dirname = File::Spec->catdir(@dirs) . '/';
2233     
2234     
2235     my $query;
2236     $query = 
2237 "SELECT File.FileIndex
2238  FROM File, Filename, Path
2239  WHERE File.FilenameId = Filename.FilenameId
2240    AND File.PathId = Path.PathId
2241    AND Filename.Name = '$filename'
2242    AND Path.Path = '$dirname'
2243    AND File.JobId = '$jobid'";
2244                 
2245     print $query,"\n" if $debug;
2246     my $result = $dbh->selectall_arrayref($query);
2247     return $result->[0]->[0];
2248 }
2249
2250
2251 # Returns list of versions of a file that could be restored
2252 # returns an array of 
2253 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2254 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2255 # and volname and md5
2256 # and of course, there will be only one jobid in the array of jobids...
2257 sub get_all_file_versions
2258 {
2259     my ($dbh,$path,$file,$client,$see_all)=@_;
2260     
2261     defined $see_all or $see_all=0;
2262     
2263     my @versions;
2264     my $query;
2265     $query = 
2266 "SELECT File.JobId, File.FileIndex, File.Lstat, 
2267         File.Md5, Media.VolumeName, Media.InChanger
2268  FROM File, Filename, Path, Job, Client, JobMedia, Media
2269  WHERE File.FilenameId = Filename.FilenameId
2270    AND File.PathId=Path.PathId
2271    AND File.JobId = Job.JobId
2272    AND Job.ClientId = Client.ClientId
2273    AND Job.JobId = JobMedia.JobId
2274    AND File.FileIndex >= JobMedia.FirstIndex
2275    AND File.FileIndex <= JobMedia.LastIndex
2276    AND JobMedia.MediaId = Media.MediaId
2277    AND Path.Path = '$path'
2278    AND Filename.Name = '$file'
2279    AND Client.Name = '$client'";
2280         
2281     print $query if $debug;
2282         
2283     my $result = $dbh->selectall_arrayref($query);
2284         
2285     foreach my $refrow (@$result)
2286     {
2287         my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2288         my @attribs = parse_lstat($lstat);
2289         my $mtime = array_attrib('st_mtime',\@attribs);
2290         my $size = array_attrib('st_size',\@attribs);
2291                 
2292         my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2293                     $inchanger, $md5, $volname);
2294         push @versions, (\@list);
2295     }
2296         
2297     # We have the list of all versions of this file.
2298     # We'll sort it by mtime desc, size, md5, inchanger desc
2299     # the rest of the algorithm will be simpler
2300     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2301     @versions = sort { $b->[4] <=> $a->[4] 
2302                     || $a->[5] <=> $b->[5] 
2303                     || $a->[7] cmp $a->[7] 
2304                     || $b->[6] <=> $a->[6]} @versions;
2305         
2306     my @good_versions;
2307     my %allready_seen_by_mtime;
2308     my %allready_seen_by_md5;
2309     # Now we should create a new array with only the interesting records
2310     foreach my $ref (@versions)
2311     {   
2312         if ($ref->[7])
2313         {
2314             # The file has a md5. We compare his md5 to other known md5...
2315             # We take size into account. It may happen that 2 files
2316             # have the same md5sum and are different. size is a supplementary
2317             # criterion
2318             
2319             # If we allready have a (better) version
2320             next if ( (not $see_all) 
2321                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}); 
2322
2323             # we never met this one before...
2324             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2325         }
2326         # Even if it has a md5, we should also work with mtimes
2327         # We allready have a (better) version
2328         next if ( (not $see_all)
2329                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]}); 
2330         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2331         
2332         # We reached there. The file hasn't been seen.
2333         push @good_versions,($ref);
2334     }
2335         
2336     # To be nice with the user, we re-sort good_versions by
2337     # inchanger desc, mtime desc
2338     @good_versions = sort { $b->[4] <=> $a->[4] 
2339                          || $b->[2] <=> $a->[2]} @good_versions;
2340         
2341     return @good_versions;
2342 }
2343
2344 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2345 # This sub creates a BSR from the information in the restore_list
2346 # Returns the BSR as a string
2347 sub create_filelist
2348 {
2349         my $self = shift;
2350         my $dbh = $self->{dbh};
2351         my %mediainfos;
2352         # This query gets all jobid/jobmedia/media combination.
2353         my $query = "
2354 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile, 
2355        JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2356        JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex, 
2357        Media.Volumename, Media.MediaType
2358 FROM Job, JobMedia, Media
2359 WHERE Job.JobId = JobMedia.JobId
2360   AND JobMedia.MediaId = Media.MediaId
2361   ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2362         
2363
2364         my $result = $dbh->selectall_arrayref($query);
2365
2366         # We will store everything hashed by jobid.
2367
2368         foreach my $refrow (@$result)
2369         {
2370                 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2371                 $firstindex, $lastindex, $startblock, $endblock,
2372                 $volindex, $volumename, $mediatype) = @{$refrow};
2373
2374                 # We just have to deal with the case where starfile != endfile
2375                 # In this case, we concatenate both, for the bsr
2376                 if ($startfile != $endfile) { 
2377                       $startfile = $startfile . '-' . $endfile;
2378                 }
2379
2380                 my @tmparray = 
2381                 ($jobid, $volsessionid, $volsessiontime, $startfile, 
2382                 $firstindex, $lastindex, $startblock .'-'. $endblock,
2383                 $volindex, $volumename, $mediatype);
2384                 
2385                 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2386         }
2387
2388         
2389         # reminder : restore_list looks like this : 
2390         # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2391         
2392         # Here, we retrieve every file/dir that could be in the restore
2393         # We do as simple as possible for the SQL engine (no crazy joins,
2394         # no pseudo join (>= FirstIndex ...), etc ...
2395         # We do a SQL union of all the files/dirs specified in the restore_list
2396         my @select_queries;
2397         foreach my $entry (@{$self->{restore_list}->{data}})
2398         {
2399                 if ($entry->[2] eq 'dir')
2400                 {
2401                         my $dir = unpack('u', $entry->[0]);
2402                         my $inclause = $entry->[3]; #curjobids
2403
2404                         my $query = 
2405 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2406   FROM File, Path, Filename
2407   WHERE Path.PathId = File.PathId
2408   AND File.FilenameId = Filename.FilenameId
2409   AND Path.Path LIKE '$dir%'
2410   AND File.JobId IN ($inclause) )";
2411                         push @select_queries,($query);
2412                 }
2413                 else
2414                 {
2415                         # It's a file. Great, we allready have most 
2416                         # of what is needed. Simple and efficient query
2417                         my $file = unpack('u', $entry->[0]);
2418                         my @file = split '/',$file;
2419                         $file = pop @file;
2420                         my $dir = join('/',@file);
2421                         
2422                         my $jobid = $entry->[1];
2423                         my $fileindex = $entry->[7];
2424                         my $inclause = $entry->[3]; # curjobids
2425                         my $query = 
2426 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2427   FROM File, Path, Filename
2428   WHERE Path.PathId = File.PathId
2429   AND File.FilenameId = Filename.FilenameId
2430   AND Path.Path = '$dir/'
2431   AND Filename.Name = '$file'
2432   AND File.JobId = $jobid)";
2433                         push @select_queries,($query);
2434                 }
2435         }
2436         $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2437
2438         print $query,"\n" if $debug;
2439         
2440         #Now we run the query and parse the result...
2441         # there may be a lot of records, so we better be efficient
2442         # We use the bind column method, working with references...
2443
2444         my $sth = $dbh->prepare($query);
2445         $sth->execute;
2446
2447         my ($path,$name,$fileindex,$jobid);
2448         $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2449         
2450         # The temp place we're going to save all file
2451         # list to before the real list
2452         my @temp_list;
2453
2454         RECORD_LOOP:
2455         while ($sth->fetchrow_arrayref())
2456         {
2457                 # This may look dumb, but we're going to do a join by ourselves,
2458                 # to save memory and avoid sending a complex query to mysql
2459                 my $complete_path = $path . $name;
2460                 my $is_dir = 0;
2461                 
2462                 if ( $name eq '')
2463                 {
2464                         $is_dir = 1;
2465                 }
2466                 
2467                 # Remove trailing slash (normalize file and dir name)
2468                 $complete_path =~ s/\/$//;
2469                 
2470                 # Let's find the ref(s) for the %mediainfo element(s) 
2471                 # containing the data for this file
2472                 # There can be several matches. It is the pseudo join.
2473                 my $med_idx=0;
2474                 my $max_elt=@{$mediainfos{$jobid}}-1;
2475                 MEDIA_LOOP:
2476                 while($med_idx <= $max_elt)
2477                 {
2478                         my $ref = $mediainfos{$jobid}->[$med_idx];
2479                         # First, can we get rid of the first elements of the
2480                         # array ? (if they don't contain valuable records
2481                         # anymore
2482                         if ($fileindex > $ref->[5])
2483                         {
2484                                 # It seems we don't need anymore
2485                                 # this entry in %mediainfo (the input data
2486                                 # is sorted...)
2487                                 # We get rid of it.
2488                                 shift @{$mediainfos{$jobid}};
2489                                 $max_elt--;
2490                                 next MEDIA_LOOP;
2491                         }
2492                         # We will do work on this elt. We can ++
2493                         # $med_idx for next loop
2494                         $med_idx++;
2495
2496                         # %mediainfo row looks like : 
2497                         # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2498                         # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2499                         # MediaType)
2500                         
2501                         # We are in range. We store and continue looping
2502                         # in the medias
2503                         if ($fileindex >= $ref->[4])
2504                         {
2505                                 my @data = ($complete_path,$is_dir,
2506                                             $fileindex,$ref);
2507                                 push @temp_list,(\@data);
2508                                 next MEDIA_LOOP;
2509                         }
2510                         
2511                         # We are not in range. No point in continuing looping
2512                         # We go to next record.
2513                         next RECORD_LOOP;
2514                 }
2515         }
2516         # Now we have the array.
2517         # We're going to sort it, by 
2518         # path, volsessiontime DESC (get the most recent file...)
2519         # The array rows look like this :
2520         # complete_path,is_dir,fileindex,
2521         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2522         #       LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2523         @temp_list = sort {$a->[0] cmp $b->[0]
2524                         || $b->[3]->[2] <=> $a->[3]->[2]
2525                           } @temp_list;
2526
2527         my @restore_list;
2528         my $prev_complete_path='////'; # Sure not to match
2529         my $prev_is_file=1;
2530         my $prev_jobid;
2531
2532         while (my $refrow = shift @temp_list)
2533         {
2534                 # For the sake of readability, we load $refrow 
2535                 # contents in real scalars
2536                 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2537                 my $jobid= $refother->[0]; # We don't need the rest...
2538
2539                 # We skip this entry.
2540                 # We allready have a newer one and this 
2541                 # isn't a continuation of the same file
2542                 next if ($complete_path eq $prev_complete_path 
2543                          and $jobid != $prev_jobid);
2544                 
2545                 
2546                 if ($prev_is_file 
2547                     and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2548                 {
2549                         # We would be recursing inside a file.
2550                         # Just what we don't want (dir replaced by file
2551                         # between two backups
2552                         next;
2553                 }
2554                 elsif ($is_dir)
2555                 {
2556                         # It is a directory
2557                         push @restore_list,($refrow);
2558                         
2559                         $prev_complete_path = $complete_path;
2560                         $prev_jobid = $jobid;
2561                         $prev_is_file = 0;
2562                 }
2563                 else
2564                 {
2565                         # It is a file
2566                         push @restore_list,($refrow);
2567                         
2568                         $prev_complete_path = $complete_path;
2569                         $prev_jobid = $jobid;
2570                         $prev_is_file = 1;
2571                 }
2572         }
2573         # We get rid of @temp_list... save memory
2574         @temp_list=();
2575
2576         # Ok everything is in the list. Let's sort it again in another way.
2577         # This time it will be in the bsr file order
2578
2579         # we sort the results by 
2580         # volsessiontime, volsessionid, volindex, fileindex 
2581         # to get all files in right order...
2582         # Reminder : The array rows look like this :
2583         # complete_path,is_dir,fileindex,
2584         # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2585         #       StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2586
2587         @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2] 
2588                            || $a->[3]->[1] <=> $b->[3]->[1] 
2589                            || $a->[3]->[7] <=> $b->[3]->[7] 
2590                            || $a->[2] <=> $b->[2] } 
2591                                 @restore_list;
2592
2593         # Now that everything is ready, we create the bsr
2594         my $prev_fileindex=-1;
2595         my $prev_volsessionid=-1;
2596         my $prev_volsessiontime=-1;
2597         my $prev_volumename=-1;
2598         my $prev_volfile=-1;
2599         my $prev_mediatype;
2600         my $prev_volblocks;
2601         my $count=0;
2602         my $first_of_current_range=0;
2603         my @fileindex_ranges;
2604         my $bsr='';
2605
2606         foreach my $refrow (@restore_list)
2607         {
2608                 my (undef,undef,$fileindex,$refother)=@{$refrow};
2609                 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2610                     $volblocks,undef,$volumename,$mediatype)=@{$refother};
2611                 
2612                 # We can specifiy the number of files in each section of the
2613                 # bsr to speedup restore (bacula can then jump over the
2614                 # end of tape files.
2615                 $count++;
2616                 
2617                 
2618                 if ($prev_volumename eq '-1')
2619                 {
2620                         # We only have to start the new range...
2621                         $first_of_current_range=$fileindex;
2622                 }
2623                 elsif ($prev_volsessionid != $volsessionid 
2624                        or $prev_volsessiontime != $volsessiontime 
2625                        or $prev_volumename ne $volumename 
2626                        or $prev_volfile ne $volfile)
2627                 {
2628                         # We have to create a new section in the bsr...
2629                         # We print the previous one ... 
2630                         # (before that, save the current range ...)
2631                         if ($first_of_current_range != $prev_fileindex)
2632                         {
2633                                 # we are in a range
2634                                 push @fileindex_ranges,
2635                                     ("$first_of_current_range-$prev_fileindex");
2636                         }
2637                         else
2638                         {
2639                                  # We are out of a range,
2640                                  # but there is only one element in the range
2641                                 push @fileindex_ranges,
2642                                     ("$first_of_current_range");
2643                         }
2644                         
2645                         $bsr.=print_bsr_section(\@fileindex_ranges,
2646                                                 $prev_volsessionid,
2647                                                 $prev_volsessiontime,
2648                                                 $prev_volumename,
2649                                                 $prev_volfile,
2650                                                 $prev_mediatype,
2651                                                 $prev_volblocks,
2652                                                 $count-1);
2653                         $count=1;
2654                         # Reset for next loop
2655                         @fileindex_ranges=();
2656                         $first_of_current_range=$fileindex;
2657                 }
2658                 elsif ($fileindex-1 != $prev_fileindex)
2659                 {
2660                         # End of a range of fileindexes
2661                         if ($first_of_current_range != $prev_fileindex)
2662                         {
2663                                 #we are in a range
2664                                 push @fileindex_ranges,
2665                                     ("$first_of_current_range-$prev_fileindex");
2666                         }
2667                         else
2668                         {
2669                                  # We are out of a range,
2670                                  # but there is only one element in the range
2671                                 push @fileindex_ranges,
2672                                     ("$first_of_current_range");
2673                         }
2674                         $first_of_current_range=$fileindex;
2675                 }
2676                 $prev_fileindex=$fileindex;
2677                 $prev_volsessionid = $volsessionid;
2678                 $prev_volsessiontime = $volsessiontime;
2679                 $prev_volumename = $volumename;
2680                 $prev_volfile=$volfile;
2681                 $prev_mediatype=$mediatype;
2682                 $prev_volblocks=$volblocks;
2683
2684         }
2685
2686         # Ok, we're out of the loop. Alas, there's still the last record ...
2687         if ($first_of_current_range != $prev_fileindex)
2688         {
2689                 # we are in a range
2690                 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2691                 
2692         }
2693         else
2694         {
2695                 # We are out of a range,
2696                 # but there is only one element in the range
2697                 push @fileindex_ranges,("$first_of_current_range");
2698                 
2699         }
2700         $bsr.=print_bsr_section(\@fileindex_ranges,
2701                                 $prev_volsessionid,
2702                                 $prev_volsessiontime,
2703                                 $prev_volumename,
2704                                 $prev_volfile,
2705                                 $prev_mediatype,
2706                                 $prev_volblocks,
2707                                 $count);
2708         
2709         return $bsr;
2710 }
2711
2712 sub print_bsr_section
2713 {
2714     my ($ref_fileindex_ranges,$volsessionid,
2715         $volsessiontime,$volumename,$volfile,
2716         $mediatype,$volblocks,$count)=@_;
2717     
2718     my $bsr='';
2719     $bsr .= "Volume=\"$volumename\"\n";
2720     $bsr .= "MediaType=\"$mediatype\"\n";
2721     $bsr .= "VolSessionId=$volsessionid\n";
2722     $bsr .= "VolSessionTime=$volsessiontime\n";
2723     $bsr .= "VolFile=$volfile\n";
2724     $bsr .= "VolBlock=$volblocks\n";
2725     
2726     foreach my $range (@{$ref_fileindex_ranges})
2727     {
2728         $bsr .= "FileIndex=$range\n";
2729     }
2730     
2731     $bsr .= "Count=$count\n";
2732     return $bsr;
2733 }
2734
2735 # This function estimates the size to be restored for an entry of the restore
2736 # list
2737 # In : self,reference to the entry
2738 # Out : size in bytes, number of files
2739 sub estimate_restore_size
2740 {
2741     # reminder : restore_list looks like this : 
2742     # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2743     my $self=shift;
2744     my ($entry)=@_;
2745     my $query;
2746     my $dbh = $self->{dbh};
2747     if ($entry->[2] eq 'dir')
2748     {
2749         my $dir = unpack('u', $entry->[0]);
2750         my $inclause = $entry->[3]; #curjobids
2751         $query = 
2752 "SELECT Path.Path, File.FilenameId, File.LStat
2753   FROM File, Path, Job
2754   WHERE Path.PathId = File.PathId
2755   AND File.JobId = Job.JobId
2756   AND Path.Path LIKE '$dir%'
2757   AND File.JobId IN ($inclause)
2758   ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2759     }
2760     else
2761     {
2762         # It's a file. Great, we allready have most 
2763         # of what is needed. Simple and efficient query
2764         my $file = unpack('u', $entry->[0]);
2765         my @file = split '/',$file;
2766         $file = pop @file;
2767         my $dir = join('/',@file);
2768         
2769         my $jobid = $entry->[1];
2770         my $fileindex = $entry->[7];
2771         my $inclause = $entry->[3]; # curjobids
2772         $query = 
2773 "SELECT Path.Path, File.FilenameId, File.Lstat
2774   FROM File, Path, Filename
2775   WHERE Path.PathId = File.PathId
2776   AND Path.Path = '$dir/'
2777   AND Filename.Name = '$file'
2778   AND File.JobId = $jobid
2779   AND Filename.FilenameId = File.FilenameId";
2780     }
2781
2782     print $query,"\n" if $debug;
2783     my ($path,$nameid,$lstat);
2784     my $sth = $dbh->prepare($query);
2785     $sth->execute;
2786     $sth->bind_columns(\$path,\$nameid,\$lstat);
2787     my $old_path='';
2788     my $old_nameid='';
2789     my $total_size=0;
2790     my $total_files=0;
2791
2792     refresh_screen();
2793
2794     my $rcount=0;
2795     # We fetch all rows
2796     while ($sth->fetchrow_arrayref())
2797     {
2798         # Only the latest version of a file
2799         next if ($nameid eq $old_nameid and $path eq $old_path);
2800
2801         if ($rcount > 15000) {
2802             refresh_screen();
2803             $rcount=0;
2804         } else {
2805             $rcount++;
2806         }
2807
2808         # We get the size of this file
2809         my $size=lstat_attrib($lstat,'st_size');
2810         $total_size += $size;
2811         $total_files++;
2812         $old_path=$path;
2813         $old_nameid=$nameid;
2814     }
2815     return ($total_size,$total_files);
2816 }
2817
2818
2819 # Get metadata
2820 {
2821     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
2822                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
2823                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
2824                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
2825                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
2826                           'data_stream' => 15);;
2827     sub array_attrib
2828     {
2829         my ($attrib,$ref_attrib)=@_;
2830         return $ref_attrib->[$attrib_name_id{$attrib}];
2831     }
2832         
2833     sub file_attrib
2834     {   # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
2835
2836         my ($file, $attrib)=@_;
2837         
2838         if (defined $attrib_name_id{$attrib}) {
2839
2840             my @d = split(' ', $file->[2]) ; # TODO : cache this
2841             
2842             return from_base64($d[$attrib_name_id{$attrib}]);
2843
2844         } elsif ($attrib eq 'jobid') {
2845
2846             return $file->[3];
2847
2848         } elsif ($attrib eq 'name') {
2849
2850             return $file->[1];
2851             
2852         } else  {
2853             die "Attribute not known : $attrib.\n";
2854         }
2855     }
2856
2857     # Return the jobid or attribute asked for a dir
2858     sub dir_attrib
2859     {
2860         my ($self,$dir,$attrib)=@_;
2861         
2862         my @dir = split('/',$dir,-1);
2863         my $refdir=$self->{dirtree}->{$self->current_client};
2864         
2865         if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
2866         {
2867             die "Attribute not known : $attrib.\n";
2868         }
2869         # Find the leaf
2870         foreach my $subdir (@dir)
2871         {
2872             $refdir = $refdir->[0]->{$subdir};
2873         }
2874         
2875         # $refdir is now the reference to the dir's array
2876         # Is the a jobid in @CurrentJobIds where the lstat is
2877         # defined (we'll search in reverse order)
2878         foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
2879         {
2880             if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
2881             {
2882                 if ($attrib eq 'jobid')
2883                 {
2884                     return $jobid;
2885                 }
2886                 else
2887                 {
2888                     my @attribs = parse_lstat($refdir->[1]->{$jobid});
2889                     return $attribs[$attrib_name_id{$attrib}+1];
2890                 }
2891             }
2892         }
2893
2894         return 0; # We cannot get a good attribute.
2895                   # This directory is here for the sake of visibility
2896     }
2897     
2898     sub lstat_attrib
2899     {
2900         my ($lstat,$attrib)=@_;
2901         if (defined $attrib_name_id{$attrib}) 
2902         {
2903             my @d = split(' ', $lstat) ; # TODO : cache this
2904             return from_base64($d[$attrib_name_id{$attrib}]);
2905         }
2906     }
2907 }
2908
2909 {
2910     # Base 64 functions, directly from recover.pl.
2911     # Thanks to
2912     # Karl Hakimian <hakimian@aha.com>
2913     # This section is also under GPL v2 or later.
2914     my @base64_digits;
2915     my @base64_map;
2916     my $is_init=0;
2917     sub init_base64
2918     {
2919         @base64_digits = (
2920         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
2921         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
2922         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
2923         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
2924         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
2925                           );
2926         @base64_map = (0) x 128;
2927         
2928         for (my $i=0; $i<64; $i++) {
2929             $base64_map[ord($base64_digits[$i])] = $i;
2930         }
2931         $is_init = 1;
2932     }
2933
2934     sub from_base64 {
2935         if(not $is_init)
2936         {
2937             init_base64();
2938         }
2939         my $where = shift;
2940         my $val = 0;
2941         my $i = 0;
2942         my $neg = 0;
2943         
2944         if (substr($where, 0, 1) eq '-') {
2945             $neg = 1;
2946             $where = substr($where, 1);
2947         }
2948         
2949         while ($where ne '') {
2950             $val *= 64;
2951             my $d = substr($where, 0, 1);
2952             $val += $base64_map[ord(substr($where, 0, 1))];
2953             $where = substr($where, 1);
2954         }
2955         
2956         return $val;
2957     }
2958
2959     sub parse_lstat {
2960         my ($lstat)=@_;
2961         my @attribs = split(' ',$lstat);
2962         foreach my $element (@attribs)
2963         {
2964             $element = from_base64($element);
2965         }
2966         return @attribs;
2967     }
2968 }
2969 1;
2970
2971 ################################################################
2972
2973 package main;
2974
2975 my $conf = "$ENV{HOME}/.brestore.conf" ;
2976 my $p = new Pref($conf);
2977
2978 if (! -f $conf) {
2979     $p->write_config();
2980 }
2981
2982 $glade_file = $p->{glade_file};
2983
2984 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
2985     if (-f "$path/$glade_file") {
2986         $glade_file = "$path/$glade_file" ;
2987         last;
2988     }
2989 }
2990
2991 if ( -f $glade_file) {
2992     my $w = new DlgResto($p);
2993
2994 } else {
2995     my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close', 
2996 "Can't find your brestore.glade (glade_file => '$glade_file')
2997 Please, edit your $conf to setup it." );
2998  
2999     $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3000     $widget->run;
3001     exit 1;
3002 }
3003
3004 Gtk2->main; # Start Gtk2 main loop      
3005
3006 # that's it!
3007
3008 exit 0;
3009
3010
3011 __END__
3012
3013 TODO : 
3014
3015
3016 # Code pour trier les colonnes    
3017     my $mod = $fileview->get_model();
3018     $mod->set_default_sort_func(sub {
3019             my ($model, $item1, $item2) = @_;
3020             my $a = $model->get($item1, 1);  # récupération de la valeur de la 2ème 
3021             my $b = $model->get($item2, 1);  # colonne (indice 1)
3022             return $a cmp $b;
3023         }
3024     );
3025     
3026     $fileview->set_headers_clickable(1);
3027     my $col = $fileview->get_column(1);    # la colonne NOM, colonne numéro 2
3028     $col->signal_connect('clicked', sub {
3029             my ($colonne, $model) = @_;
3030             $model->set_sort_column_id (1, 'ascending');
3031         },
3032         $mod
3033     );