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,
125 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
127 my $icon = ($inchanger)?$DlgResto::yesicon:$DlgResto::noicon;
129 DlgResto::listview_push($fileview,
130 $file, $jobid, 'file',
131 $icon, $volname, $jobid, $size,
132 scalar(localtime($mtime)), $md5);
135 $self->{version} = $glade_box->get_widget('dlg_version');
136 $self->{version}->show();
141 ################################################################
146 my ($package, $text) = @_;
150 my $glade = Gtk2::GladeXML->new($glade_file, "dlg_warn");
152 # Connect signals magically
153 $glade->signal_autoconnect_from_package($self);
154 $glade->get_widget('label_warn')->set_text($text);
158 $self->{window} = $glade->get_widget('dlg_warn');
159 $self->{window}->show_all();
166 $self->{window}->destroy();
170 ################################################################
176 # %arg = (bsr_file => '/path/to/bsr', # on director
177 # volumes => [ '00001', '00004']
183 my ($class, %arg) = @_;
186 bsr_file => $arg{bsr_file}, # /path/to/bsr on director
187 pref => $arg{pref}, # Pref ref
188 glade => undef, # GladeXML ref
189 bconsole => undef, # Bconsole ref
192 # we load launch widget of $glade_file
193 my $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file,
196 # Connect signals magically
197 $glade->signal_autoconnect_from_package($self);
199 my $widget = $glade->get_widget('volumeview');
200 my $volview = Gtk2::SimpleList->new_from_treeview(
202 'InChanger' => 'pixbuf',
206 my $infos = get_volume_inchanger($arg{pref}->{dbh}, $arg{volumes}) ;
208 # we replace 0 and 1 by $noicon and $yesicon
209 for my $i (@{$infos}) {
211 $i->[0] = $DlgResto::noicon;
213 $i->[0] = $DlgResto::yesicon;
218 push @{ $volview->{data} }, @{$infos} ;
220 my $console = $self->{bconsole} = new Bconsole(pref => $arg{pref});
222 # fill client combobox (with director defined clients
223 my @clients = $console->list_client() ; # get from bconsole
224 if ($console->{error}) {
225 new DlgWarn("Can't use bconsole:\n$arg{pref}->{bconsole}: $console->{error}") ;
227 my $w = $self->{combo_client} = $glade->get_widget('combo_launch_client') ;
228 $self->{list_client} = DlgResto::init_combo($w, 'text');
229 DlgResto::fill_combo($self->{list_client},
230 $DlgResto::client_list_empty,
234 # fill fileset combobox
235 my @fileset = $console->list_fileset() ;
236 $w = $self->{combo_fileset} = $glade->get_widget('combo_launch_fileset') ;
237 $self->{list_fileset} = DlgResto::init_combo($w, 'text');
238 DlgResto::fill_combo($self->{list_fileset}, '', @fileset);
241 my @job = $console->list_job() ;
242 $w = $self->{combo_job} = $glade->get_widget('combo_launch_job') ;
243 $self->{list_job} = DlgResto::init_combo($w, 'text');
244 DlgResto::fill_combo($self->{list_job}, '', @job);
246 # find default_restore_job in jobs list
247 my $default_restore_job = $arg{pref}->{default_restore_job} ;
251 if ($j =~ /$default_restore_job/io) {
257 $w->set_active($index);
259 # fill storage combobox
260 my @storage = $console->list_storage() ;
261 $w = $self->{combo_storage} = $glade->get_widget('combo_launch_storage') ;
262 $self->{list_storage} = DlgResto::init_combo($w, 'text');
263 DlgResto::fill_combo($self->{list_storage}, '', @storage);
265 $glade->get_widget('dlg_launch')->show_all();
272 my ($self, $client, $jobid) = @_;
274 my $ret = $self->{pref}->go_bweb("?action=dsp_cur_job;jobid=$jobid;client=$client", "view job status");
277 my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'info', 'close',
278 "Your job have been submited to bacula.
279 To follow it, you must use bconsole (or install/configure bweb)");
284 $self->on_cancel_resto_clicked();
287 sub on_cancel_resto_clicked
290 $self->{glade}->get_widget('dlg_launch')->destroy();
293 sub on_submit_resto_clicked
296 my $glade = $self->{glade};
298 my $r = $self->copy_bsr($self->{bsr_file}, $self->{pref}->{bsr_dest}) ;
301 new DlgWarn("Can't copy bsr file to director ($self->{error})");
305 my $fileset = $glade->get_widget('combo_launch_fileset')
308 my $storage = $glade->get_widget('combo_launch_storage')
311 my $where = $glade->get_widget('entry_launch_where')->get_text();
313 my $job = $glade->get_widget('combo_launch_job')
317 new DlgWarn("Can't use this job");
321 my $client = $glade->get_widget('combo_launch_client')
324 if (! $client or $client eq $DlgResto::client_list_empty) {
325 new DlgWarn("Can't use this client ($client)");
329 my $prio = $glade->get_widget('spin_launch_priority')->get_value();
331 my $replace = $glade->get_widget('chkbp_launch_replace')->get_active();
332 $replace=($replace)?'always':'never';
334 my $jobid = $self->{bconsole}->run(job => $job,
343 $self->show_job($client, $jobid);
346 sub on_combo_storage_button_press_event
349 print "on_combo_storage_button_press_event()\n";
352 sub on_combo_fileset_button_press_event
355 print "on_combo_fileset_button_press_event()\n";
359 sub on_combo_job_button_press_event
362 print "on_combo_job_button_press_event()\n";
365 sub get_volume_inchanger
367 my ($dbh, $vols) = @_;
369 my $lst = join(',', map { $dbh->quote($_) } @{ $vols } ) ;
371 my $rq = "SELECT InChanger, VolumeName
373 WHERE VolumeName IN ($lst)
376 my $res = $dbh->selectall_arrayref($rq);
377 return $res; # [ [ 1, VolName].. ]
381 use File::Copy qw/copy/;
382 use File::Basename qw/basename/;
384 # We must kown the path+filename destination
385 # $self->{error} contains error message
386 # it return 0/1 if fail/success
389 my ($self, $src, $dst) = @_ ;
390 print "$src => $dst\n"
397 if ($dst =~ m!file:/(/.+)!) {
398 $ret = copy($src, $1);
400 $dstfile = "$1/" . basename($src) ;
402 } elsif ($dst =~ m!scp://([^:]+:(.+))!) {
403 $err = `scp $src $1 2>&1` ;
405 $dstfile = "$2/" . basename($src) ;
409 $err = "$dst not implemented yet";
410 File::Copy::copy($src, \*STDOUT);
413 $self->{error} = $err;
416 $self->{error} = $err;
425 ################################################################
433 unless ($about_widget) {
434 my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_about") ;
435 $about_widget = $glade_box->get_widget("dlg_about") ;
436 $glade_box->signal_autoconnect_from_package('DlgAbout');
438 $about_widget->show() ;
441 sub on_about_okbutton_clicked
443 $about_widget->hide() ;
448 ################################################################
454 my ($class, $config_file) = @_;
457 config_file => $config_file,
458 password => '', # db passwd
459 username => '', # db username
460 connection_string => '',# db connection string
461 bconsole => 'bconsole', # path and arg to bconsole
462 bsr_dest => '', # destination url for bsr files
463 debug => 0, # debug level 0|1
464 use_ok_bkp_only => 1, # dont use bad backup
465 bweb => 'http://localhost/cgi-bin/bweb/bweb.pl', # bweb url
466 glade_file => $glade_file,
467 mozilla => 'mozilla', # mozilla bin
468 default_restore_job => 'restore', # regular expression to select default
471 # keywords that are used to fill DlgPref
472 chk_keyword => [ qw/use_ok_bkp_only debug/ ],
473 entry_keyword => [ qw/username password bweb mozilla
474 connection_string default_restore_job
475 bconsole bsr_dest glade_file/],
478 $self->read_config();
487 # We read the parameters. They come from the configuration files
488 my $cfgfile ; my $tmpbuffer;
489 if (open FICCFG, $self->{config_file})
491 while(read FICCFG,$tmpbuffer,4096)
493 $cfgfile .= $tmpbuffer;
497 no strict; # I have no idea of the contents of the file
498 eval '$refparams' . " = $cfgfile";
501 for my $p (keys %{$refparams}) {
502 $self->{$p} = $refparams->{$p};
505 if (defined $self->{debug}) {
506 $debug = $self->{debug} ;
509 # TODO : Force dumb default values and display a message
519 for my $k (@{ $self->{entry_keyword} }) {
520 $parameters{$k} = $self->{$k};
523 for my $k (@{ $self->{chk_keyword} }) {
524 $parameters{$k} = $self->{$k};
527 if (open FICCFG,">$self->{config_file}")
529 print FICCFG Data::Dumper->Dump([\%parameters], [qw($parameters)]);
534 # TODO : Display a message
543 $self->{dbh}->disconnect() ;
547 delete $self->{error};
549 if (not $self->{connection_string})
551 # The parameters have not been set. Maybe the conf
552 # file is empty for now
553 $self->{error} = "No configuration found for database connection. " .
554 "Please set this up.";
559 $self->{dbh} = DBI->connect($self->{connection_string},
564 $self->{error} = "Can't open bacula database. " .
565 "Database connect string '" .
566 $self->{connection_string} ."' $!";
569 $self->{dbh}->{RowCacheSize}=100;
575 my ($self, $url, $msg) = @_;
577 unless ($self->{mozilla} and $self->{bweb}) {
578 new DlgWarn("You must install Bweb and set your mozilla bin to $msg");
582 system("$self->{mozilla} -remote 'Ping()'");
584 new DlgWarn("Warning, you must have a running $self->{mozilla} to $msg");
588 my $cmd = "$self->{mozilla} -remote 'OpenURL($self->{bweb}$url,new-tab)'" ;
596 ################################################################
600 # my $pref = new Pref(config_file => 'brestore.conf');
601 # my $dlg = new DlgPref($pref);
602 # my $dlg_resto = new DlgResto($pref);
603 # $dlg->display($dlg_resto);
606 my ($class, $pref) = @_;
609 pref => $pref, # Pref ref
610 dlgresto => undef, # DlgResto ref
618 my ($self, $dlgresto) = @_ ;
620 unless ($self->{glade}) {
621 $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_pref") ;
622 $self->{glade}->signal_autoconnect_from_package($self);
625 $self->{dlgresto} = $dlgresto;
627 my $g = $self->{glade};
628 my $p = $self->{pref};
630 for my $k (@{ $p->{entry_keyword} }) {
631 $g->get_widget("entry_$k")->set_text($p->{$k}) ;
634 for my $k (@{ $p->{chk_keyword} }) {
635 $g->get_widget("chkbp_$k")->set_active($p->{$k}) ;
638 $g->get_widget("dlg_pref")->show_all() ;
641 sub on_applybutton_clicked
644 my $glade = $self->{glade};
645 my $pref = $self->{pref};
647 for my $k (@{ $pref->{entry_keyword} }) {
648 my $w = $glade->get_widget("entry_$k") ;
649 $pref->{$k} = $w->get_text();
652 for my $k (@{ $pref->{chk_keyword} }) {
653 my $w = $glade->get_widget("chkbp_$k") ;
654 $pref->{$k} = $w->get_active();
657 $pref->write_config();
658 if ($pref->connect_db()) {
659 $self->{dlgresto}->set_dbh($pref->{dbh});
660 $self->{dlgresto}->set_status('Preferences updated');
661 $self->{dlgresto}->init_server_backup_combobox();
663 $self->{dlgresto}->set_status($pref->{error});
667 # Handle prefs ok click (apply/dismiss dialog)
668 sub on_okbutton_clicked
671 $self->on_applybutton_clicked();
673 unless ($self->{pref}->{error}) {
674 $self->on_cancelbutton_clicked();
677 sub on_dialog_delete_event
680 $self->on_cancelbutton_clicked();
684 sub on_cancelbutton_clicked
687 $self->{glade}->get_widget('dlg_pref')->hide();
688 delete $self->{dlgresto};
692 ################################################################
702 # Kept as is from the perl-gtk example. Draws the pretty icons
708 $diricon = $self->{mainwin}->render_icon('gtk-open', $size);
709 $fileicon = $self->{mainwin}->render_icon('gtk-new', $size);
710 $yesicon = $self->{mainwin}->render_icon('gtk-yes', $size);
711 $noicon = $self->{mainwin}->render_icon('gtk-no', $size);
715 # init combo (and create ListStore object)
718 my ($widget, @type) = @_ ;
719 my %type_info = ('text' => 'Glib::String',
720 'markup' => 'Glib::String',
723 my $lst = new Gtk2::ListStore ( map { $type_info{$_} } @type );
725 $widget->set_model($lst);
729 if ($t eq 'text' or $t eq 'markup') {
730 $cell = new Gtk2::CellRendererText();
732 $widget->pack_start($cell, 1);
733 $widget->add_attribute($cell, $t, $i++);
738 # fill simple combo (one element per row)
741 my ($list, @what) = @_;
745 foreach my $w (@what)
748 my $i = $list->append();
749 $list->set($i, 0, $w);
756 my @unit = qw(b Kb Mb Gb Tb);
759 my $format = '%i %s';
760 while ($val / 1024 > 1) {
764 $format = ($i>0)?'%0.1f %s':'%i %s';
765 return sprintf($format, $val, $unit[$i]);
770 my ($self, $dbh) = @_;
776 my ($fileview) = shift;
777 my $fileview_target_entry = {target => 'STRING',
778 flags => ['GTK_TARGET_SAME_APP'],
781 $fileview->enable_model_drag_source(['button1_mask', 'button3_mask'],
782 ['copy'],$fileview_target_entry);
783 $fileview->get_selection->set_mode('multiple');
785 # set some useful SimpleList properties
786 $fileview->set_headers_clickable(0);
787 foreach ($fileview->get_columns())
789 $_->set_resizable(1);
790 $_->set_sizing('grow-only');
796 my ($class, $pref) = @_;
801 location => undef, # location entry widget
802 mainwin => undef, # mainwin widget
803 filelist_file_menu => undef, # file menu widget
804 filelist_dir_menu => undef, # dir menu widget
805 glade => undef, # glade object
806 status => undef, # status bar widget
807 dlg_pref => undef, # DlgPref object
808 fileattrib => {}, # cache file
809 fileview => undef, # fileview widget SimpleList
810 fileinfo => undef, # fileinfo widget SimpleList
812 client_combobox => undef, # client_combobox widget
813 restore_backup_combobox => undef, # date combobox widget
814 list_client => undef, # Gtk2::ListStore
815 list_backup => undef, # Gtk2::ListStore
818 # load menu (to use handler with self reference)
819 my $glade = Gtk2::GladeXML->new($glade_file, "filelist_file_menu");
820 $glade->signal_autoconnect_from_package($self);
821 $self->{filelist_file_menu} = $glade->get_widget("filelist_file_menu");
823 $glade = Gtk2::GladeXML->new($glade_file, "filelist_dir_menu");
824 $glade->signal_autoconnect_from_package($self);
825 $self->{filelist_dir_menu} = $glade->get_widget("filelist_dir_menu");
827 $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_resto");
828 $glade->signal_autoconnect_from_package($self);
830 $self->{status} = $glade->get_widget('statusbar');
831 $self->{mainwin} = $glade->get_widget('dlg_resto');
832 $self->{location} = $glade->get_widget('entry_location');
833 $self->render_icons();
835 $self->{dlg_pref} = new DlgPref($pref);
837 my $c = $self->{client_combobox} = $glade->get_widget('combo_client');
838 $self->{list_client} = init_combo($c, 'text');
840 $c = $self->{restore_backup_combobox} = $glade->get_widget('combo_list_backups');
841 $self->{list_backup} = init_combo($c, 'text', 'markup');
843 # Connect glade-fileview to Gtk2::SimpleList
844 # and set up drag n drop between $fileview and $restore_list
846 # WARNING : we have big dirty thinks with gtk/perl and utf8/iso strings
847 # we use an hidden field uuencoded to bypass theses bugs (h_name)
849 my $widget = $glade->get_widget('fileview');
850 my $fileview = $self->{fileview} = Gtk2::SimpleList->new_from_treeview(
852 'h_name' => 'hidden',
853 'h_jobid' => 'hidden',
854 'h_type' => 'hidden',
857 'File Name' => 'text',
860 init_drag_drop($fileview);
861 $fileview->set_search_column(4); # search on File Name
863 # Connect glade-restore_list to Gtk2::SimpleList
864 $widget = $glade->get_widget('restorelist');
865 my $restore_list = $self->{restore_list} = Gtk2::SimpleList->new_from_treeview(
867 'h_name' => 'hidden',
868 'h_jobid' => 'hidden',
869 'h_type' => 'hidden',
870 'h_curjobid' => 'hidden',
873 'File Name' => 'text',
875 'FileIndex' => 'text');
877 my @restore_list_target_table = ({'target' => 'STRING',
881 $restore_list->enable_model_drag_dest(['copy'],@restore_list_target_table);
882 $restore_list->get_selection->set_mode('multiple');
884 $widget = $glade->get_widget('infoview');
885 my $infoview = $self->{fileinfo} = Gtk2::SimpleList->new_from_treeview(
887 'h_name' => 'hidden',
888 'h_jobid' => 'hidden',
889 'h_type' => 'hidden',
891 'InChanger' => 'pixbuf',
898 init_drag_drop($infoview);
900 $pref->connect_db() || $self->{dlg_pref}->display($self);
903 $self->{dbh} = $pref->{dbh};
904 $self->init_server_backup_combobox();
908 # set status bar informations
911 my ($self, $string) = @_;
912 my $context = $self->{status}->get_context_id('Main');
913 $self->{status}->push($context, $string);
916 sub on_time_select_changed
924 my $c = $self->{glade}->get_widget('combo_time');
925 return $c->get_active_text;
928 # This sub returns all clients declared in DB
932 my $query = "SELECT Name FROM Client ORDER BY Name";
933 print $query,"\n" if $debug;
934 my $result = $dbh->selectall_arrayref($query);
936 foreach my $refrow (@$result)
938 push @return_array,($refrow->[0]);
940 return @return_array;
943 sub get_wanted_job_status
950 return "'T', 'A', 'E'";
954 # This sub gives a full list of the EndTimes for a ClientId
955 # ( [ 'Date', 'FileSet', 'Type', 'Status', 'JobId'],
956 # ['Date', 'FileSet', 'Type', 'Status', 'JobId']..)
957 sub get_all_endtimes_for_job
959 my ($dbh, $client, $ok_only)=@_;
960 my $status = get_wanted_job_status($ok_only);
962 SELECT Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus, Job.JobId
963 FROM Job,Client,FileSet
964 WHERE Job.ClientId=Client.ClientId
965 AND Client.Name = '$client'
967 AND JobStatus IN ($status)
968 AND Job.FileSetId = FileSet.FileSetId
969 ORDER BY EndTime desc";
970 print $query,"\n" if $debug;
971 my $result = $dbh->selectall_arrayref($query);
977 # init infoview widget
981 @{$self->{fileinfo}->{data}} = ();
988 @{$self->{restore_list}->{data}} = ();
991 use File::Temp qw/tempfile/;
993 sub on_go_button_clicked
996 my $bsr = $self->create_filelist();
997 my ($fh, $filename) = tempfile();
1000 chmod(0644, $filename);
1002 print "Dumping BSR info to $filename\n"
1005 # we get Volume list
1006 my %a = map { $_ => 1 } ($bsr =~ /Volume="(.+)"/g);
1007 my $vol = [ keys %a ] ; # need only one occurrence of each volume
1009 new DlgLaunch(pref => $self->{pref},
1011 bsr_file => $filename,
1016 our $client_list_empty = 'Clients list';
1017 our %type_markup = ('F' => '<b>$label F</b>',
1020 'B' => '<b>$label B</b>',
1022 'A' => '<span foreground=\"red\">$label</span>',
1024 'E' => '<span foreground=\"red\">$label</span>',
1027 sub on_list_client_changed
1029 my ($self, $widget) = @_;
1030 return 0 unless defined $self->{fileview};
1031 my $dbh = $self->{dbh};
1033 $self->{list_backup}->clear();
1035 if ($self->current_client eq $client_list_empty) {
1039 my @endtimes=get_all_endtimes_for_job($dbh,
1040 $self->current_client,
1041 $self->{pref}->{use_ok_bkp_only});
1042 foreach my $endtime (@endtimes)
1044 my $i = $self->{list_backup}->append();
1046 my $label = $endtime->[1] . " (" . $endtime->[4] . ")";
1047 eval "\$label = \"$type_markup{$endtime->[2]}\""; # job type
1048 eval "\$label = \"$type_markup{$endtime->[3]}\""; # job status
1050 $self->{list_backup}->set($i,
1055 $self->{restore_backup_combobox}->set_active(0);
1057 $self->{CurrentJobIds} = [
1058 set_job_ids_for_date($dbh,
1059 $self->current_client,
1060 $self->current_date,
1061 $self->{pref}->{use_ok_bkp_only})
1066 # undef $self->{dirtree};
1067 $self->refresh_fileview();
1071 sub fill_server_list
1073 my ($dbh, $combo, $list) = @_;
1075 my @clients=get_all_clients($dbh);
1079 my $i = $list->append();
1080 $list->set($i, 0, $client_list_empty);
1082 foreach my $client (@clients)
1084 $i = $list->append();
1085 $list->set($i, 0, $client);
1087 $combo->set_active(0);
1090 sub init_server_backup_combobox
1093 fill_server_list($self->{dbh},
1094 $self->{client_combobox},
1095 $self->{list_client}) ;
1098 #----------------------------------------------------------------------
1099 #Refreshes the file-view Redraws everything. The dir data is cached, the file
1100 #data isn't. There is additionnal complexity for dirs (visibility problems),
1101 #so the @CurrentJobIds is not sufficient.
1102 sub refresh_fileview
1105 my $fileview = $self->{fileview};
1106 my $client_combobox = $self->{client_combobox};
1107 my $cwd = $self->{cwd};
1109 @{$fileview->{data}} = ();
1111 $self->clear_infoview();
1113 my $client_name = $self->current_client;
1115 if (!$client_name or ($client_name eq $client_list_empty)) {
1116 $self->set_status("Client list empty");
1120 my @dirs = $self->list_dirs($cwd,$client_name);
1121 # [ [listfiles.id, listfiles.Name, File.LStat, File.JobId]..]
1122 my $files = $self->list_files($cwd);
1123 print "CWD : $cwd\n" if ($debug);
1125 my $file_count = 0 ;
1126 my $total_bytes = 0;
1128 # Add directories to view
1129 foreach my $dir (@dirs) {
1130 my $time = localtime($self->dir_attrib("$cwd/$dir",'st_mtime'));
1131 $total_bytes += 4096;
1134 listview_push($fileview,
1136 $self->dir_attrib("$cwd/$dir",'jobid'),
1146 foreach my $file (@$files)
1148 my $size = file_attrib($file,'st_size');
1149 my $time = localtime(file_attrib($file,'st_mtime'));
1150 $total_bytes += $size;
1152 # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
1154 listview_push($fileview,
1161 human($size), $time);
1164 $self->set_status("$file_count files/" . human($total_bytes));
1166 # set a decent default selection (makes keyboard nav easy)
1167 $fileview->select(0);
1171 sub on_about_activate
1173 DlgAbout::display();
1178 my ($tree, $path, $data) = @_;
1180 my @items = listview_get_all($tree) ;
1182 foreach my $i (@items)
1184 my @file_info = @{$i};
1187 # Ok, we have a corner case :
1192 $file = pack("u", $file_info[0]);
1196 $file = pack("u", $path . '/' . $file_info[0]);
1198 push @ret, join(" ; ", $file,
1199 $file_info[1], # $jobid
1200 $file_info[2], # $type
1204 my $data_get = join(" :: ", @ret);
1206 $data->set_text($data_get,-1);
1209 sub fileview_data_get
1211 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1212 drag_set_info($widget, $self->{cwd}, $data);
1215 sub fileinfo_data_get
1217 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1218 drag_set_info($widget, $self->{cwd}, $data);
1221 sub restore_list_data_received
1223 my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_;
1226 if ($info eq 40 || $info eq 0) # patch for display!=:0
1228 foreach my $elt (split(/ :: /, $data->data()))
1231 my ($file, $jobid, $type) =
1233 $file = unpack("u", $file);
1235 $self->add_selected_file_to_list($file, $jobid, $type);
1240 sub on_back_button_clicked {
1244 sub on_location_go_button_clicked
1247 $self->ch_dir($self->{location}->get_text());
1249 sub on_quit_activate {Gtk2->main_quit;}
1250 sub on_preferences_activate
1253 $self->{dlg_pref}->display($self) ;
1255 sub on_main_delete_event {Gtk2->main_quit;}
1256 sub on_bweb_activate
1259 $self->set_status("Open bweb on your browser");
1260 $self->{pref}->go_bweb('', "go on bweb");
1263 # Change to parent directory
1267 if ($self->{cwd} eq '/')
1271 my @dirs = File::Spec->splitdir ($self->{cwd});
1273 $self->ch_dir(File::Spec->catdir(@dirs));
1276 # Change the current working directory
1277 # * Updates fileview, location, and selection
1282 $self->{cwd} = shift;
1284 $self->refresh_fileview();
1285 $self->{location}->set_text($self->{cwd});
1290 # Handle dialog 'close' (window-decoration induced close)
1291 # * Just hide the dialog, and tell Gtk not to do anything else
1295 my ($self, $w) = @_;
1298 1; # consume this event!
1301 # Handle key presses in location text edit control
1302 # * Translate a Return/Enter key into a 'Go' command
1303 # * All other key presses left for GTK
1305 sub on_location_entry_key_release_event
1311 my $keypress = $event->keyval;
1312 if ($keypress == $Gtk2::Gdk::Keysyms{KP_Enter} ||
1313 $keypress == $Gtk2::Gdk::Keysyms{Return})
1315 $self->ch_dir($widget->get_text());
1317 return 1; # consume keypress
1320 return 0; # let gtk have the keypress
1323 sub on_fileview_key_press_event
1325 my ($self, $widget, $event) = @_;
1329 sub listview_get_first
1332 my @selected = $list->get_selected_indices();
1333 if (@selected > 0) {
1334 my ($name, @other) = @{$list->{data}->[$selected[0]]};
1335 return (unpack('u', $name), @other);
1341 sub listview_get_all
1345 my @selected = $list->get_selected_indices();
1347 for my $i (@selected) {
1348 my ($name, @other) = @{$list->{data}->[$i]};
1349 push @ret, [unpack('u', $name), @other];
1357 my ($list, $name, @other) = @_;
1358 push @{$list->{data}}, [pack('u', $name), @other];
1361 #----------------------------------------------------------------------
1362 # Handle keypress in file-view
1363 # * Translates backspace into a 'cd ..' command
1364 # * All other key presses left for GTK
1366 sub on_fileview_key_release_event
1368 my ($self, $widget, $event) = @_;
1369 if (not $event->keyval)
1373 if ($event->keyval == $Gtk2::Gdk::Keysyms{BackSpace}) {
1375 return 1; # eat keypress
1378 return 0; # let gtk have keypress
1381 sub on_forward_keypress
1386 #----------------------------------------------------------------------
1387 # Handle double-click (or enter) on file-view
1388 # * Translates into a 'cd <dir>' command
1390 sub on_fileview_row_activated
1392 my ($self, $widget) = @_;
1394 my ($name, undef, $type, undef) = listview_get_first($widget);
1398 if ($self->{cwd} eq '')
1400 $self->ch_dir($name);
1402 elsif ($self->{cwd} eq '/')
1404 $self->ch_dir('/' . $name);
1408 $self->ch_dir($self->{cwd} . '/' . $name);
1412 $self->fill_infoview($self->{cwd}, $name);
1415 return 1; # consume event
1420 my ($self, $path, $file) = @_;
1421 $self->clear_infoview();
1422 my @v = get_all_file_versions($self->{dbh},
1425 $self->current_client);
1427 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
1429 my $icon = ($inchanger)?$yesicon:$noicon;
1431 $mtime = localtime($mtime) ;
1433 listview_push($self->{fileinfo},
1434 $file, $jobid, 'file',
1435 $icon, $volname, $jobid, human($size), $mtime, $md5);
1442 return $self->{restore_backup_combobox}->get_active_text;
1448 return $self->{client_combobox}->get_active_text;
1451 sub on_list_backups_changed
1453 my ($self, $widget) = @_;
1454 return 0 unless defined $self->{fileview};
1456 $self->{CurrentJobIds} = [
1457 set_job_ids_for_date($self->{dbh},
1458 $self->current_client,
1459 $self->current_date,
1460 $self->{pref}->{use_ok_bkp_only})
1463 $self->refresh_fileview();
1467 sub on_restore_list_keypress
1469 my ($self, $widget, $event) = @_;
1470 if ($event->keyval == $Gtk2::Gdk::Keysyms{Delete})
1472 my @sel = $widget->get_selected_indices;
1473 foreach my $elt (reverse(sort {$a <=> $b} @sel))
1475 splice @{$self->{restore_list}->{data}},$elt,1;
1480 sub on_fileview_button_press_event
1482 my ($self,$widget,$event) = @_;
1483 if ($event->button == 3)
1485 $self->on_right_click_filelist($widget,$event);
1489 if ($event->button == 2)
1491 $self->on_see_all_version();
1498 sub on_see_all_version
1502 my @lst = listview_get_all($self->{fileview});
1505 my ($name, undef) = @{$i};
1507 new DlgFileVersion($self->{dbh},
1508 $self->current_client,
1509 $self->{cwd}, $name);
1513 sub on_right_click_filelist
1515 my ($self,$widget,$event) = @_;
1516 # I need to know what's selected
1517 my @sel = listview_get_all($self->{fileview});
1522 $type = $sel[0]->[2]; # $type
1527 if (@sel >=2 or $type eq 'dir')
1529 # We have selected more than one or it is a directories
1530 $w = $self->{filelist_dir_menu};
1534 $w = $self->{filelist_file_menu};
1540 $event->button, $event->time);
1543 sub context_add_to_filelist
1547 my @sel = listview_get_all($self->{fileview});
1549 foreach my $i (@sel)
1551 my ($file, $jobid, $type, undef) = @{$i};
1552 $file = $self->{cwd} . '/' . $file;
1553 $self->add_selected_file_to_list($file, $jobid, $type);
1557 # Adds a file to the filelist
1558 sub add_selected_file_to_list
1560 my ($self, $name, $jobid, $type)=@_;
1562 my $dbh = $self->{dbh};
1563 my $restore_list = $self->{restore_list};
1565 my $curjobids=join(',', @{$self->{CurrentJobIds}});
1572 if ($name and substr $name,-1 ne '/')
1574 $name .= '/'; # For bacula
1576 my $dirfileindex = get_fileindex_from_dir_jobid($dbh,$name,$jobid);
1577 listview_push($restore_list,
1578 $name, $jobid, 'dir', $curjobids,
1579 $diricon, $name,$jobid,$dirfileindex);
1581 elsif ($type eq 'file')
1583 my $fileindex = get_fileindex_from_file_jobid($dbh,$name,$jobid);
1585 listview_push($restore_list,
1586 $name, $jobid, 'file', $curjobids,
1587 $fileicon, $name, $jobid, $fileindex );
1591 # TODO : we want be able to restore files from a bad ended backup
1592 # we have JobStatus IN ('T', 'A', 'E') and we must
1594 # Data acces subs from here. Interaction with SGBD and caching
1596 # This sub retrieves the list of jobs corresponding to the jobs selected in the
1597 # GUI and stores them in @CurrentJobIds
1598 sub set_job_ids_for_date
1600 my ($dbh, $client, $date, $only_ok)=@_;
1602 if (!$client or !$date) {
1606 my $status = get_wanted_job_status($only_ok);
1608 # The algorithm : for a client, we get all the backups for each
1609 # fileset, in reverse order Then, for each fileset, we store the 'good'
1610 # incrementals and differentials until we have found a full so it goes
1611 # like this : store all incrementals until we have found a differential
1612 # or a full, then find the full #
1614 my $query = "SELECT JobId, FileSet, Level, JobStatus
1615 FROM Job, Client, FileSet
1616 WHERE Job.ClientId = Client.ClientId
1617 AND FileSet.FileSetId = Job.FileSetId
1618 AND EndTime <= '$date'
1619 AND Client.Name = '$client'
1621 AND JobStatus IN ($status)
1622 ORDER BY FileSet, JobTDate DESC";
1624 print $query,"\n" if $debug;
1626 my $result = $dbh->selectall_arrayref($query);
1628 foreach my $refrow (@$result)
1630 my $jobid = $refrow->[0];
1631 my $fileset = $refrow->[1];
1632 my $level = $refrow->[2];
1634 defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
1636 next if $progress{$fileset} eq 'F'; # It's over for this fileset...
1640 next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
1641 push @CurrentJobIds,($jobid);
1643 elsif ($level eq 'D')
1645 next if $progress{$fileset} eq 'D'; # We allready have a differential
1646 push @CurrentJobIds,($jobid);
1648 elsif ($level eq 'F')
1650 push @CurrentJobIds,($jobid);
1653 my $status = $refrow->[3] ;
1654 if ($status eq 'T') { # good end of job
1655 $progress{$fileset} = $level;
1658 print Data::Dumper::Dumper(\@CurrentJobIds) if $debug;
1660 return @CurrentJobIds;
1663 # Lists all directories contained inside a directory.
1664 # Uses the current dir, the client name, and CurrentJobIds for visibility.
1665 # Returns an array of dirs
1668 my ($self,$dir,$client)=@_;
1669 print "list_dirs($dir, $client)\n";
1671 # Is data allready cached ?
1672 if (not $self->{dirtree}->{$client})
1674 $self->cache_dirs($client);
1677 if ($dir ne '' and substr $dir,-1 ne '/')
1679 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1681 # Here, the tree is cached in ram
1682 my @dir = split('/',$dir,-1);
1683 pop @dir; # We don't need the empty trailing element
1685 # We have to get the reference of the hash containing $dir contents
1687 my $refdir=$self->{dirtree}->{$client};
1690 foreach my $subdir (@dir)
1696 $refdir = $refdir->[0]->{$subdir};
1699 # We reached the directory
1702 foreach my $dir (sort(keys %{$refdir->[0]}))
1704 # We return the directory's content : only visible directories
1705 foreach my $jobid (reverse(sort(@{$self->{CurrentJobIds}})))
1707 if (defined $refdir->[0]->{$dir}->[1]->{$jobid})
1709 my $dirname = $refdir->[0]->{$dir}->[2]; # The real dirname...
1710 push @return_list,($dirname);
1711 next DIRLOOP; # No need to waste more CPU cycles...
1715 print "LIST DIR : ", Data::Dumper::Dumper(\@return_list),"\n";
1716 return @return_list;
1720 # List all files in a directory. dir as parameter, CurrentJobIds for visibility
1721 # Returns an array of dirs
1724 my ($self, $dir)=@_;
1725 my $dbh = $self->{dbh};
1729 print "list_files($dir)\n";
1731 if ($dir ne '' and substr $dir,-1 ne '/')
1733 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1736 my $query = "SELECT Path.PathId FROM Path WHERE Path.Path = '$dir'";
1737 print $query,"\n" if $debug;
1739 my $result = $dbh->selectall_arrayref($query);
1740 foreach my $refrow (@$result)
1742 push @list_pathid,($refrow->[0]);
1745 if (@list_pathid == 0)
1747 print "No pathid found for $dir\n" if $debug;
1751 my $inlistpath = join (',', @list_pathid);
1752 my $inclause = join (',', @{$self->{CurrentJobIds}});
1753 if ($inclause eq '')
1759 "SELECT listfiles.id, listfiles.Name, File.LStat, File.JobId
1761 (SELECT Filename.Name, max(File.FileId) as id
1763 WHERE File.FilenameId = Filename.FilenameId
1764 AND Filename.Name != ''
1765 AND File.PathId IN ($inlistpath)
1766 AND File.JobId IN ($inclause)
1767 GROUP BY Filename.Name
1768 ORDER BY Filename.Name) AS listfiles,
1770 WHERE File.FileId = listfiles.id";
1772 print $query,"\n" if $debug;
1773 $result = $dbh->selectall_arrayref($query);
1778 # For the dirs, because of the db schema, it's inefficient to get the
1779 # directories contained inside other directories (regexp match or tossing
1780 # lots of records...). So we load all the tree and cache it. The data is
1781 # stored in a structure of this form :
1782 # Each directory is an array.
1783 # - In this array, the first element is a ref to next dir (hash)
1784 # - The second element is a hash containing all jobids pointing
1785 # on an array containing their lstat (or 1 if this jobid is there because
1787 # - The third is the filename itself (it could get mangled because of
1790 # So it looks like this :
1791 # $reftree->[ { 'dir1' => $refdir1
1792 # 'dir2' => $refdir2
1795 # { 'jobid1' => 'lstat1',
1796 # 'jobid2' => 'lstat2',
1797 # 'jobid3' => 1 # This one is here for "visibility"
1802 # Client as a parameter
1803 # Returns an array of dirs
1806 my ($self, $client) = @_;
1807 print "cache_dirs()\n";
1809 $self->{dirtree}->{$client} = []; # reset cache
1810 my $dbh = $self->{dbh};
1812 # TODO : If we get here, things could get lenghty ... draw a popup window .
1813 my $widget = Gtk2::MessageDialog->new($self->{mainwin},
1814 'destroy-with-parent',
1816 'Populating cache');
1818 Gtk2->main_iteration while (Gtk2->events_pending);
1820 # We have to build the tree, as it's the first time it is asked...
1823 # First, we only need the jobids of the selected server.
1824 # It's not the same as @CurrentJobIds (we need ALL the jobs)
1825 # We get the JobIds first in order to have the best execution
1826 # plan possible for the big query, with an in clause.
1828 my $status = get_wanted_job_status($self->{pref}->{use_ok_bkp_only});
1832 WHERE Job.ClientId = Client.ClientId
1833 AND Client.Name = '$client'
1834 AND Job.JobStatus IN ($status)
1835 AND Job.Type = 'B'";
1837 print $query,"\n" if $debug;
1838 my $result = $dbh->selectall_arrayref($query);
1840 foreach my $record (@{$result})
1842 push @jobids,($record->[0]);
1844 my $inclause = join(',',@jobids);
1845 if ($inclause eq '')
1848 $self->set_status("No previous backup found for $client");
1852 # Then, still to help dear mysql, we'll retrieve the PathId from empty Path (directory entries...)
1855 "SELECT Filename.FilenameId FROM Filename WHERE Filename.Name=''";
1857 print $query,"\n" if $debug;
1858 $result = $dbh->selectall_arrayref($query);
1859 foreach my $record (@{$result})
1861 push @dirids,$record->[0];
1863 my $dirinclause = join(',',@dirids);
1865 # This query is a bit complicated :
1866 # whe need to find all dir entries that should be displayed, even
1867 # if the directory itself has no entry in File table (it means a file
1868 # is explicitely chosen in the backup configuration)
1869 # Here's what I wanted to do :
1872 # SELECT T1.Path, T2.Lstat, T2.JobId
1873 # FROM ( SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
1874 # WHERE File.PathId = Path.PathId
1875 # AND File.JobId IN ($inclause)) AS T1
1877 # ( SELECT File.Lstat, File.JobId, File.PathId FROM File
1878 # WHERE File.FilenameId IN ($dirinclause)
1879 # AND File.JobId IN ($inclause)) AS T2
1880 # ON (T1.PathId = T2.PathId)
1882 # It works perfectly with postgresql, but mysql doesn't seem to be able
1883 # to do the hash join correcty, so the performance sucks.
1884 # So it will be done in 4 steps :
1885 # o create T1 and T2 as temp tables
1886 # o create an index on T2.PathId
1888 # o remove the temp tables
1890 CREATE TEMPORARY TABLE T1 AS
1891 SELECT DISTINCT Path.PathId, Path.Path FROM File, Path
1892 WHERE File.PathId = Path.PathId
1893 AND File.JobId IN ($inclause)
1895 print $query,"\n" if $debug;
1898 CREATE TEMPORARY TABLE T2 AS
1899 SELECT File.Lstat, File.JobId, File.PathId FROM File
1900 WHERE File.FilenameId IN ($dirinclause)
1901 AND File.JobId IN ($inclause)
1903 print $query,"\n" if $debug;
1906 CREATE INDEX tmp2 ON T2(PathId)
1908 print $query,"\n" if $debug;
1912 SELECT T1.Path, T2.Lstat, T2.JobId
1913 FROM T1 LEFT JOIN T2
1914 ON (T1.PathId = T2.PathId)
1917 print $query,"\n" if $debug;
1918 $result = $dbh->selectall_arrayref($query);
1920 foreach my $record (@{$result})
1922 # Dirty hack to force the string encoding on perl... we don't
1923 # want implicit conversions
1924 my $path = pack "U0C*", unpack "C*",$record->[0];
1926 my @path = split('/',$path,-1);
1927 pop @path; # we don't need the trailing empty element
1928 my $lstat = $record->[1];
1929 my $jobid = $record->[2];
1931 # We're going to store all the data on the cache tree.
1932 # We find the leaf, then store data there
1933 my $reftree=$self->{dirtree}->{$client};
1934 foreach my $dir(@path)
1940 if (not defined($reftree->[0]->{$dir}))
1943 $reftree->[0]->{$dir}=\@tmparray;
1945 $reftree=$reftree->[0]->{$dir};
1948 # We can now add the metadata for this dir ...
1950 # $result = $dbh->selectall_arrayref($query);
1953 # contains something
1954 $reftree->[1]->{$jobid}=$lstat;
1958 # We have a very special case here...
1959 # lstat is not defined.
1960 # it means the directory is there because a file has been
1961 # backuped. so the dir has no entry in File table.
1962 # That's a rare case, so we can afford to determine it's
1963 # visibility with a query
1964 my $select_path=$record->[0];
1965 $select_path=$dbh->quote($select_path); # gotta be careful
1969 WHERE File.PathId = Path.PathId
1970 AND Path.Path = $select_path
1972 print $query,"\n" if $debug;
1973 my $result2 = $dbh->selectall_arrayref($query);
1974 foreach my $record (@{$result2})
1976 my $jobid=$record->[0];
1977 $reftree->[1]->{$jobid}=1;
1985 print $query,"\n" if $debug;
1990 print $query,"\n" if $debug;
1994 list_visible($self->{dirtree}->{$client});
1997 # print Data::Dumper::Dumper($self->{dirtree});
2000 # Recursive function to calculate the visibility of each directory in the cache
2001 # tree Working with references to save time and memory
2002 # For each directory, we want to propagate it's visible jobids onto it's
2003 # parents directory.
2004 # A tree is visible if
2005 # - it's been in a backup pointed by the CurrentJobIds
2006 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2007 # In the second case, the directory is visible but has no metadata.
2008 # We symbolize this with lstat = 1 for this jobid in the cache.
2010 # Input : reference directory
2011 # Output : visibility of this dir. Has to know visibility of all subdirs
2012 # to know it's visibility, hence the recursing.
2018 # Get the subdirs array references list
2019 my @list_ref_subdirs;
2020 while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2022 push @list_ref_subdirs,($ref_subdir);
2025 # Now lets recurse over these subdirs and retrieve the reference of a hash
2026 # containing the jobs where they are visible
2027 foreach my $ref_subdir (@list_ref_subdirs)
2029 my $ref_list_jobs = list_visible($ref_subdir);
2030 foreach my $jobid (keys %$ref_list_jobs)
2032 $visibility{$jobid}=1;
2036 # Ok. Now, we've got the list of those jobs. We are going to update our
2037 # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2038 # the lstat for the known jobids. Put 1 in the new elements... But first,
2039 # let's store the current jobids
2041 foreach my $jobid (keys %{$refdir->[1]})
2043 push @known_jobids,($jobid);
2047 foreach my $jobid (keys %visibility)
2049 next if ($refdir->[1]->{$jobid});
2050 $refdir->[1]->{$jobid} = 1;
2052 # Add the known_jobids to %visibility
2053 foreach my $jobid (@known_jobids)
2055 $visibility{$jobid}=1;
2057 return \%visibility;
2060 # Returns the list of media required for a list of jobids.
2061 # Input : dbh, jobid1, jobid2...
2062 # Output : reference to array of (joibd, inchanger)
2063 sub get_required_media_from_jobid
2065 my ($dbh, @jobids)=@_;
2066 my $inclause = join(',',@jobids);
2068 SELECT DISTINCT JobMedia.MediaId, Media.InChanger
2069 FROM JobMedia, Media
2070 WHERE JobMedia.MediaId=Media.MediaId
2071 AND JobId In ($inclause)
2073 my $result = $dbh->selectall_arrayref($query);
2077 # Returns the fileindex from dirname and jobid.
2078 # Input : dbh, dirname, jobid
2079 # Output : fileindex
2080 sub get_fileindex_from_dir_jobid
2082 my ($dbh, $dirname, $jobid)=@_;
2084 $query = "SELECT File.FileIndex
2085 FROM File, Filename, Path
2086 WHERE File.FilenameId = Filename.FilenameId
2087 AND File.PathId = Path.PathId
2088 AND Filename.Name = ''
2089 AND Path.Path = '$dirname'
2090 AND File.JobId = '$jobid'
2093 print $query,"\n" if $debug;
2094 my $result = $dbh->selectall_arrayref($query);
2095 return $result->[0]->[0];
2098 # Returns the fileindex from filename and jobid.
2099 # Input : dbh, filename, jobid
2100 # Output : fileindex
2101 sub get_fileindex_from_file_jobid
2103 my ($dbh, $filename, $jobid)=@_;
2105 my @dirs = File::Spec->splitdir ($filename);
2106 $filename=pop(@dirs);
2107 my $dirname = File::Spec->catdir(@dirs) . '/';
2112 "SELECT File.FileIndex
2113 FROM File, Filename, Path
2114 WHERE File.FilenameId = Filename.FilenameId
2115 AND File.PathId = Path.PathId
2116 AND Filename.Name = '$filename'
2117 AND Path.Path = '$dirname'
2118 AND File.JobId = '$jobid'";
2120 print $query,"\n" if $debug;
2121 my $result = $dbh->selectall_arrayref($query);
2122 return $result->[0]->[0];
2126 # Returns list of versions of a file that could be restored
2127 # returns an array of
2128 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2129 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2130 # and volname and md5
2131 # and of course, there will be only one jobid in the array of jobids...
2132 sub get_all_file_versions
2134 my ($dbh,$path,$file,$client,$see_all)=@_;
2136 defined $see_all or $see_all=0;
2141 "SELECT File.JobId, File.FileIndex, File.Lstat,
2142 File.Md5, Media.VolumeName, Media.InChanger
2143 FROM File, Filename, Path, Job, Client, JobMedia, Media
2144 WHERE File.FilenameId = Filename.FilenameId
2145 AND File.PathId=Path.PathId
2146 AND File.JobId = Job.JobId
2147 AND Job.ClientId = Client.ClientId
2148 AND Job.JobId = JobMedia.JobId
2149 AND File.FileIndex >= JobMedia.FirstIndex
2150 AND File.FileIndex <= JobMedia.LastIndex
2151 AND JobMedia.MediaId = Media.MediaId
2152 AND Path.Path = '$path'
2153 AND Filename.Name = '$file'
2154 AND Client.Name = '$client'";
2156 print $query if $debug;
2158 my $result = $dbh->selectall_arrayref($query);
2160 foreach my $refrow (@$result)
2162 my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2163 my @attribs = parse_lstat($lstat);
2164 my $mtime = array_attrib('st_mtime',\@attribs);
2165 my $size = array_attrib('st_size',\@attribs);
2167 my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2168 $inchanger, $md5, $volname);
2169 push @versions, (\@list);
2172 # We have the list of all versions of this file.
2173 # We'll sort it by mtime desc, size, md5, inchanger desc
2174 # the rest of the algorithm will be simpler
2175 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2176 @versions = sort { $b->[4] <=> $a->[4]
2177 || $a->[5] <=> $b->[5]
2178 || $a->[7] cmp $a->[7]
2179 || $b->[6] <=> $a->[6]} @versions;
2182 my %allready_seen_by_mtime;
2183 my %allready_seen_by_md5;
2184 # Now we should create a new array with only the interesting records
2185 foreach my $ref (@versions)
2189 # The file has a md5. We compare his md5 to other known md5...
2190 # We take size into account. It may happen that 2 files
2191 # have the same md5sum and are different. size is a supplementary
2194 # If we allready have a (better) version
2195 next if ( (not $see_all)
2196 and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
2198 # we never met this one before...
2199 $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2201 # Even if it has a md5, we should also work with mtimes
2202 # We allready have a (better) version
2203 next if ( (not $see_all)
2204 and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
2205 $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2207 # We reached there. The file hasn't been seen.
2208 push @good_versions,($ref);
2211 # To be nice with the user, we re-sort good_versions by
2212 # inchanger desc, mtime desc
2213 @good_versions = sort { $b->[4] <=> $a->[4]
2214 || $b->[2] <=> $a->[2]} @good_versions;
2216 return @good_versions;
2219 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2220 # This sub creates a BSR from the information in the restore_list
2221 # Returns the BSR as a string
2225 my $dbh = $self->{dbh};
2227 # This query gets all jobid/jobmedia/media combination.
2229 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile,
2230 JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2231 JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex,
2232 Media.Volumename, Media.MediaType
2233 FROM Job, JobMedia, Media
2234 WHERE Job.JobId = JobMedia.JobId
2235 AND JobMedia.MediaId = Media.MediaId
2236 ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2239 my $result = $dbh->selectall_arrayref($query);
2241 # We will store everything hashed by jobid.
2243 foreach my $refrow (@$result)
2245 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2246 $firstindex, $lastindex, $startblock, $endblock,
2247 $volindex, $volumename, $mediatype) = @{$refrow};
2249 # We just have to deal with the case where starfile != endfile
2250 # In this case, we concatenate both, for the bsr
2251 if ($startfile != $endfile) {
2252 $startfile = $startfile . '-' . $endfile;
2256 ($jobid, $volsessionid, $volsessiontime, $startfile,
2257 $firstindex, $lastindex, $startblock .'-'. $endblock,
2258 $volindex, $volumename, $mediatype);
2260 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2264 # reminder : restore_list looks like this :
2265 # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2267 # Here, we retrieve every file/dir that could be in the restore
2268 # We do as simple as possible for the SQL engine (no crazy joins,
2269 # no pseudo join (>= FirstIndex ...), etc ...
2270 # We do a SQL union of all the files/dirs specified in the restore_list
2272 foreach my $entry (@{$self->{restore_list}->{data}})
2274 if ($entry->[2] eq 'dir')
2276 my $dir = unpack('u', $entry->[0]);
2277 my $inclause = $entry->[3]; #curjobids
2280 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2281 FROM File, Path, Filename
2282 WHERE Path.PathId = File.PathId
2283 AND File.FilenameId = Filename.FilenameId
2284 AND Path.Path LIKE '$dir%'
2285 AND File.JobId IN ($inclause) )";
2286 push @select_queries,($query);
2290 # It's a file. Great, we allready have most
2291 # of what is needed. Simple and efficient query
2292 my $file = unpack('u', $entry->[0]);
2293 my @file = split '/',$file;
2295 my $dir = join('/',@file);
2297 my $jobid = $entry->[1];
2298 my $fileindex = $entry->[7];
2299 my $inclause = $entry->[3]; # curjobids
2301 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2302 FROM File, Path, Filename
2303 WHERE Path.PathId = File.PathId
2304 AND File.FilenameId = Filename.FilenameId
2305 AND Path.Path = '$dir/'
2306 AND Filename.Name = '$file'
2307 AND File.JobId = $jobid)";
2308 push @select_queries,($query);
2311 $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2313 print $query,"\n" if $debug;
2315 #Now we run the query and parse the result...
2316 # there may be a lot of records, so we better be efficient
2317 # We use the bind column method, working with references...
2319 my $sth = $dbh->prepare($query);
2322 my ($path,$name,$fileindex,$jobid);
2323 $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2325 # The temp place we're going to save all file
2326 # list to before the real list
2330 while ($sth->fetchrow_arrayref())
2332 # This may look dumb, but we're going to do a join by ourselves,
2333 # to save memory and avoid sending a complex query to mysql
2334 my $complete_path = $path . $name;
2342 # Remove trailing slash (normalize file and dir name)
2343 $complete_path =~ s/\/$//;
2345 # Let's find the ref(s) for the %mediainfo element(s)
2346 # containing the data for this file
2347 # There can be several matches. It is the pseudo join.
2349 my $max_elt=@{$mediainfos{$jobid}}-1;
2351 while($med_idx <= $max_elt)
2353 my $ref = $mediainfos{$jobid}->[$med_idx];
2354 # First, can we get rid of the first elements of the
2355 # array ? (if they don't contain valuable records
2357 if ($fileindex > $ref->[5])
2359 # It seems we don't need anymore
2360 # this entry in %mediainfo (the input data
2363 shift @{$mediainfos{$jobid}};
2367 # We will do work on this elt. We can ++
2368 # $med_idx for next loop
2371 # %mediainfo row looks like :
2372 # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2373 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2376 # We are in range. We store and continue looping
2378 if ($fileindex >= $ref->[4])
2380 my @data = ($complete_path,$is_dir,
2382 push @temp_list,(\@data);
2386 # We are not in range. No point in continuing looping
2387 # We go to next record.
2391 # Now we have the array.
2392 # We're going to sort it, by
2393 # path, volsessiontime DESC (get the most recent file...)
2394 # The array rows look like this :
2395 # complete_path,is_dir,fileindex,
2396 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2397 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2398 @temp_list = sort {$a->[0] cmp $b->[0]
2399 || $b->[3]->[2] <=> $a->[3]->[2]
2403 my $prev_complete_path='////'; # Sure not to match
2407 while (my $refrow = shift @temp_list)
2409 # For the sake of readability, we load $refrow
2410 # contents in real scalars
2411 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2412 my $jobid= $refother->[0]; # We don't need the rest...
2414 # We skip this entry.
2415 # We allready have a newer one and this
2416 # isn't a continuation of the same file
2417 next if ($complete_path eq $prev_complete_path
2418 and $jobid != $prev_jobid);
2422 and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2424 # We would be recursing inside a file.
2425 # Just what we don't want (dir replaced by file
2426 # between two backups
2432 push @restore_list,($refrow);
2434 $prev_complete_path = $complete_path;
2435 $prev_jobid = $jobid;
2441 push @restore_list,($refrow);
2443 $prev_complete_path = $complete_path;
2444 $prev_jobid = $jobid;
2448 # We get rid of @temp_list... save memory
2451 # Ok everything is in the list. Let's sort it again in another way.
2452 # This time it will be in the bsr file order
2454 # we sort the results by
2455 # volsessiontime, volsessionid, volindex, fileindex
2456 # to get all files in right order...
2457 # Reminder : The array rows look like this :
2458 # complete_path,is_dir,fileindex,
2459 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2460 # StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2462 @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2]
2463 || $a->[3]->[1] <=> $b->[3]->[1]
2464 || $a->[3]->[7] <=> $b->[3]->[7]
2465 || $a->[2] <=> $b->[2] }
2468 # Now that everything is ready, we create the bsr
2469 my $prev_fileindex=-1;
2470 my $prev_volsessionid=-1;
2471 my $prev_volsessiontime=-1;
2472 my $prev_volumename=-1;
2473 my $prev_volfile=-1;
2477 my $first_of_current_range=0;
2478 my @fileindex_ranges;
2481 foreach my $refrow (@restore_list)
2483 my (undef,undef,$fileindex,$refother)=@{$refrow};
2484 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2485 $volblocks,undef,$volumename,$mediatype)=@{$refother};
2487 # We can specifiy the number of files in each section of the
2488 # bsr to speedup restore (bacula can then jump over the
2489 # end of tape files.
2493 if ($prev_volumename eq '-1')
2495 # We only have to start the new range...
2496 $first_of_current_range=$fileindex;
2498 elsif ($prev_volsessionid != $volsessionid
2499 or $prev_volsessiontime != $volsessiontime
2500 or $prev_volumename ne $volumename
2501 or $prev_volfile != $volfile)
2503 # We have to create a new section in the bsr...
2504 # We print the previous one ...
2505 # (before that, save the current range ...)
2506 if ($first_of_current_range != $prev_fileindex)
2509 push @fileindex_ranges,
2510 ("$first_of_current_range-$prev_fileindex");
2514 # We are out of a range,
2515 # but there is only one element in the range
2516 push @fileindex_ranges,
2517 ("$first_of_current_range");
2520 $bsr.=print_bsr_section(\@fileindex_ranges,
2522 $prev_volsessiontime,
2529 # Reset for next loop
2530 @fileindex_ranges=();
2531 $first_of_current_range=$fileindex;
2533 elsif ($fileindex-1 != $prev_fileindex)
2535 # End of a range of fileindexes
2536 if ($first_of_current_range != $prev_fileindex)
2539 push @fileindex_ranges,
2540 ("$first_of_current_range-$prev_fileindex");
2544 # We are out of a range,
2545 # but there is only one element in the range
2546 push @fileindex_ranges,
2547 ("$first_of_current_range");
2549 $first_of_current_range=$fileindex;
2551 $prev_fileindex=$fileindex;
2552 $prev_volsessionid = $volsessionid;
2553 $prev_volsessiontime = $volsessiontime;
2554 $prev_volumename = $volumename;
2555 $prev_volfile=$volfile;
2556 $prev_mediatype=$mediatype;
2557 $prev_volblocks=$volblocks;
2561 # Ok, we're out of the loop. Alas, there's still the last record ...
2562 if ($first_of_current_range != $prev_fileindex)
2565 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2570 # We are out of a range,
2571 # but there is only one element in the range
2572 push @fileindex_ranges,("$first_of_current_range");
2575 $bsr.=print_bsr_section(\@fileindex_ranges,
2577 $prev_volsessiontime,
2587 sub print_bsr_section
2589 my ($ref_fileindex_ranges,$volsessionid,
2590 $volsessiontime,$volumename,$volfile,
2591 $mediatype,$volblocks,$count)=@_;
2594 $bsr .= "Volume=\"$volumename\"\n";
2595 $bsr .= "MediaType=\"$mediatype\"\n";
2596 $bsr .= "VolSessionId=$volsessionid\n";
2597 $bsr .= "VolSessionTime=$volsessiontime\n";
2598 $bsr .= "VolFile=$volfile\n";
2599 $bsr .= "VolBlock=$volblocks\n";
2601 foreach my $range (@{$ref_fileindex_ranges})
2603 $bsr .= "FileIndex=$range\n";
2606 $bsr .= "Count=$count\n";
2612 my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
2613 'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
2614 'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
2615 'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
2616 'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
2617 'data_stream' => 15);;
2620 my ($attrib,$ref_attrib)=@_;
2621 return $ref_attrib->[$attrib_name_id{$attrib}];
2625 { # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
2627 my ($file, $attrib)=@_;
2629 if (defined $attrib_name_id{$attrib}) {
2631 my @d = split(' ', $file->[2]) ; # TODO : cache this
2633 return from_base64($d[$attrib_name_id{$attrib}]);
2635 } elsif ($attrib eq 'jobid') {
2639 } elsif ($attrib eq 'name') {
2644 die "Attribute not known : $attrib.\n";
2648 # Return the jobid or attribute asked for a dir
2651 my ($self,$dir,$attrib)=@_;
2653 my @dir = split('/',$dir,-1);
2654 my $refdir=$self->{dirtree}->{$self->current_client};
2656 if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
2658 die "Attribute not known : $attrib.\n";
2661 foreach my $subdir (@dir)
2663 $refdir = $refdir->[0]->{$subdir};
2666 # $refdir is now the reference to the dir's array
2667 # Is the a jobid in @CurrentJobIds where the lstat is
2668 # defined (we'll search in reverse order)
2669 foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
2671 if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
2673 if ($attrib eq 'jobid')
2679 my @attribs = parse_lstat($refdir->[1]->{$jobid});
2680 return $attribs[$attrib_name_id{$attrib}+1];
2685 return 0; # We cannot get a good attribute.
2686 # This directory is here for the sake of visibility
2691 # Base 64 functions, directly from recover.pl.
2693 # Karl Hakimian <hakimian@aha.com>
2694 # This section is also under GPL v2 or later.
2701 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
2702 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
2703 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
2704 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
2705 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
2707 @base64_map = (0) x 128;
2709 for (my $i=0; $i<64; $i++) {
2710 $base64_map[ord($base64_digits[$i])] = $i;
2725 if (substr($where, 0, 1) eq '-') {
2727 $where = substr($where, 1);
2730 while ($where ne '') {
2732 my $d = substr($where, 0, 1);
2733 $val += $base64_map[ord(substr($where, 0, 1))];
2734 $where = substr($where, 1);
2742 my @attribs = split(' ',$lstat);
2743 foreach my $element (@attribs)
2745 $element = from_base64($element);
2752 ################################################################
2756 my $conf = "$ENV{HOME}/.brestore.conf" ;
2757 my $p = new Pref($conf);
2763 $glade_file = $p->{glade_file};
2765 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
2766 if (-f "$path/$glade_file") {
2767 $glade_file = "$path/$glade_file" ;
2772 if ( -f $glade_file) {
2773 my $w = new DlgResto($p);
2776 my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close',
2777 "Can't find your brestore.glade (glade_file => '$glade_file')
2778 Please, edit your $conf to setup it." );
2780 $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
2785 Gtk2->main; # Start Gtk2 main loop
2797 # Code pour trier les colonnes
2798 my $mod = $fileview->get_model();
2799 $mod->set_default_sort_func(sub {
2800 my ($model, $item1, $item2) = @_;
2801 my $a = $model->get($item1, 1); # récupération de la valeur de la 2ème
2802 my $b = $model->get($item2, 1); # colonne (indice 1)
2807 $fileview->set_headers_clickable(1);
2808 my $col = $fileview->get_column(1); # la colonne NOM, colonne numéro 2
2809 $col->signal_connect('clicked', sub {
2810 my ($colonne, $model) = @_;
2811 $model->set_sort_column_id (1, 'ascending');