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