4 # path to your brestore.glade
5 my $glade_file = 'brestore.glade' ;
9 brestore.pl - A Perl/Gtk console for Bacula
17 Setup ~/.brestore.conf to find your brestore.glade
19 On debian like system, you need :
20 - libgtk2-gladexml-perl
21 - libdbd-mysql-perl or libdbd-pg-perl
24 To speed up database query you have to create theses indexes
25 - CREATE INDEX file_pathid on File(PathId);
28 To follow restore job, you must have a running Bweb installation.
32 Copyright (C) 2006 Marc Cousin and Eric Bollengier
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.
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.
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.
49 Base 64 functions from Karl Hakimian <hakimian@aha.com>
50 Integrally copied from recover.pl from bacula source distribution.
54 use File::Spec; # portable path manipulations
55 use Gtk2 '-init'; # auto-initialize Gtk2
57 use Gtk2::SimpleList; # easy wrapper for list views
58 use Gtk2::Gdk::Keysyms; # keyboard code constants
59 use Data::Dumper qw/Dumper/;
61 my $debug=0; # can be on brestore.conf
63 ################################################################
65 package DlgFileVersion;
67 sub on_versions_close_clicked
69 my ($self, $widget)=@_;
70 $self->{version}->destroy();
73 sub on_selection_button_press_event
75 print "on_selection_button_press_event()\n";
80 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
82 DlgResto::drag_set_info($widget,
89 my ($class, $dbh, $client, $path, $file) = @_;
92 version => undef, # main window
95 # we load version widget of $glade_file
96 my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_version");
98 # Connect signals magically
99 $glade_box->signal_autoconnect_from_package($self);
101 $glade_box->get_widget("version_label")
102 ->set_markup("<b>File revisions : $client:$path/$file</b>");
104 my $widget = $glade_box->get_widget('version_fileview');
105 my $fileview = Gtk2::SimpleList->new_from_treeview(
107 'h_name' => 'hidden',
108 'h_jobid' => 'hidden',
109 'h_type' => 'hidden',
111 'InChanger' => 'pixbuf',
118 DlgResto::init_drag_drop($fileview);
120 my @v = DlgResto::get_all_file_versions($dbh,
126 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
128 my $icon = ($inchanger)?$DlgResto::yesicon:$DlgResto::noicon;
130 DlgResto::listview_push($fileview,
131 $file, $jobid, 'file',
132 $icon, $volname, $jobid,DlgResto::human($size),
133 scalar(localtime($mtime)), $md5);
136 $self->{version} = $glade_box->get_widget('dlg_version');
137 $self->{version}->show();
142 sub on_forward_keypress
148 ################################################################
153 my ($package, $text) = @_;
157 my $glade = Gtk2::GladeXML->new($glade_file, "dlg_warn");
159 # Connect signals magically
160 $glade->signal_autoconnect_from_package($self);
161 $glade->get_widget('label_warn')->set_text($text);
165 $self->{window} = $glade->get_widget('dlg_warn');
166 $self->{window}->show_all();
173 $self->{window}->destroy();
177 ################################################################
183 # %arg = (bsr_file => '/path/to/bsr', # on director
184 # volumes => [ '00001', '00004']
190 my ($class, %arg) = @_;
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
199 # we load launch widget of $glade_file
200 my $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file,
203 # Connect signals magically
204 $glade->signal_autoconnect_from_package($self);
206 my $widget = $glade->get_widget('volumeview');
207 my $volview = Gtk2::SimpleList->new_from_treeview(
209 'InChanger' => 'pixbuf',
213 my $infos = get_volume_inchanger($arg{pref}->{dbh}, $arg{volumes}) ;
215 # we replace 0 and 1 by $noicon and $yesicon
216 for my $i (@{$infos}) {
218 $i->[0] = $DlgResto::noicon;
220 $i->[0] = $DlgResto::yesicon;
225 push @{ $volview->{data} }, @{$infos} ;
227 my $console = $self->{bconsole} = new Bconsole(pref => $arg{pref});
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}") ;
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,
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);
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);
253 # find default_restore_job in jobs list
254 my $default_restore_job = $arg{pref}->{default_restore_job} ;
258 if ($j =~ /$default_restore_job/io) {
264 $w->set_active($index);
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);
272 $glade->get_widget('dlg_launch')->show_all();
279 my ($self, $client, $jobid) = @_;
281 my $ret = $self->{pref}->go_bweb("?action=dsp_cur_job;jobid=$jobid;client=$client", "view job status");
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)");
291 $self->on_cancel_resto_clicked();
294 sub on_cancel_resto_clicked
297 $self->{glade}->get_widget('dlg_launch')->destroy();
300 sub on_submit_resto_clicked
303 my $glade = $self->{glade};
305 my $r = $self->copy_bsr($self->{bsr_file}, $self->{pref}->{bsr_dest}) ;
308 new DlgWarn("Can't copy bsr file to director ($self->{error})");
312 my $fileset = $glade->get_widget('combo_launch_fileset')
315 my $storage = $glade->get_widget('combo_launch_storage')
318 my $where = $glade->get_widget('entry_launch_where')->get_text();
320 my $job = $glade->get_widget('combo_launch_job')
324 new DlgWarn("Can't use this job");
328 my $client = $glade->get_widget('combo_launch_client')
331 if (! $client or $client eq $DlgResto::client_list_empty) {
332 new DlgWarn("Can't use this client ($client)");
336 my $prio = $glade->get_widget('spin_launch_priority')->get_value();
338 my $replace = $glade->get_widget('chkbp_launch_replace')->get_active();
339 $replace=($replace)?'always':'never';
341 my $jobid = $self->{bconsole}->run(job => $job,
350 $self->show_job($client, $jobid);
353 sub on_combo_storage_button_press_event
356 print "on_combo_storage_button_press_event()\n";
359 sub on_combo_fileset_button_press_event
362 print "on_combo_fileset_button_press_event()\n";
366 sub on_combo_job_button_press_event
369 print "on_combo_job_button_press_event()\n";
372 sub get_volume_inchanger
374 my ($dbh, $vols) = @_;
376 my $lst = join(',', map { $dbh->quote($_) } @{ $vols } ) ;
378 my $rq = "SELECT InChanger, VolumeName
380 WHERE VolumeName IN ($lst)
383 my $res = $dbh->selectall_arrayref($rq);
384 return $res; # [ [ 1, VolName].. ]
388 use File::Copy qw/copy/;
389 use File::Basename qw/basename/;
391 # We must kown the path+filename destination
392 # $self->{error} contains error message
393 # it return 0/1 if fail/success
396 my ($self, $src, $dst) = @_ ;
397 print "$src => $dst\n"
404 if ($dst =~ m!file:/(/.+)!) {
405 $ret = copy($src, $1);
407 $dstfile = "$1/" . basename($src) ;
409 } elsif ($dst =~ m!scp://([^:]+:(.+))!) {
410 $err = `scp $src $1 2>&1` ;
412 $dstfile = "$2/" . basename($src) ;
416 $err = "$dst not implemented yet";
417 File::Copy::copy($src, \*STDOUT);
420 $self->{error} = $err;
423 $self->{error} = $err;
432 ################################################################
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');
445 $about_widget->show() ;
448 sub on_about_okbutton_clicked
450 $about_widget->hide() ;
455 ################################################################
461 my ($class, $config_file) = @_;
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
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/],
486 $self->read_config();
495 # We read the parameters. They come from the configuration files
496 my $cfgfile ; my $tmpbuffer;
497 if (open FICCFG, $self->{config_file})
499 while(read FICCFG,$tmpbuffer,4096)
501 $cfgfile .= $tmpbuffer;
505 no strict; # I have no idea of the contents of the file
506 eval '$refparams' . " = $cfgfile";
509 for my $p (keys %{$refparams}) {
510 $self->{$p} = $refparams->{$p};
513 if (defined $self->{debug}) {
514 $debug = $self->{debug} ;
517 # TODO : Force dumb default values and display a message
527 for my $k (@{ $self->{entry_keyword} }) {
528 $parameters{$k} = $self->{$k};
531 for my $k (@{ $self->{chk_keyword} }) {
532 $parameters{$k} = $self->{$k};
535 if (open FICCFG,">$self->{config_file}")
537 print FICCFG Data::Dumper->Dump([\%parameters], [qw($parameters)]);
542 # TODO : Display a message
551 $self->{dbh}->disconnect() ;
555 delete $self->{error};
557 if (not $self->{connection_string})
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.";
567 $self->{dbh} = DBI->connect($self->{connection_string},
572 $self->{error} = "Can't open bacula database. " .
573 "Database connect string '" .
574 $self->{connection_string} ."' $!";
577 $self->{dbh}->{RowCacheSize}=100;
583 my ($self, $url, $msg) = @_;
585 unless ($self->{mozilla} and $self->{bweb}) {
586 new DlgWarn("You must install Bweb and set your mozilla bin to $msg");
590 system("$self->{mozilla} -remote 'Ping()'");
592 new DlgWarn("Warning, you must have a running $self->{mozilla} to $msg");
596 my $cmd = "$self->{mozilla} -remote 'OpenURL($self->{bweb}$url,new-tab)'" ;
604 ################################################################
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);
614 my ($class, $pref) = @_;
617 pref => $pref, # Pref ref
618 dlgresto => undef, # DlgResto ref
626 my ($self, $dlgresto) = @_ ;
628 unless ($self->{glade}) {
629 $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_pref") ;
630 $self->{glade}->signal_autoconnect_from_package($self);
633 $self->{dlgresto} = $dlgresto;
635 my $g = $self->{glade};
636 my $p = $self->{pref};
638 for my $k (@{ $p->{entry_keyword} }) {
639 $g->get_widget("entry_$k")->set_text($p->{$k}) ;
642 for my $k (@{ $p->{chk_keyword} }) {
643 $g->get_widget("chkbp_$k")->set_active($p->{$k}) ;
646 $g->get_widget("dlg_pref")->show_all() ;
649 sub on_applybutton_clicked
652 my $glade = $self->{glade};
653 my $pref = $self->{pref};
655 for my $k (@{ $pref->{entry_keyword} }) {
656 my $w = $glade->get_widget("entry_$k") ;
657 $pref->{$k} = $w->get_text();
660 for my $k (@{ $pref->{chk_keyword} }) {
661 my $w = $glade->get_widget("chkbp_$k") ;
662 $pref->{$k} = $w->get_active();
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();
671 $self->{dlgresto}->set_status($pref->{error});
675 # Handle prefs ok click (apply/dismiss dialog)
676 sub on_okbutton_clicked
679 $self->on_applybutton_clicked();
681 unless ($self->{pref}->{error}) {
682 $self->on_cancelbutton_clicked();
685 sub on_dialog_delete_event
688 $self->on_cancelbutton_clicked();
692 sub on_cancelbutton_clicked
695 $self->{glade}->get_widget('dlg_pref')->hide();
696 delete $self->{dlgresto};
700 ################################################################
710 # Kept as is from the perl-gtk example. Draws the pretty icons
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);
723 # init combo (and create ListStore object)
726 my ($widget, @type) = @_ ;
727 my %type_info = ('text' => 'Glib::String',
728 'markup' => 'Glib::String',
731 my $lst = new Gtk2::ListStore ( map { $type_info{$_} } @type );
733 $widget->set_model($lst);
737 if ($t eq 'text' or $t eq 'markup') {
738 $cell = new Gtk2::CellRendererText();
740 $widget->pack_start($cell, 1);
741 $widget->add_attribute($cell, $t, $i++);
746 # fill simple combo (one element per row)
749 my ($list, @what) = @_;
753 foreach my $w (@what)
756 my $i = $list->append();
757 $list->set($i, 0, $w);
764 my @unit = qw(b Kb Mb Gb Tb);
767 my $format = '%i %s';
768 while ($val / 1024 > 1) {
772 $format = ($i>0)?'%0.1f %s':'%i %s';
773 return sprintf($format, $val, $unit[$i]);
778 my ($self, $dbh) = @_;
784 my ($fileview) = shift;
785 my $fileview_target_entry = {target => 'STRING',
786 flags => ['GTK_TARGET_SAME_APP'],
789 $fileview->enable_model_drag_source(['button1_mask', 'button3_mask'],
790 ['copy'],$fileview_target_entry);
791 $fileview->get_selection->set_mode('multiple');
793 # set some useful SimpleList properties
794 $fileview->set_headers_clickable(0);
795 foreach ($fileview->get_columns())
797 $_->set_resizable(1);
798 $_->set_sizing('grow-only');
804 my ($class, $pref) = @_;
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
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
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");
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");
835 $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_resto");
836 $glade->signal_autoconnect_from_package($self);
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();
843 $self->{dlg_pref} = new DlgPref($pref);
845 my $c = $self->{client_combobox} = $glade->get_widget('combo_client');
846 $self->{list_client} = init_combo($c, 'text');
848 $c = $self->{restore_backup_combobox} = $glade->get_widget('combo_list_backups');
849 $self->{list_backup} = init_combo($c, 'text', 'markup');
851 # Connect glade-fileview to Gtk2::SimpleList
852 # and set up drag n drop between $fileview and $restore_list
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)
857 my $widget = $glade->get_widget('fileview');
858 my $fileview = $self->{fileview} = Gtk2::SimpleList->new_from_treeview(
860 'h_name' => 'hidden',
861 'h_jobid' => 'hidden',
862 'h_type' => 'hidden',
865 'File Name' => 'text',
868 init_drag_drop($fileview);
869 $fileview->set_search_column(4); # search on File Name
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(
875 'h_name' => 'hidden',
876 'h_jobid' => 'hidden',
877 'h_type' => 'hidden',
878 'h_curjobid' => 'hidden',
881 'File Name' => 'text',
883 'FileIndex' => 'text');
885 my @restore_list_target_table = ({'target' => 'STRING',
889 $restore_list->enable_model_drag_dest(['copy'],@restore_list_target_table);
890 $restore_list->get_selection->set_mode('multiple');
892 $widget = $glade->get_widget('infoview');
893 my $infoview = $self->{fileinfo} = Gtk2::SimpleList->new_from_treeview(
895 'h_name' => 'hidden',
896 'h_jobid' => 'hidden',
897 'h_type' => 'hidden',
899 'InChanger' => 'pixbuf',
906 init_drag_drop($infoview);
908 $pref->connect_db() || $self->{dlg_pref}->display($self);
911 $self->{dbh} = $pref->{dbh};
912 $self->init_server_backup_combobox();
916 # set status bar informations
919 my ($self, $string) = @_;
920 my $context = $self->{status}->get_context_id('Main');
921 $self->{status}->push($context, $string);
924 sub on_time_select_changed
932 my $c = $self->{glade}->get_widget('combo_time');
933 return $c->get_active_text;
936 # This sub returns all clients declared in DB
940 my $query = "SELECT Name FROM Client ORDER BY Name";
941 print $query,"\n" if $debug;
942 my $result = $dbh->selectall_arrayref($query);
944 foreach my $refrow (@$result)
946 push @return_array,($refrow->[0]);
948 return @return_array;
951 sub get_wanted_job_status
958 return "'T', 'A', 'E'";
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
967 my ($dbh, $client, $ok_only)=@_;
968 my $status = get_wanted_job_status($ok_only);
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'
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);
985 # init infoview widget
989 @{$self->{fileinfo}->{data}} = ();
996 @{$self->{restore_list}->{data}} = ();
999 sub on_estimate_clicked
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',
1010 'Computing size...');
1014 my $title = "Computing size...\n";
1016 foreach my $entry (@{$self->{restore_list}->{data}})
1018 my ($size, $nb) = $self->estimate_restore_size($entry);
1019 my $name = unpack('u', $entry->[0]);
1021 $txt .= "\n<i>$name</i> : $nb file(s)/" . human($size) ;
1022 $widget->set_markup($title . $txt);
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();});
1036 sub on_gen_bsr_clicked
1040 my @options = ("Choose a bsr file", $self->{mainwin}, 'save',
1041 'gtk-save','ok', 'gtk-cancel', 'cancel');
1044 my $w = new Gtk2::FileChooserDialog ( @options );
1049 if ($a eq 'cancel') {
1054 my $f = $w->get_filename();
1056 my $dlg = Gtk2::MessageDialog->new($self->{mainwin},
1057 'destroy-with-parent',
1058 'warning', 'ok-cancel', 'This file already exists, do you want to overwrite it ?');
1059 if ($dlg->run() eq 'ok') {
1073 if (open(FP, ">$save")) {
1074 my $bsr = $self->create_filelist();
1077 $self->set_status("Dumping BSR to $save ok");
1079 $self->set_status("Can't dump BSR to $save: $!");
1084 use File::Temp qw/tempfile/;
1086 sub on_go_button_clicked
1089 my $bsr = $self->create_filelist();
1090 my ($fh, $filename) = tempfile();
1093 chmod(0644, $filename);
1095 print "Dumping BSR info to $filename\n"
1098 # we get Volume list
1099 my %a = map { $_ => 1 } ($bsr =~ /Volume="(.+)"/g);
1100 my $vol = [ keys %a ] ; # need only one occurrence of each volume
1102 new DlgLaunch(pref => $self->{pref},
1104 bsr_file => $filename,
1109 our $client_list_empty = 'Clients list';
1110 our %type_markup = ('F' => '<b>$label F</b>',
1113 'B' => '<b>$label B</b>',
1115 'A' => '<span foreground=\"red\">$label</span>',
1117 'E' => '<span foreground=\"red\">$label</span>',
1120 sub on_list_client_changed
1122 my ($self, $widget) = @_;
1123 return 0 unless defined $self->{fileview};
1124 my $dbh = $self->{dbh};
1126 $self->{list_backup}->clear();
1128 if ($self->current_client eq $client_list_empty) {
1132 my @endtimes=get_all_endtimes_for_job($dbh,
1133 $self->current_client,
1134 $self->{pref}->{use_ok_bkp_only});
1135 foreach my $endtime (@endtimes)
1137 my $i = $self->{list_backup}->append();
1139 my $label = $endtime->[1] . " (" . $endtime->[4] . ")";
1140 eval "\$label = \"$type_markup{$endtime->[2]}\""; # job type
1141 eval "\$label = \"$type_markup{$endtime->[3]}\""; # job status
1143 $self->{list_backup}->set($i,
1148 $self->{restore_backup_combobox}->set_active(0);
1150 $self->{CurrentJobIds} = [
1151 set_job_ids_for_date($dbh,
1152 $self->current_client,
1153 $self->current_date,
1154 $self->{pref}->{use_ok_bkp_only})
1159 # undef $self->{dirtree};
1160 $self->refresh_fileview();
1164 sub fill_server_list
1166 my ($dbh, $combo, $list) = @_;
1168 my @clients=get_all_clients($dbh);
1172 my $i = $list->append();
1173 $list->set($i, 0, $client_list_empty);
1175 foreach my $client (@clients)
1177 $i = $list->append();
1178 $list->set($i, 0, $client);
1180 $combo->set_active(0);
1183 sub init_server_backup_combobox
1186 fill_server_list($self->{dbh},
1187 $self->{client_combobox},
1188 $self->{list_client}) ;
1191 #----------------------------------------------------------------------
1192 #Refreshes the file-view Redraws everything. The dir data is cached, the file
1193 #data isn't. There is additionnal complexity for dirs (visibility problems),
1194 #so the @CurrentJobIds is not sufficient.
1195 sub refresh_fileview
1198 my $fileview = $self->{fileview};
1199 my $client_combobox = $self->{client_combobox};
1200 my $cwd = $self->{cwd};
1202 @{$fileview->{data}} = ();
1204 $self->clear_infoview();
1206 my $client_name = $self->current_client;
1208 if (!$client_name or ($client_name eq $client_list_empty)) {
1209 $self->set_status("Client list empty");
1213 my @dirs = $self->list_dirs($cwd,$client_name);
1214 # [ [listfiles.id, listfiles.Name, File.LStat, File.JobId]..]
1215 my $files = $self->list_files($cwd);
1216 print "CWD : $cwd\n" if ($debug);
1218 my $file_count = 0 ;
1219 my $total_bytes = 0;
1221 # Add directories to view
1222 foreach my $dir (@dirs) {
1223 my $time = localtime($self->dir_attrib("$cwd/$dir",'st_mtime'));
1224 $total_bytes += 4096;
1227 listview_push($fileview,
1229 $self->dir_attrib("$cwd/$dir",'jobid'),
1239 foreach my $file (@$files)
1241 my $size = file_attrib($file,'st_size');
1242 my $time = localtime(file_attrib($file,'st_mtime'));
1243 $total_bytes += $size;
1245 # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
1247 listview_push($fileview,
1254 human($size), $time);
1257 $self->set_status("$file_count files/" . human($total_bytes));
1259 # set a decent default selection (makes keyboard nav easy)
1260 $fileview->select(0);
1264 sub on_about_activate
1266 DlgAbout::display();
1271 my ($tree, $path, $data) = @_;
1273 my @items = listview_get_all($tree) ;
1275 foreach my $i (@items)
1277 my @file_info = @{$i};
1280 # Ok, we have a corner case :
1285 $file = pack("u", $file_info[0]);
1289 $file = pack("u", $path . '/' . $file_info[0]);
1291 push @ret, join(" ; ", $file,
1292 $file_info[1], # $jobid
1293 $file_info[2], # $type
1297 my $data_get = join(" :: ", @ret);
1299 $data->set_text($data_get,-1);
1302 sub fileview_data_get
1304 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1305 drag_set_info($widget, $self->{cwd}, $data);
1308 sub fileinfo_data_get
1310 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1311 drag_set_info($widget, $self->{cwd}, $data);
1314 sub restore_list_data_received
1316 my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_;
1319 if ($info eq 40 || $info eq 0) # patch for display!=:0
1321 foreach my $elt (split(/ :: /, $data->data()))
1324 my ($file, $jobid, $type) =
1326 $file = unpack("u", $file);
1328 $self->add_selected_file_to_list($file, $jobid, $type);
1333 sub on_back_button_clicked {
1337 sub on_location_go_button_clicked
1340 $self->ch_dir($self->{location}->get_text());
1342 sub on_quit_activate {Gtk2->main_quit;}
1343 sub on_preferences_activate
1346 $self->{dlg_pref}->display($self) ;
1348 sub on_main_delete_event {Gtk2->main_quit;}
1349 sub on_bweb_activate
1352 $self->set_status("Open bweb on your browser");
1353 $self->{pref}->go_bweb('', "go on bweb");
1356 # Change to parent directory
1360 if ($self->{cwd} eq '/')
1364 my @dirs = File::Spec->splitdir ($self->{cwd});
1366 $self->ch_dir(File::Spec->catdir(@dirs));
1369 # Change the current working directory
1370 # * Updates fileview, location, and selection
1375 $self->{cwd} = shift;
1377 $self->refresh_fileview();
1378 $self->{location}->set_text($self->{cwd});
1383 # Handle dialog 'close' (window-decoration induced close)
1384 # * Just hide the dialog, and tell Gtk not to do anything else
1388 my ($self, $w) = @_;
1391 1; # consume this event!
1394 # Handle key presses in location text edit control
1395 # * Translate a Return/Enter key into a 'Go' command
1396 # * All other key presses left for GTK
1398 sub on_location_entry_key_release_event
1404 my $keypress = $event->keyval;
1405 if ($keypress == $Gtk2::Gdk::Keysyms{KP_Enter} ||
1406 $keypress == $Gtk2::Gdk::Keysyms{Return})
1408 $self->ch_dir($widget->get_text());
1410 return 1; # consume keypress
1413 return 0; # let gtk have the keypress
1416 sub on_fileview_key_press_event
1418 my ($self, $widget, $event) = @_;
1422 sub listview_get_first
1425 my @selected = $list->get_selected_indices();
1426 if (@selected > 0) {
1427 my ($name, @other) = @{$list->{data}->[$selected[0]]};
1428 return (unpack('u', $name), @other);
1434 sub listview_get_all
1438 my @selected = $list->get_selected_indices();
1440 for my $i (@selected) {
1441 my ($name, @other) = @{$list->{data}->[$i]};
1442 push @ret, [unpack('u', $name), @other];
1450 my ($list, $name, @other) = @_;
1451 push @{$list->{data}}, [pack('u', $name), @other];
1454 #----------------------------------------------------------------------
1455 # Handle keypress in file-view
1456 # * Translates backspace into a 'cd ..' command
1457 # * All other key presses left for GTK
1459 sub on_fileview_key_release_event
1461 my ($self, $widget, $event) = @_;
1462 if (not $event->keyval)
1466 if ($event->keyval == $Gtk2::Gdk::Keysyms{BackSpace}) {
1468 return 1; # eat keypress
1471 return 0; # let gtk have keypress
1474 sub on_forward_keypress
1479 #----------------------------------------------------------------------
1480 # Handle double-click (or enter) on file-view
1481 # * Translates into a 'cd <dir>' command
1483 sub on_fileview_row_activated
1485 my ($self, $widget) = @_;
1487 my ($name, undef, $type, undef) = listview_get_first($widget);
1491 if ($self->{cwd} eq '')
1493 $self->ch_dir($name);
1495 elsif ($self->{cwd} eq '/')
1497 $self->ch_dir('/' . $name);
1501 $self->ch_dir($self->{cwd} . '/' . $name);
1505 $self->fill_infoview($self->{cwd}, $name);
1508 return 1; # consume event
1513 my ($self, $path, $file) = @_;
1514 $self->clear_infoview();
1515 my @v = get_all_file_versions($self->{dbh},
1518 $self->current_client,
1519 $self->{pref}->{see_all_versions});
1521 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
1523 my $icon = ($inchanger)?$yesicon:$noicon;
1525 $mtime = localtime($mtime) ;
1527 listview_push($self->{fileinfo},
1528 $file, $jobid, 'file',
1529 $icon, $volname, $jobid, human($size), $mtime, $md5);
1536 return $self->{restore_backup_combobox}->get_active_text;
1542 return $self->{client_combobox}->get_active_text;
1545 sub on_list_backups_changed
1547 my ($self, $widget) = @_;
1548 return 0 unless defined $self->{fileview};
1550 $self->{CurrentJobIds} = [
1551 set_job_ids_for_date($self->{dbh},
1552 $self->current_client,
1553 $self->current_date,
1554 $self->{pref}->{use_ok_bkp_only})
1557 $self->refresh_fileview();
1561 sub on_restore_list_keypress
1563 my ($self, $widget, $event) = @_;
1564 if ($event->keyval == $Gtk2::Gdk::Keysyms{Delete})
1566 my @sel = $widget->get_selected_indices;
1567 foreach my $elt (reverse(sort {$a <=> $b} @sel))
1569 splice @{$self->{restore_list}->{data}},$elt,1;
1574 sub on_fileview_button_press_event
1576 my ($self,$widget,$event) = @_;
1577 if ($event->button == 3)
1579 $self->on_right_click_filelist($widget,$event);
1583 if ($event->button == 2)
1585 $self->on_see_all_version();
1592 sub on_see_all_version
1596 my @lst = listview_get_all($self->{fileview});
1599 my ($name, undef) = @{$i};
1601 new DlgFileVersion($self->{dbh},
1602 $self->current_client,
1603 $self->{cwd}, $name);
1607 sub on_right_click_filelist
1609 my ($self,$widget,$event) = @_;
1610 # I need to know what's selected
1611 my @sel = listview_get_all($self->{fileview});
1616 $type = $sel[0]->[2]; # $type
1621 if (@sel >=2 or $type eq 'dir')
1623 # We have selected more than one or it is a directories
1624 $w = $self->{filelist_dir_menu};
1628 $w = $self->{filelist_file_menu};
1634 $event->button, $event->time);
1637 sub context_add_to_filelist
1641 my @sel = listview_get_all($self->{fileview});
1643 foreach my $i (@sel)
1645 my ($file, $jobid, $type, undef) = @{$i};
1646 $file = $self->{cwd} . '/' . $file;
1647 $self->add_selected_file_to_list($file, $jobid, $type);
1651 # Adds a file to the filelist
1652 sub add_selected_file_to_list
1654 my ($self, $name, $jobid, $type)=@_;
1656 my $dbh = $self->{dbh};
1657 my $restore_list = $self->{restore_list};
1659 my $curjobids=join(',', @{$self->{CurrentJobIds}});
1666 if ($name and substr $name,-1 ne '/')
1668 $name .= '/'; # For bacula
1670 my $dirfileindex = get_fileindex_from_dir_jobid($dbh,$name,$jobid);
1671 listview_push($restore_list,
1672 $name, $jobid, 'dir', $curjobids,
1673 $diricon, $name,$jobid,$dirfileindex);
1675 elsif ($type eq 'file')
1677 my $fileindex = get_fileindex_from_file_jobid($dbh,$name,$jobid);
1679 listview_push($restore_list,
1680 $name, $jobid, 'file', $curjobids,
1681 $fileicon, $name, $jobid, $fileindex );
1685 # TODO : we want be able to restore files from a bad ended backup
1686 # we have JobStatus IN ('T', 'A', 'E') and we must
1688 # Data acces subs from here. Interaction with SGBD and caching
1690 # This sub retrieves the list of jobs corresponding to the jobs selected in the
1691 # GUI and stores them in @CurrentJobIds
1692 sub set_job_ids_for_date
1694 my ($dbh, $client, $date, $only_ok)=@_;
1696 if (!$client or !$date) {
1700 my $status = get_wanted_job_status($only_ok);
1702 # The algorithm : for a client, we get all the backups for each
1703 # fileset, in reverse order Then, for each fileset, we store the 'good'
1704 # incrementals and differentials until we have found a full so it goes
1705 # like this : store all incrementals until we have found a differential
1706 # or a full, then find the full #
1708 my $query = "SELECT JobId, FileSet, Level, JobStatus
1709 FROM Job, Client, FileSet
1710 WHERE Job.ClientId = Client.ClientId
1711 AND FileSet.FileSetId = Job.FileSetId
1712 AND EndTime <= '$date'
1713 AND Client.Name = '$client'
1715 AND JobStatus IN ($status)
1716 ORDER BY FileSet, JobTDate DESC";
1718 print $query,"\n" if $debug;
1720 my $result = $dbh->selectall_arrayref($query);
1722 foreach my $refrow (@$result)
1724 my $jobid = $refrow->[0];
1725 my $fileset = $refrow->[1];
1726 my $level = $refrow->[2];
1728 defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
1730 next if $progress{$fileset} eq 'F'; # It's over for this fileset...
1734 next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
1735 push @CurrentJobIds,($jobid);
1737 elsif ($level eq 'D')
1739 next if $progress{$fileset} eq 'D'; # We allready have a differential
1740 push @CurrentJobIds,($jobid);
1742 elsif ($level eq 'F')
1744 push @CurrentJobIds,($jobid);
1747 my $status = $refrow->[3] ;
1748 if ($status eq 'T') { # good end of job
1749 $progress{$fileset} = $level;
1752 print Data::Dumper::Dumper(\@CurrentJobIds) if $debug;
1754 return @CurrentJobIds;
1757 # Lists all directories contained inside a directory.
1758 # Uses the current dir, the client name, and CurrentJobIds for visibility.
1759 # Returns an array of dirs
1762 my ($self,$dir,$client)=@_;
1763 print "list_dirs($dir, $client)\n";
1765 # Is data allready cached ?
1766 if (not $self->{dirtree}->{$client})
1768 $self->cache_dirs($client);
1771 if ($dir ne '' and substr $dir,-1 ne '/')
1773 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1775 # Here, the tree is cached in ram
1776 my @dir = split('/',$dir,-1);
1777 pop @dir; # We don't need the empty trailing element
1779 # We have to get the reference of the hash containing $dir contents
1781 my $refdir=$self->{dirtree}->{$client};
1784 foreach my $subdir (@dir)
1790 $refdir = $refdir->[0]->{$subdir};
1793 # We reached the directory
1796 foreach my $dir (sort(keys %{$refdir->[0]}))
1798 # We return the directory's content : only visible directories
1799 foreach my $jobid (reverse(sort(@{$self->{CurrentJobIds}})))
1801 if (defined $refdir->[0]->{$dir}->[1]->{$jobid})
1803 my $dirname = $refdir->[0]->{$dir}->[2]; # The real dirname...
1804 push @return_list,($dirname);
1805 next DIRLOOP; # No need to waste more CPU cycles...
1809 print "LIST DIR : ", Data::Dumper::Dumper(\@return_list),"\n";
1810 return @return_list;
1814 # List all files in a directory. dir as parameter, CurrentJobIds for visibility
1815 # Returns an array of dirs
1818 my ($self, $dir)=@_;
1819 my $dbh = $self->{dbh};
1823 print "list_files($dir)\n";
1825 if ($dir ne '' and substr $dir,-1 ne '/')
1827 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1830 my $query = "SELECT Path.PathId FROM Path WHERE Path.Path = '$dir'";
1831 print $query,"\n" if $debug;
1833 my $result = $dbh->selectall_arrayref($query);
1834 foreach my $refrow (@$result)
1836 push @list_pathid,($refrow->[0]);
1839 if (@list_pathid == 0)
1841 print "No pathid found for $dir\n" if $debug;
1845 my $inlistpath = join (',', @list_pathid);
1846 my $inclause = join (',', @{$self->{CurrentJobIds}});
1847 if ($inclause eq '')
1853 "SELECT listfiles.id, listfiles.Name, File.LStat, File.JobId
1855 (SELECT Filename.Name, max(File.FileId) as id
1857 WHERE File.FilenameId = Filename.FilenameId
1858 AND Filename.Name != ''
1859 AND File.PathId IN ($inlistpath)
1860 AND File.JobId IN ($inclause)
1861 GROUP BY Filename.Name
1862 ORDER BY Filename.Name) AS listfiles,
1864 WHERE File.FileId = listfiles.id";
1866 print $query,"\n" if $debug;
1867 $result = $dbh->selectall_arrayref($query);
1874 Gtk2->main_iteration while (Gtk2->events_pending);
1877 # For the dirs, because of the db schema, it's inefficient to get the
1878 # directories contained inside other directories (regexp match or tossing
1879 # lots of records...). So we load all the tree and cache it. The data is
1880 # stored in a structure of this form :
1881 # Each directory is an array.
1882 # - In this array, the first element is a ref to next dir (hash)
1883 # - The second element is a hash containing all jobids pointing
1884 # on an array containing their lstat (or 1 if this jobid is there because
1886 # - The third is the filename itself (it could get mangled because of
1889 # So it looks like this :
1890 # $reftree->[ { 'dir1' => $refdir1
1891 # 'dir2' => $refdir2
1894 # { 'jobid1' => 'lstat1',
1895 # 'jobid2' => 'lstat2',
1896 # 'jobid3' => 1 # This one is here for "visibility"
1901 # Client as a parameter
1902 # Returns an array of dirs
1905 my ($self, $client) = @_;
1906 print "cache_dirs()\n";
1908 $self->{dirtree}->{$client} = []; # reset cache
1909 my $dbh = $self->{dbh};
1911 # TODO : If we get here, things could get lenghty ... draw a popup window .
1912 my $widget = Gtk2::MessageDialog->new($self->{mainwin},
1913 'destroy-with-parent',
1915 'Populating cache');
1918 # We have to build the tree, as it's the first time it is asked...
1921 # First, we only need the jobids of the selected server.
1922 # It's not the same as @CurrentJobIds (we need ALL the jobs)
1923 # We get the JobIds first in order to have the best execution
1924 # plan possible for the big query, with an in clause.
1926 my $status = get_wanted_job_status($self->{pref}->{use_ok_bkp_only});
1930 WHERE Job.ClientId = Client.ClientId
1931 AND Client.Name = '$client'
1932 AND Job.JobStatus IN ($status)
1933 AND Job.Type = 'B'";
1935 print $query,"\n" if $debug;
1936 my $result = $dbh->selectall_arrayref($query);
1940 foreach my $record (@{$result})
1942 push @jobids,($record->[0]);
1944 my $inclause = join(',',@jobids);
1945 if ($inclause eq '')
1948 $self->set_status("No previous backup found for $client");
1952 # Then, still to help dear mysql, we'll retrieve the PathId from empty Path (directory entries...)
1955 "SELECT Filename.FilenameId FROM Filename WHERE Filename.Name=''";
1957 print $query,"\n" if $debug;
1958 $result = $dbh->selectall_arrayref($query);
1961 foreach my $record (@{$result})
1963 push @dirids,$record->[0];
1965 my $dirinclause = join(',',@dirids);
1967 # This query is a bit complicated :
1968 # whe need to find all dir entries that should be displayed, even
1969 # if the directory itself has no entry in File table (it means a file
1970 # is explicitely chosen in the backup configuration)
1971 # Here's what I wanted to do :
1974 # SELECT T1.Path, T2.Lstat, T2.JobId
1975 # FROM ( SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
1976 # WHERE File.PathId = Path.PathId
1977 # AND File.JobId IN ($inclause)) AS T1
1979 # ( SELECT File.Lstat, File.JobId, File.PathId FROM File
1980 # WHERE File.FilenameId IN ($dirinclause)
1981 # AND File.JobId IN ($inclause)) AS T2
1982 # ON (T1.PathId = T2.PathId)
1984 # It works perfectly with postgresql, but mysql doesn't seem to be able
1985 # to do the hash join correcty, so the performance sucks.
1986 # So it will be done in 4 steps :
1987 # o create T1 and T2 as temp tables
1988 # o create an index on T2.PathId
1990 # o remove the temp tables
1992 CREATE TEMPORARY TABLE T1 AS
1993 SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
1994 WHERE File.PathId = Path.PathId
1995 AND File.JobId IN ($inclause)
1997 print $query,"\n" if $debug;
2002 CREATE TEMPORARY TABLE T2 AS
2003 SELECT File.Lstat, File.JobId, File.PathId FROM File
2004 WHERE File.FilenameId IN ($dirinclause)
2005 AND File.JobId IN ($inclause)
2007 print $query,"\n" if $debug;
2012 CREATE INDEX tmp2 ON T2(PathId)
2014 print $query,"\n" if $debug;
2019 SELECT T1.Path, T2.Lstat, T2.JobId
2020 FROM T1 LEFT JOIN T2
2021 ON (T1.PathId = T2.PathId)
2023 print $query,"\n" if $debug;
2024 $result = $dbh->selectall_arrayref($query);
2028 foreach my $record (@{$result})
2030 if ($rcount > 15000) {
2036 # Dirty hack to force the string encoding on perl... we don't
2037 # want implicit conversions
2038 my $path = pack "U0C*", unpack "C*",$record->[0];
2040 my @path = split('/',$path,-1);
2041 pop @path; # we don't need the trailing empty element
2042 my $lstat = $record->[1];
2043 my $jobid = $record->[2];
2045 # We're going to store all the data on the cache tree.
2046 # We find the leaf, then store data there
2047 my $reftree=$self->{dirtree}->{$client};
2048 foreach my $dir(@path)
2054 if (not defined($reftree->[0]->{$dir}))
2057 $reftree->[0]->{$dir}=\@tmparray;
2059 $reftree=$reftree->[0]->{$dir};
2062 # We can now add the metadata for this dir ...
2064 # $result = $dbh->selectall_arrayref($query);
2067 # contains something
2068 $reftree->[1]->{$jobid}=$lstat;
2072 # We have a very special case here...
2073 # lstat is not defined.
2074 # it means the directory is there because a file has been
2075 # backuped. so the dir has no entry in File table.
2076 # That's a rare case, so we can afford to determine it's
2077 # visibility with a query
2078 my $select_path=$record->[0];
2079 $select_path=$dbh->quote($select_path); # gotta be careful
2083 WHERE File.PathId = Path.PathId
2084 AND Path.Path = $select_path
2086 print $query,"\n" if $debug;
2087 my $result2 = $dbh->selectall_arrayref($query);
2088 foreach my $record (@{$result2})
2090 my $jobid=$record->[0];
2091 $reftree->[1]->{$jobid}=1;
2099 print $query,"\n" if $debug;
2104 print $query,"\n" if $debug;
2108 list_visible($self->{dirtree}->{$client});
2111 # print Data::Dumper::Dumper($self->{dirtree});
2114 # Recursive function to calculate the visibility of each directory in the cache
2115 # tree Working with references to save time and memory
2116 # For each directory, we want to propagate it's visible jobids onto it's
2117 # parents directory.
2118 # A tree is visible if
2119 # - it's been in a backup pointed by the CurrentJobIds
2120 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2121 # In the second case, the directory is visible but has no metadata.
2122 # We symbolize this with lstat = 1 for this jobid in the cache.
2124 # Input : reference directory
2125 # Output : visibility of this dir. Has to know visibility of all subdirs
2126 # to know it's visibility, hence the recursing.
2132 # Get the subdirs array references list
2133 my @list_ref_subdirs;
2134 while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2136 push @list_ref_subdirs,($ref_subdir);
2139 # Now lets recurse over these subdirs and retrieve the reference of a hash
2140 # containing the jobs where they are visible
2141 foreach my $ref_subdir (@list_ref_subdirs)
2143 my $ref_list_jobs = list_visible($ref_subdir);
2144 foreach my $jobid (keys %$ref_list_jobs)
2146 $visibility{$jobid}=1;
2150 # Ok. Now, we've got the list of those jobs. We are going to update our
2151 # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2152 # the lstat for the known jobids. Put 1 in the new elements... But first,
2153 # let's store the current jobids
2155 foreach my $jobid (keys %{$refdir->[1]})
2157 push @known_jobids,($jobid);
2161 foreach my $jobid (keys %visibility)
2163 next if ($refdir->[1]->{$jobid});
2164 $refdir->[1]->{$jobid} = 1;
2166 # Add the known_jobids to %visibility
2167 foreach my $jobid (@known_jobids)
2169 $visibility{$jobid}=1;
2171 return \%visibility;
2174 # Returns the list of media required for a list of jobids.
2175 # Input : dbh, jobid1, jobid2...
2176 # Output : reference to array of (joibd, inchanger)
2177 sub get_required_media_from_jobid
2179 my ($dbh, @jobids)=@_;
2180 my $inclause = join(',',@jobids);
2182 SELECT DISTINCT JobMedia.MediaId, Media.InChanger
2183 FROM JobMedia, Media
2184 WHERE JobMedia.MediaId=Media.MediaId
2185 AND JobId In ($inclause)
2187 my $result = $dbh->selectall_arrayref($query);
2191 # Returns the fileindex from dirname and jobid.
2192 # Input : dbh, dirname, jobid
2193 # Output : fileindex
2194 sub get_fileindex_from_dir_jobid
2196 my ($dbh, $dirname, $jobid)=@_;
2198 $query = "SELECT File.FileIndex
2199 FROM File, Filename, Path
2200 WHERE File.FilenameId = Filename.FilenameId
2201 AND File.PathId = Path.PathId
2202 AND Filename.Name = ''
2203 AND Path.Path = '$dirname'
2204 AND File.JobId = '$jobid'
2207 print $query,"\n" if $debug;
2208 my $result = $dbh->selectall_arrayref($query);
2209 return $result->[0]->[0];
2212 # Returns the fileindex from filename and jobid.
2213 # Input : dbh, filename, jobid
2214 # Output : fileindex
2215 sub get_fileindex_from_file_jobid
2217 my ($dbh, $filename, $jobid)=@_;
2219 my @dirs = File::Spec->splitdir ($filename);
2220 $filename=pop(@dirs);
2221 my $dirname = File::Spec->catdir(@dirs) . '/';
2226 "SELECT File.FileIndex
2227 FROM File, Filename, Path
2228 WHERE File.FilenameId = Filename.FilenameId
2229 AND File.PathId = Path.PathId
2230 AND Filename.Name = '$filename'
2231 AND Path.Path = '$dirname'
2232 AND File.JobId = '$jobid'";
2234 print $query,"\n" if $debug;
2235 my $result = $dbh->selectall_arrayref($query);
2236 return $result->[0]->[0];
2240 # Returns list of versions of a file that could be restored
2241 # returns an array of
2242 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2243 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2244 # and volname and md5
2245 # and of course, there will be only one jobid in the array of jobids...
2246 sub get_all_file_versions
2248 my ($dbh,$path,$file,$client,$see_all)=@_;
2250 defined $see_all or $see_all=0;
2255 "SELECT File.JobId, File.FileIndex, File.Lstat,
2256 File.Md5, Media.VolumeName, Media.InChanger
2257 FROM File, Filename, Path, Job, Client, JobMedia, Media
2258 WHERE File.FilenameId = Filename.FilenameId
2259 AND File.PathId=Path.PathId
2260 AND File.JobId = Job.JobId
2261 AND Job.ClientId = Client.ClientId
2262 AND Job.JobId = JobMedia.JobId
2263 AND File.FileIndex >= JobMedia.FirstIndex
2264 AND File.FileIndex <= JobMedia.LastIndex
2265 AND JobMedia.MediaId = Media.MediaId
2266 AND Path.Path = '$path'
2267 AND Filename.Name = '$file'
2268 AND Client.Name = '$client'";
2270 print $query if $debug;
2272 my $result = $dbh->selectall_arrayref($query);
2274 foreach my $refrow (@$result)
2276 my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2277 my @attribs = parse_lstat($lstat);
2278 my $mtime = array_attrib('st_mtime',\@attribs);
2279 my $size = array_attrib('st_size',\@attribs);
2281 my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2282 $inchanger, $md5, $volname);
2283 push @versions, (\@list);
2286 # We have the list of all versions of this file.
2287 # We'll sort it by mtime desc, size, md5, inchanger desc
2288 # the rest of the algorithm will be simpler
2289 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2290 @versions = sort { $b->[4] <=> $a->[4]
2291 || $a->[5] <=> $b->[5]
2292 || $a->[7] cmp $a->[7]
2293 || $b->[6] <=> $a->[6]} @versions;
2296 my %allready_seen_by_mtime;
2297 my %allready_seen_by_md5;
2298 # Now we should create a new array with only the interesting records
2299 foreach my $ref (@versions)
2303 # The file has a md5. We compare his md5 to other known md5...
2304 # We take size into account. It may happen that 2 files
2305 # have the same md5sum and are different. size is a supplementary
2308 # If we allready have a (better) version
2309 next if ( (not $see_all)
2310 and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
2312 # we never met this one before...
2313 $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2315 # Even if it has a md5, we should also work with mtimes
2316 # We allready have a (better) version
2317 next if ( (not $see_all)
2318 and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
2319 $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2321 # We reached there. The file hasn't been seen.
2322 push @good_versions,($ref);
2325 # To be nice with the user, we re-sort good_versions by
2326 # inchanger desc, mtime desc
2327 @good_versions = sort { $b->[4] <=> $a->[4]
2328 || $b->[2] <=> $a->[2]} @good_versions;
2330 return @good_versions;
2333 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2334 # This sub creates a BSR from the information in the restore_list
2335 # Returns the BSR as a string
2339 my $dbh = $self->{dbh};
2341 # This query gets all jobid/jobmedia/media combination.
2343 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile,
2344 JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2345 JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex,
2346 Media.Volumename, Media.MediaType
2347 FROM Job, JobMedia, Media
2348 WHERE Job.JobId = JobMedia.JobId
2349 AND JobMedia.MediaId = Media.MediaId
2350 ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2353 my $result = $dbh->selectall_arrayref($query);
2355 # We will store everything hashed by jobid.
2357 foreach my $refrow (@$result)
2359 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2360 $firstindex, $lastindex, $startblock, $endblock,
2361 $volindex, $volumename, $mediatype) = @{$refrow};
2363 # We just have to deal with the case where starfile != endfile
2364 # In this case, we concatenate both, for the bsr
2365 if ($startfile != $endfile) {
2366 $startfile = $startfile . '-' . $endfile;
2370 ($jobid, $volsessionid, $volsessiontime, $startfile,
2371 $firstindex, $lastindex, $startblock .'-'. $endblock,
2372 $volindex, $volumename, $mediatype);
2374 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2378 # reminder : restore_list looks like this :
2379 # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2381 # Here, we retrieve every file/dir that could be in the restore
2382 # We do as simple as possible for the SQL engine (no crazy joins,
2383 # no pseudo join (>= FirstIndex ...), etc ...
2384 # We do a SQL union of all the files/dirs specified in the restore_list
2386 foreach my $entry (@{$self->{restore_list}->{data}})
2388 if ($entry->[2] eq 'dir')
2390 my $dir = unpack('u', $entry->[0]);
2391 my $inclause = $entry->[3]; #curjobids
2394 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2395 FROM File, Path, Filename
2396 WHERE Path.PathId = File.PathId
2397 AND File.FilenameId = Filename.FilenameId
2398 AND Path.Path LIKE '$dir%'
2399 AND File.JobId IN ($inclause) )";
2400 push @select_queries,($query);
2404 # It's a file. Great, we allready have most
2405 # of what is needed. Simple and efficient query
2406 my $file = unpack('u', $entry->[0]);
2407 my @file = split '/',$file;
2409 my $dir = join('/',@file);
2411 my $jobid = $entry->[1];
2412 my $fileindex = $entry->[7];
2413 my $inclause = $entry->[3]; # curjobids
2415 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2416 FROM File, Path, Filename
2417 WHERE Path.PathId = File.PathId
2418 AND File.FilenameId = Filename.FilenameId
2419 AND Path.Path = '$dir/'
2420 AND Filename.Name = '$file'
2421 AND File.JobId = $jobid)";
2422 push @select_queries,($query);
2425 $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2427 print $query,"\n" if $debug;
2429 #Now we run the query and parse the result...
2430 # there may be a lot of records, so we better be efficient
2431 # We use the bind column method, working with references...
2433 my $sth = $dbh->prepare($query);
2436 my ($path,$name,$fileindex,$jobid);
2437 $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2439 # The temp place we're going to save all file
2440 # list to before the real list
2444 while ($sth->fetchrow_arrayref())
2446 # This may look dumb, but we're going to do a join by ourselves,
2447 # to save memory and avoid sending a complex query to mysql
2448 my $complete_path = $path . $name;
2456 # Remove trailing slash (normalize file and dir name)
2457 $complete_path =~ s/\/$//;
2459 # Let's find the ref(s) for the %mediainfo element(s)
2460 # containing the data for this file
2461 # There can be several matches. It is the pseudo join.
2463 my $max_elt=@{$mediainfos{$jobid}}-1;
2465 while($med_idx <= $max_elt)
2467 my $ref = $mediainfos{$jobid}->[$med_idx];
2468 # First, can we get rid of the first elements of the
2469 # array ? (if they don't contain valuable records
2471 if ($fileindex > $ref->[5])
2473 # It seems we don't need anymore
2474 # this entry in %mediainfo (the input data
2477 shift @{$mediainfos{$jobid}};
2481 # We will do work on this elt. We can ++
2482 # $med_idx for next loop
2485 # %mediainfo row looks like :
2486 # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2487 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2490 # We are in range. We store and continue looping
2492 if ($fileindex >= $ref->[4])
2494 my @data = ($complete_path,$is_dir,
2496 push @temp_list,(\@data);
2500 # We are not in range. No point in continuing looping
2501 # We go to next record.
2505 # Now we have the array.
2506 # We're going to sort it, by
2507 # path, volsessiontime DESC (get the most recent file...)
2508 # The array rows look like this :
2509 # complete_path,is_dir,fileindex,
2510 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2511 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2512 @temp_list = sort {$a->[0] cmp $b->[0]
2513 || $b->[3]->[2] <=> $a->[3]->[2]
2517 my $prev_complete_path='////'; # Sure not to match
2521 while (my $refrow = shift @temp_list)
2523 # For the sake of readability, we load $refrow
2524 # contents in real scalars
2525 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2526 my $jobid= $refother->[0]; # We don't need the rest...
2528 # We skip this entry.
2529 # We allready have a newer one and this
2530 # isn't a continuation of the same file
2531 next if ($complete_path eq $prev_complete_path
2532 and $jobid != $prev_jobid);
2536 and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2538 # We would be recursing inside a file.
2539 # Just what we don't want (dir replaced by file
2540 # between two backups
2546 push @restore_list,($refrow);
2548 $prev_complete_path = $complete_path;
2549 $prev_jobid = $jobid;
2555 push @restore_list,($refrow);
2557 $prev_complete_path = $complete_path;
2558 $prev_jobid = $jobid;
2562 # We get rid of @temp_list... save memory
2565 # Ok everything is in the list. Let's sort it again in another way.
2566 # This time it will be in the bsr file order
2568 # we sort the results by
2569 # volsessiontime, volsessionid, volindex, fileindex
2570 # to get all files in right order...
2571 # Reminder : The array rows look like this :
2572 # complete_path,is_dir,fileindex,
2573 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2574 # StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2576 @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2]
2577 || $a->[3]->[1] <=> $b->[3]->[1]
2578 || $a->[3]->[7] <=> $b->[3]->[7]
2579 || $a->[2] <=> $b->[2] }
2582 # Now that everything is ready, we create the bsr
2583 my $prev_fileindex=-1;
2584 my $prev_volsessionid=-1;
2585 my $prev_volsessiontime=-1;
2586 my $prev_volumename=-1;
2587 my $prev_volfile=-1;
2591 my $first_of_current_range=0;
2592 my @fileindex_ranges;
2595 foreach my $refrow (@restore_list)
2597 my (undef,undef,$fileindex,$refother)=@{$refrow};
2598 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2599 $volblocks,undef,$volumename,$mediatype)=@{$refother};
2601 # We can specifiy the number of files in each section of the
2602 # bsr to speedup restore (bacula can then jump over the
2603 # end of tape files.
2607 if ($prev_volumename eq '-1')
2609 # We only have to start the new range...
2610 $first_of_current_range=$fileindex;
2612 elsif ($prev_volsessionid != $volsessionid
2613 or $prev_volsessiontime != $volsessiontime
2614 or $prev_volumename ne $volumename
2615 or $prev_volfile ne $volfile)
2617 # We have to create a new section in the bsr...
2618 # We print the previous one ...
2619 # (before that, save the current range ...)
2620 if ($first_of_current_range != $prev_fileindex)
2623 push @fileindex_ranges,
2624 ("$first_of_current_range-$prev_fileindex");
2628 # We are out of a range,
2629 # but there is only one element in the range
2630 push @fileindex_ranges,
2631 ("$first_of_current_range");
2634 $bsr.=print_bsr_section(\@fileindex_ranges,
2636 $prev_volsessiontime,
2643 # Reset for next loop
2644 @fileindex_ranges=();
2645 $first_of_current_range=$fileindex;
2647 elsif ($fileindex-1 != $prev_fileindex)
2649 # End of a range of fileindexes
2650 if ($first_of_current_range != $prev_fileindex)
2653 push @fileindex_ranges,
2654 ("$first_of_current_range-$prev_fileindex");
2658 # We are out of a range,
2659 # but there is only one element in the range
2660 push @fileindex_ranges,
2661 ("$first_of_current_range");
2663 $first_of_current_range=$fileindex;
2665 $prev_fileindex=$fileindex;
2666 $prev_volsessionid = $volsessionid;
2667 $prev_volsessiontime = $volsessiontime;
2668 $prev_volumename = $volumename;
2669 $prev_volfile=$volfile;
2670 $prev_mediatype=$mediatype;
2671 $prev_volblocks=$volblocks;
2675 # Ok, we're out of the loop. Alas, there's still the last record ...
2676 if ($first_of_current_range != $prev_fileindex)
2679 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2684 # We are out of a range,
2685 # but there is only one element in the range
2686 push @fileindex_ranges,("$first_of_current_range");
2689 $bsr.=print_bsr_section(\@fileindex_ranges,
2691 $prev_volsessiontime,
2701 sub print_bsr_section
2703 my ($ref_fileindex_ranges,$volsessionid,
2704 $volsessiontime,$volumename,$volfile,
2705 $mediatype,$volblocks,$count)=@_;
2708 $bsr .= "Volume=\"$volumename\"\n";
2709 $bsr .= "MediaType=\"$mediatype\"\n";
2710 $bsr .= "VolSessionId=$volsessionid\n";
2711 $bsr .= "VolSessionTime=$volsessiontime\n";
2712 $bsr .= "VolFile=$volfile\n";
2713 $bsr .= "VolBlock=$volblocks\n";
2715 foreach my $range (@{$ref_fileindex_ranges})
2717 $bsr .= "FileIndex=$range\n";
2720 $bsr .= "Count=$count\n";
2724 # This function estimates the size to be restored for an entry of the restore
2726 # In : self,reference to the entry
2727 # Out : size in bytes, number of files
2728 sub estimate_restore_size
2730 # reminder : restore_list looks like this :
2731 # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2735 my $dbh = $self->{dbh};
2736 if ($entry->[2] eq 'dir')
2738 my $dir = unpack('u', $entry->[0]);
2739 my $inclause = $entry->[3]; #curjobids
2741 "SELECT Path.Path, File.FilenameId, File.LStat
2742 FROM File, Path, Job
2743 WHERE Path.PathId = File.PathId
2744 AND File.JobId = Job.JobId
2745 AND Path.Path LIKE '$dir%'
2746 AND File.JobId IN ($inclause)
2747 ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2751 # It's a file. Great, we allready have most
2752 # of what is needed. Simple and efficient query
2753 my $file = unpack('u', $entry->[0]);
2754 my @file = split '/',$file;
2756 my $dir = join('/',@file);
2758 my $jobid = $entry->[1];
2759 my $fileindex = $entry->[7];
2760 my $inclause = $entry->[3]; # curjobids
2762 "SELECT Path.Path, File.FilenameId, File.Lstat
2763 FROM File, Path, Filename
2764 WHERE Path.PathId = File.PathId
2765 AND Path.Path = '$dir/'
2766 AND Filename.Name = '$file'
2767 AND File.JobId = $jobid
2768 AND Filename.FilenameId = File.FilenameId";
2771 print $query,"\n" if $debug;
2772 my ($path,$nameid,$lstat);
2773 my $sth = $dbh->prepare($query);
2775 $sth->bind_columns(\$path,\$nameid,\$lstat);
2785 while ($sth->fetchrow_arrayref())
2787 # Only the latest version of a file
2788 next if ($nameid eq $old_nameid and $path eq $old_path);
2790 if ($rcount > 15000) {
2797 # We get the size of this file
2798 my $size=lstat_attrib($lstat,'st_size');
2799 $total_size += $size;
2802 $old_nameid=$nameid;
2804 return ($total_size,$total_files);
2810 my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
2811 'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
2812 'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
2813 'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
2814 'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
2815 'data_stream' => 15);;
2818 my ($attrib,$ref_attrib)=@_;
2819 return $ref_attrib->[$attrib_name_id{$attrib}];
2823 { # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
2825 my ($file, $attrib)=@_;
2827 if (defined $attrib_name_id{$attrib}) {
2829 my @d = split(' ', $file->[2]) ; # TODO : cache this
2831 return from_base64($d[$attrib_name_id{$attrib}]);
2833 } elsif ($attrib eq 'jobid') {
2837 } elsif ($attrib eq 'name') {
2842 die "Attribute not known : $attrib.\n";
2846 # Return the jobid or attribute asked for a dir
2849 my ($self,$dir,$attrib)=@_;
2851 my @dir = split('/',$dir,-1);
2852 my $refdir=$self->{dirtree}->{$self->current_client};
2854 if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
2856 die "Attribute not known : $attrib.\n";
2859 foreach my $subdir (@dir)
2861 $refdir = $refdir->[0]->{$subdir};
2864 # $refdir is now the reference to the dir's array
2865 # Is the a jobid in @CurrentJobIds where the lstat is
2866 # defined (we'll search in reverse order)
2867 foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
2869 if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
2871 if ($attrib eq 'jobid')
2877 my @attribs = parse_lstat($refdir->[1]->{$jobid});
2878 return $attribs[$attrib_name_id{$attrib}+1];
2883 return 0; # We cannot get a good attribute.
2884 # This directory is here for the sake of visibility
2889 my ($lstat,$attrib)=@_;
2890 if (defined $attrib_name_id{$attrib})
2892 my @d = split(' ', $lstat) ; # TODO : cache this
2893 return from_base64($d[$attrib_name_id{$attrib}]);
2899 # Base 64 functions, directly from recover.pl.
2901 # Karl Hakimian <hakimian@aha.com>
2902 # This section is also under GPL v2 or later.
2909 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
2910 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
2911 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
2912 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
2913 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
2915 @base64_map = (0) x 128;
2917 for (my $i=0; $i<64; $i++) {
2918 $base64_map[ord($base64_digits[$i])] = $i;
2933 if (substr($where, 0, 1) eq '-') {
2935 $where = substr($where, 1);
2938 while ($where ne '') {
2940 my $d = substr($where, 0, 1);
2941 $val += $base64_map[ord(substr($where, 0, 1))];
2942 $where = substr($where, 1);
2950 my @attribs = split(' ',$lstat);
2951 foreach my $element (@attribs)
2953 $element = from_base64($element);
2960 ################################################################
2964 my $conf = "$ENV{HOME}/.brestore.conf" ;
2965 my $p = new Pref($conf);
2971 $glade_file = $p->{glade_file};
2973 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
2974 if (-f "$path/$glade_file") {
2975 $glade_file = "$path/$glade_file" ;
2980 if ( -f $glade_file) {
2981 my $w = new DlgResto($p);
2984 my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close',
2985 "Can't find your brestore.glade (glade_file => '$glade_file')
2986 Please, edit your $conf to setup it." );
2988 $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
2993 Gtk2->main; # Start Gtk2 main loop
3005 # Code pour trier les colonnes
3006 my $mod = $fileview->get_model();
3007 $mod->set_default_sort_func(sub {
3008 my ($model, $item1, $item2) = @_;
3009 my $a = $model->get($item1, 1); # récupération de la valeur de la 2ème
3010 my $b = $model->get($item2, 1); # colonne (indice 1)
3015 $fileview->set_headers_clickable(1);
3016 my $col = $fileview->get_column(1); # la colonne NOM, colonne numéro 2
3017 $col->signal_connect('clicked', sub {
3018 my ($colonne, $model) = @_;
3019 $model->set_sort_column_id (1, 'ascending');