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 You have to add brestore_xxx tables to your catalog.
26 To speed up database query you have to create theses indexes
27 - CREATE INDEX file_pathid on File(PathId);
30 To follow restore job, you must have a running Bweb installation.
34 Bacula® - The Network Backup Solution
36 Copyright (C) 2000-2006 Free Software Foundation Europe e.V.
38 Brestore authors are Marc Cousin and Eric Bollengier.
39 The main author of Bacula is Kern Sibbald, with contributions from
40 many others, a complete list can be found in the file AUTHORS.
42 This program is Free Software; you can redistribute it and/or
43 modify it under the terms of version two of the GNU General Public
44 License as published by the Free Software Foundation plus additions
45 that are listed in the file LICENSE.
47 This program is distributed in the hope that it will be useful, but
48 WITHOUT ANY WARRANTY; without even the implied warranty of
49 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
50 General Public License for more details.
52 You should have received a copy of the GNU General Public License
53 along with this program; if not, write to the Free Software
54 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
57 Bacula® is a registered trademark of John Walker.
58 The licensor of Bacula is the Free Software Foundation Europe
59 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zurich,
60 Switzerland, email:ftf@fsfeurope.org.
62 Base 64 functions from Karl Hakimian <hakimian@aha.com>
63 Integrally copied from recover.pl from bacula source distribution.
67 use Gtk2; # auto-initialize Gtk2
69 use Gtk2::SimpleList; # easy wrapper for list views
70 use Gtk2::Gdk::Keysyms; # keyboard code constants
71 use Data::Dumper qw/Dumper/;
73 my $debug=0; # can be on brestore.conf
75 ################################################################
77 package DlgFileVersion;
79 sub on_versions_close_clicked
81 my ($self, $widget)=@_;
82 $self->{version}->destroy();
85 sub on_selection_button_press_event
87 print STDERR "on_selection_button_press_event()\n";
92 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
94 DlgResto::drag_set_info($widget,
101 my ($class, $dbh, $client, $path, $file) = @_;
104 version => undef, # main window
107 # we load version widget of $glade_file
108 my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_version");
110 # Connect signals magically
111 $glade_box->signal_autoconnect_from_package($self);
113 $glade_box->get_widget("version_label")
114 ->set_markup("<b>File revisions : $client:$path/$file</b>");
116 my $widget = $glade_box->get_widget('version_fileview');
117 my $fileview = Gtk2::SimpleList->new_from_treeview(
119 'h_name' => 'hidden',
120 'h_jobid' => 'hidden',
121 'h_type' => 'hidden',
123 'InChanger' => 'pixbuf',
130 DlgResto::init_drag_drop($fileview);
132 my @v = DlgResto::get_all_file_versions($dbh,
138 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
140 my $icon = ($inchanger)?$DlgResto::yesicon:$DlgResto::noicon;
142 DlgResto::listview_push($fileview,
143 $file, $jobid, 'file',
144 $icon, $volname, $jobid,DlgResto::human($size),
145 scalar(localtime($mtime)), $md5);
148 $self->{version} = $glade_box->get_widget('dlg_version');
149 $self->{version}->show();
154 sub on_forward_keypress
160 ################################################################
165 my ($package, $text) = @_;
169 my $glade = Gtk2::GladeXML->new($glade_file, "dlg_warn");
171 # Connect signals magically
172 $glade->signal_autoconnect_from_package($self);
173 $glade->get_widget('label_warn')->set_text($text);
175 print STDERR "$text\n";
177 $self->{window} = $glade->get_widget('dlg_warn');
178 $self->{window}->show_all();
185 $self->{window}->destroy();
189 ################################################################
192 use HTTP::Request::Common;
196 my ($class, %arg) = @_;
199 pref => $arg{pref}, # Pref object
200 timeout => $arg{timeout} || 20,
201 debug => $arg{debug} || 0,
204 'list_fileset' => '',
205 'list_storage' => '',
214 my ($self, @what) = @_;
215 my $ua = LWP::UserAgent->new();
216 $ua->agent("Brestore ");
217 my $req = POST($self->{pref}->{bconsole},
218 Content_Type => 'form-data',
219 Content => [ map { (action => $_) } @what ]);
220 #$req->authorization_basic('eric', 'test');
222 my $res = $ua->request($req);
224 if ($res->is_success) {
225 foreach my $l (split(/\n/, $res->content)) {
226 my ($k, $c) = split(/=/,$l,2);
230 $self->{error} = "Can't connect to bweb : " . $res->status_line;
231 new DlgWarn($self->{error});
237 my ($self, %arg) = @_;
239 my $ua = LWP::UserAgent->new();
240 $ua->agent("Brestore ");
241 my $req = POST($self->{pref}->{bconsole},
242 Content_Type => 'form-data',
243 Content => [ job => $arg{job},
244 client => $arg{client},
245 storage => $arg{storage} || '',
246 fileset => $arg{fileset} || '',
247 where => $arg{where},
248 replace => $arg{replace},
249 priority=> $arg{prio} || '',
252 bootstrap => [$arg{bootstrap}],
254 #$req->authorization_basic('eric', 'test');
256 my $res = $ua->request($req);
258 if ($res->is_success) {
259 foreach my $l (split(/\n/, $res->content)) {
260 my ($k, $c) = split(/=/,$l,2);
266 unlink($arg{bootstrap});
267 new DlgWarn("Can't connect to bweb : " . $res->status_line);
276 return sort split(/;/, $self->{'list_job'});
282 return sort split(/;/, $self->{'list_fileset'});
288 return sort split(/;/, $self->{'list_storage'});
293 return sort split(/;/, $self->{'list_client'});
298 ################################################################
304 # %arg = (bsr_file => '/path/to/bsr', # on director
305 # volumes => [ '00001', '00004']
313 if ($pref->{bconsole} =~ /^http/) {
314 return new BwebConsole(pref => $pref);
316 if (eval { require Bconsole; }) {
317 return new Bconsole(pref => $pref);
319 new DlgWarn("Can't use bconsole, verify your setup");
327 my ($class, %arg) = @_;
330 bsr_file => $arg{bsr_file}, # /path/to/bsr on director
331 pref => $arg{pref}, # Pref ref
332 glade => undef, # GladeXML ref
333 bconsole => undef, # Bconsole ref
336 my $console = $self->{bconsole} = get_bconsole($arg{pref});
341 # we load launch widget of $glade_file
342 my $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file,
345 # Connect signals magically
346 $glade->signal_autoconnect_from_package($self);
348 my $widget = $glade->get_widget('volumeview');
349 my $volview = Gtk2::SimpleList->new_from_treeview(
351 'InChanger' => 'pixbuf',
355 my $infos = get_volume_inchanger($arg{pref}->{dbh}, $arg{volumes}) ;
357 # we replace 0 and 1 by $noicon and $yesicon
358 for my $i (@{$infos}) {
360 $i->[0] = $DlgResto::noicon;
362 $i->[0] = $DlgResto::yesicon;
367 push @{ $volview->{data} }, @{$infos} ;
369 $console->prepare(qw/list_client list_job list_fileset list_storage/);
371 # fill client combobox (with director defined clients
372 my @clients = $console->list_client() ; # get from bconsole
373 if ($console->{error}) {
374 new DlgWarn("Can't use bconsole:\n$arg{pref}->{bconsole}: $console->{error}") ;
376 my $w = $self->{combo_client} = $glade->get_widget('combo_launch_client') ;
377 $self->{list_client} = DlgResto::init_combo($w, 'text');
378 DlgResto::fill_combo($self->{list_client},
379 $DlgResto::client_list_empty,
383 # fill fileset combobox
384 my @fileset = $console->list_fileset() ;
385 $w = $self->{combo_fileset} = $glade->get_widget('combo_launch_fileset') ;
386 $self->{list_fileset} = DlgResto::init_combo($w, 'text');
387 DlgResto::fill_combo($self->{list_fileset}, '', @fileset);
390 my @job = $console->list_job() ;
391 $w = $self->{combo_job} = $glade->get_widget('combo_launch_job') ;
392 $self->{list_job} = DlgResto::init_combo($w, 'text');
393 DlgResto::fill_combo($self->{list_job}, '', @job);
395 # find default_restore_job in jobs list
396 my $default_restore_job = $arg{pref}->{default_restore_job} ;
400 if ($j =~ /$default_restore_job/io) {
406 $w->set_active($index);
408 # fill storage combobox
409 my @storage = $console->list_storage() ;
410 $w = $self->{combo_storage} = $glade->get_widget('combo_launch_storage') ;
411 $self->{list_storage} = DlgResto::init_combo($w, 'text');
412 DlgResto::fill_combo($self->{list_storage}, '', @storage);
414 $glade->get_widget('dlg_launch')->show_all();
421 my ($self, $client, $jobid) = @_;
423 my $ret = $self->{pref}->go_bweb("?action=dsp_cur_job;jobid=$jobid;client=$client", "view job status");
426 my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'info', 'close',
427 "Your job have been submited to bacula.
428 To follow it, you must use bconsole (or install/configure bweb)");
433 $self->on_cancel_resto_clicked();
436 sub on_cancel_resto_clicked
439 $self->{glade}->get_widget('dlg_launch')->destroy();
442 sub on_submit_resto_clicked
445 my $glade = $self->{glade};
447 my $r = $self->copy_bsr($self->{bsr_file}, $self->{pref}->{bsr_dest}) ;
450 new DlgWarn("Can't copy bsr file to director ($self->{error})");
454 my $fileset = $glade->get_widget('combo_launch_fileset')
457 my $storage = $glade->get_widget('combo_launch_storage')
460 my $where = $glade->get_widget('entry_launch_where')->get_text();
462 my $job = $glade->get_widget('combo_launch_job')
466 new DlgWarn("Can't use this job");
470 my $client = $glade->get_widget('combo_launch_client')
473 if (! $client or $client eq $DlgResto::client_list_empty) {
474 new DlgWarn("Can't use this client ($client)");
478 my $prio = $glade->get_widget('spin_launch_priority')->get_value();
480 my $replace = $glade->get_widget('chkbp_launch_replace')->get_active();
481 $replace=($replace)?'always':'never';
483 my $jobid = $self->{bconsole}->run(job => $job,
492 $self->show_job($client, $jobid);
495 sub on_combo_storage_button_press_event
498 print "on_combo_storage_button_press_event()\n";
501 sub on_combo_fileset_button_press_event
504 print "on_combo_fileset_button_press_event()\n";
508 sub on_combo_job_button_press_event
511 print "on_combo_job_button_press_event()\n";
514 sub get_volume_inchanger
516 my ($dbh, $vols) = @_;
518 my $lst = join(',', map { $dbh->quote($_) } @{ $vols } ) ;
520 my $rq = "SELECT InChanger, VolumeName
522 WHERE VolumeName IN ($lst)
525 my $res = $dbh->selectall_arrayref($rq);
526 return $res; # [ [ 1, VolName].. ]
530 use File::Copy qw/copy/;
531 use File::Basename qw/basename/;
533 # We must kown the path+filename destination
534 # $self->{error} contains error message
535 # it return 0/1 if fail/success
538 my ($self, $src, $dst) = @_ ;
539 print "$src => $dst\n"
546 if ($dst =~ m!file:/(/.+)!) {
547 $ret = copy($src, $1);
549 $dstfile = "$1/" . basename($src) ;
551 } elsif ($dst =~ m!scp://([^:]+:(.+))!) {
552 $err = `scp $src $1 2>&1` ;
554 $dstfile = "$2/" . basename($src) ;
558 $err = "$dst not implemented yet";
559 File::Copy::copy($src, \*STDOUT);
562 $self->{error} = $err;
565 $self->{error} = $err;
574 ################################################################
582 unless ($about_widget) {
583 my $glade_box = Gtk2::GladeXML->new($glade_file, "dlg_about") ;
584 $about_widget = $glade_box->get_widget("dlg_about") ;
585 $glade_box->signal_autoconnect_from_package('DlgAbout');
587 $about_widget->show() ;
590 sub on_about_okbutton_clicked
592 $about_widget->hide() ;
597 ################################################################
603 my ($class, $config_file) = @_;
606 config_file => $config_file,
607 password => '', # db passwd
608 username => '', # db username
609 connection_string => '',# db connection string
610 bconsole => 'bconsole', # path and arg to bconsole
611 bsr_dest => '', # destination url for bsr files
612 debug => 0, # debug level 0|1
613 use_ok_bkp_only => 1, # dont use bad backup
614 bweb => 'http://localhost/cgi-bin/bweb/bweb.pl', # bweb url
615 glade_file => $glade_file,
616 see_all_versions => 0, # display all file versions in FileInfo
617 mozilla => 'mozilla', # mozilla bin
618 default_restore_job => 'restore', # regular expression to select default
621 # keywords that are used to fill DlgPref
622 chk_keyword => [ qw/use_ok_bkp_only debug see_all_versions/ ],
623 entry_keyword => [ qw/username password bweb mozilla
624 connection_string default_restore_job
625 bconsole bsr_dest glade_file/],
628 $self->read_config();
637 # We read the parameters. They come from the configuration files
638 my $cfgfile ; my $tmpbuffer;
639 if (open FICCFG, $self->{config_file})
641 while(read FICCFG,$tmpbuffer,4096)
643 $cfgfile .= $tmpbuffer;
647 no strict; # I have no idea of the contents of the file
648 eval '$refparams' . " = $cfgfile";
651 for my $p (keys %{$refparams}) {
652 $self->{$p} = $refparams->{$p};
655 if (defined $self->{debug}) {
656 $debug = $self->{debug} ;
659 # TODO : Force dumb default values and display a message
669 for my $k (@{ $self->{entry_keyword} }) {
670 $parameters{$k} = $self->{$k};
673 for my $k (@{ $self->{chk_keyword} }) {
674 $parameters{$k} = $self->{$k};
677 if (open FICCFG,">$self->{config_file}")
679 print FICCFG Data::Dumper->Dump([\%parameters], [qw($parameters)]);
684 # TODO : Display a message
693 $self->{dbh}->disconnect() ;
697 delete $self->{error};
699 if (not $self->{connection_string})
701 # The parameters have not been set. Maybe the conf
702 # file is empty for now
703 $self->{error} = "No configuration found for database connection. " .
704 "Please set this up.";
709 $self->{dbh} = DBI->connect($self->{connection_string},
714 $self->{error} = "Can't open bacula database. " .
715 "Database connect string '" .
716 $self->{connection_string} ."' $!";
719 $self->{dbh}->{RowCacheSize}=100;
725 my ($self, $url, $msg) = @_;
727 unless ($self->{mozilla} and $self->{bweb}) {
728 new DlgWarn("You must install Bweb and set your mozilla bin to $msg");
732 if ($^O eq 'MSWin32') {
733 system("start /B $self->{mozilla} \"$self->{bweb}$url\"");
736 system("$self->{mozilla} -remote 'Ping()'");
737 my $cmd = "$self->{mozilla} '$self->{bweb}$url'";
739 $cmd = "$self->{mozilla} -remote 'OpenURL($self->{bweb}$url,new-tab)'" ;
749 ################################################################
753 # my $pref = new Pref(config_file => 'brestore.conf');
754 # my $dlg = new DlgPref($pref);
755 # my $dlg_resto = new DlgResto($pref);
756 # $dlg->display($dlg_resto);
759 my ($class, $pref) = @_;
762 pref => $pref, # Pref ref
763 dlgresto => undef, # DlgResto ref
771 my ($self, $dlgresto) = @_ ;
773 unless ($self->{glade}) {
774 $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_pref") ;
775 $self->{glade}->signal_autoconnect_from_package($self);
778 $self->{dlgresto} = $dlgresto;
780 my $g = $self->{glade};
781 my $p = $self->{pref};
783 for my $k (@{ $p->{entry_keyword} }) {
784 $g->get_widget("entry_$k")->set_text($p->{$k}) ;
787 for my $k (@{ $p->{chk_keyword} }) {
788 $g->get_widget("chkbp_$k")->set_active($p->{$k}) ;
791 $g->get_widget("dlg_pref")->show_all() ;
794 sub on_applybutton_clicked
797 my $glade = $self->{glade};
798 my $pref = $self->{pref};
800 for my $k (@{ $pref->{entry_keyword} }) {
801 my $w = $glade->get_widget("entry_$k") ;
802 $pref->{$k} = $w->get_text();
805 for my $k (@{ $pref->{chk_keyword} }) {
806 my $w = $glade->get_widget("chkbp_$k") ;
807 $pref->{$k} = $w->get_active();
810 $pref->write_config();
811 if ($pref->connect_db()) {
812 $self->{dlgresto}->set_dbh($pref->{dbh});
813 $self->{dlgresto}->set_status('Preferences updated');
814 $self->{dlgresto}->init_server_backup_combobox();
815 $self->{dlgresto}->create_brestore_tables();
816 $self->{dlgresto}->set_status($pref->{error});
818 $self->{dlgresto}->set_status($pref->{error});
822 # Handle prefs ok click (apply/dismiss dialog)
823 sub on_okbutton_clicked
826 $self->on_applybutton_clicked();
828 unless ($self->{pref}->{error}) {
829 $self->on_cancelbutton_clicked();
832 sub on_dialog_delete_event
835 $self->on_cancelbutton_clicked();
839 sub on_cancelbutton_clicked
842 $self->{glade}->get_widget('dlg_pref')->hide();
843 delete $self->{dlgresto};
847 ################################################################
857 # Kept as is from the perl-gtk example. Draws the pretty icons
863 $diricon = $self->{mainwin}->render_icon('gtk-open', $size);
864 $fileicon = $self->{mainwin}->render_icon('gtk-new', $size);
865 $yesicon = $self->{mainwin}->render_icon('gtk-yes', $size);
866 $noicon = $self->{mainwin}->render_icon('gtk-no', $size);
870 # init combo (and create ListStore object)
873 my ($widget, @type) = @_ ;
874 my %type_info = ('text' => 'Glib::String',
875 'markup' => 'Glib::String',
878 my $lst = new Gtk2::ListStore ( map { $type_info{$_} } @type );
880 $widget->set_model($lst);
884 if ($t eq 'text' or $t eq 'markup') {
885 $cell = new Gtk2::CellRendererText();
887 $widget->pack_start($cell, 1);
888 $widget->add_attribute($cell, $t, $i++);
893 # fill simple combo (one element per row)
896 my ($list, @what) = @_;
900 foreach my $w (@what)
903 my $i = $list->append();
904 $list->set($i, 0, $w);
911 my @unit = qw(b Kb Mb Gb Tb);
914 my $format = '%i %s';
915 while ($val / 1024 > 1) {
919 $format = ($i>0)?'%0.1f %s':'%i %s';
920 return sprintf($format, $val, $unit[$i]);
925 my ($self, $dbh) = @_;
931 my ($fileview) = shift;
932 my $fileview_target_entry = {target => 'STRING',
933 flags => ['GTK_TARGET_SAME_APP'],
936 $fileview->enable_model_drag_source(['button1_mask', 'button3_mask'],
937 ['copy'],$fileview_target_entry);
938 $fileview->get_selection->set_mode('multiple');
940 # set some useful SimpleList properties
941 $fileview->set_headers_clickable(0);
942 foreach ($fileview->get_columns())
944 $_->set_resizable(1);
945 $_->set_sizing('grow-only');
951 my ($self, $what) = @_;
955 print Data::Dumper::Dumper($what);
956 } elsif (defined $what) {
964 my ($self, $query) = @_;
965 $self->debug($query);
966 return $self->{dbh}->prepare($query);
971 my ($self, $query) = @_;
972 $self->debug($query);
973 return $self->{dbh}->do($query);
976 sub dbh_selectall_arrayref
978 my ($self, $query) = @_;
979 $self->debug($query);
980 return $self->{dbh}->selectall_arrayref($query);
983 sub dbh_selectrow_arrayref
985 my ($self, $query) = @_;
986 $self->debug($query);
987 return $self->{dbh}->selectrow_arrayref($query);
992 my ($class, $pref) = @_;
997 location => undef, # location entry widget
998 mainwin => undef, # mainwin widget
999 filelist_file_menu => undef, # file menu widget
1000 filelist_dir_menu => undef, # dir menu widget
1001 glade => undef, # glade object
1002 status => undef, # status bar widget
1003 dlg_pref => undef, # DlgPref object
1004 fileattrib => {}, # cache file
1005 fileview => undef, # fileview widget SimpleList
1006 fileinfo => undef, # fileinfo widget SimpleList
1008 client_combobox => undef, # client_combobox widget
1009 restore_backup_combobox => undef, # date combobox widget
1010 list_client => undef, # Gtk2::ListStore
1011 list_backup => undef, # Gtk2::ListStore
1012 cache_ppathid => {}, #
1015 # load menu (to use handler with self reference)
1016 my $glade = Gtk2::GladeXML->new($glade_file, "filelist_file_menu");
1017 $glade->signal_autoconnect_from_package($self);
1018 $self->{filelist_file_menu} = $glade->get_widget("filelist_file_menu");
1020 $glade = Gtk2::GladeXML->new($glade_file, "filelist_dir_menu");
1021 $glade->signal_autoconnect_from_package($self);
1022 $self->{filelist_dir_menu} = $glade->get_widget("filelist_dir_menu");
1024 $glade = $self->{glade} = Gtk2::GladeXML->new($glade_file, "dlg_resto");
1025 $glade->signal_autoconnect_from_package($self);
1027 $self->{status} = $glade->get_widget('statusbar');
1028 $self->{mainwin} = $glade->get_widget('dlg_resto');
1029 $self->{location} = $glade->get_widget('entry_location');
1030 $self->render_icons();
1032 $self->{dlg_pref} = new DlgPref($pref);
1034 my $c = $self->{client_combobox} = $glade->get_widget('combo_client');
1035 $self->{list_client} = init_combo($c, 'text');
1037 $c = $self->{restore_backup_combobox} = $glade->get_widget('combo_list_backups');
1038 $self->{list_backup} = init_combo($c, 'text', 'markup');
1040 # Connect glade-fileview to Gtk2::SimpleList
1041 # and set up drag n drop between $fileview and $restore_list
1043 # WARNING : we have big dirty thinks with gtk/perl and utf8/iso strings
1044 # we use an hidden field uuencoded to bypass theses bugs (h_name)
1046 my $widget = $glade->get_widget('fileview');
1047 my $fileview = $self->{fileview} = Gtk2::SimpleList->new_from_treeview(
1049 'h_name' => 'hidden',
1050 'h_jobid' => 'hidden',
1051 'h_type' => 'hidden',
1054 'File Name' => 'text',
1057 init_drag_drop($fileview);
1058 $fileview->set_search_column(4); # search on File Name
1060 # Connect glade-restore_list to Gtk2::SimpleList
1061 $widget = $glade->get_widget('restorelist');
1062 my $restore_list = $self->{restore_list} = Gtk2::SimpleList->new_from_treeview(
1064 'h_name' => 'hidden',
1065 'h_jobid' => 'hidden',
1066 'h_type' => 'hidden',
1067 'h_curjobid' => 'hidden',
1070 'File Name' => 'text',
1072 'FileIndex' => 'text',
1074 'Nb Files' => 'text', #8
1075 'Size' => 'text', #9
1076 'size_b' => 'hidden', #10
1079 my @restore_list_target_table = ({'target' => 'STRING',
1083 $restore_list->enable_model_drag_dest(['copy'],@restore_list_target_table);
1084 $restore_list->get_selection->set_mode('multiple');
1086 $widget = $glade->get_widget('infoview');
1087 my $infoview = $self->{fileinfo} = Gtk2::SimpleList->new_from_treeview(
1089 'h_name' => 'hidden',
1090 'h_jobid' => 'hidden',
1091 'h_type' => 'hidden',
1093 'InChanger' => 'pixbuf',
1100 init_drag_drop($infoview);
1102 $pref->connect_db() || $self->{dlg_pref}->display($self);
1105 $self->{dbh} = $pref->{dbh};
1106 $self->init_server_backup_combobox();
1107 $self->create_brestore_tables();
1110 $self->set_status($pref->{error});
1113 # set status bar informations
1116 my ($self, $string) = @_;
1117 return unless ($string);
1119 my $context = $self->{status}->get_context_id('Main');
1120 $self->{status}->push($context, $string);
1123 sub on_time_select_changed
1131 my $c = $self->{glade}->get_widget('combo_time');
1132 return $c->get_active_text;
1135 # This sub returns all clients declared in DB
1139 my $query = "SELECT Name FROM Client ORDER BY Name";
1140 print STDERR $query,"\n" if $debug;
1142 my $result = $dbh->selectall_arrayref($query);
1144 return map { $_->[0] } @$result;
1147 sub get_wanted_job_status
1154 return "'T', 'A', 'E'";
1158 # This sub gives a full list of the EndTimes for a ClientId
1159 # ( [ 'Date', 'FileSet', 'Type', 'Status', 'JobId'],
1160 # ['Date', 'FileSet', 'Type', 'Status', 'JobId']..)
1161 sub get_all_endtimes_for_job
1163 my ($dbh, $client, $ok_only)=@_;
1164 my $status = get_wanted_job_status($ok_only);
1166 SELECT Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus, Job.JobId
1167 FROM Job,Client,FileSet
1168 WHERE Job.ClientId=Client.ClientId
1169 AND Client.Name = '$client'
1171 AND JobStatus IN ($status)
1172 AND Job.FileSetId = FileSet.FileSetId
1173 ORDER BY EndTime desc";
1174 print STDERR $query,"\n" if $debug;
1175 my $result = $dbh->selectall_arrayref($query);
1181 # init infoview widget
1185 @{$self->{fileinfo}->{data}} = ();
1189 sub on_clear_clicked
1192 @{$self->{restore_list}->{data}} = ();
1195 sub on_estimate_clicked
1202 # TODO : If we get here, things could get lenghty ... draw a popup window .
1203 my $widget = Gtk2::MessageDialog->new($self->{mainwin},
1204 'destroy-with-parent',
1206 'Computing size...');
1210 my $title = "Computing size...\n";
1212 foreach my $entry (@{$self->{restore_list}->{data}})
1214 unless ($entry->[9]) {
1215 my ($size, $nb) = $self->estimate_restore_size($entry);
1216 $entry->[10] = $size;
1217 $entry->[9] = human($size);
1221 my $name = unpack('u', $entry->[0]);
1223 $txt .= "\n<i>$name</i> : " . $entry->[8] . " file(s)/" . $entry->[9] ;
1224 $widget->set_markup($title . $txt);
1226 $size_total+=$entry->[10];
1227 $nb_total+=$entry->[8];
1231 $txt .= "\n\n<b>Total</b> : $nb_total file(s)/" . human($size_total);
1232 $widget->set_markup("Size estimation :\n" . $txt);
1233 $widget->signal_connect ("response", sub { my $w=shift; $w->destroy();});
1238 sub on_gen_bsr_clicked
1242 my @options = ("Choose a bsr file", $self->{mainwin}, 'save',
1243 'gtk-save','ok', 'gtk-cancel', 'cancel');
1246 my $w = new Gtk2::FileChooserDialog ( @options );
1251 if ($a eq 'cancel') {
1256 my $f = $w->get_filename();
1258 my $dlg = Gtk2::MessageDialog->new($self->{mainwin},
1259 'destroy-with-parent',
1260 'warning', 'ok-cancel', 'This file already exists, do you want to overwrite it ?');
1261 if ($dlg->run() eq 'ok') {
1275 if (open(FP, ">$save")) {
1276 my $bsr = $self->create_filelist();
1279 $self->set_status("Dumping BSR to $save ok");
1281 $self->set_status("Can't dump BSR to $save: $!");
1286 use File::Temp qw/tempfile/;
1288 sub on_go_button_clicked
1291 unless (scalar(@{$self->{restore_list}->{data}})) {
1292 new DlgWarn("No file to restore");
1295 my $bsr = $self->create_filelist();
1296 my ($fh, $filename) = tempfile();
1299 chmod(0644, $filename);
1301 print "Dumping BSR info to $filename\n"
1304 # we get Volume list
1305 my %a = map { $_ => 1 } ($bsr =~ /Volume="(.+)"/g);
1306 my $vol = [ keys %a ] ; # need only one occurrence of each volume
1308 new DlgLaunch(pref => $self->{pref},
1310 bsr_file => $filename,
1315 our $client_list_empty = 'Clients list';
1316 our %type_markup = ('F' => '<b>$label F</b>',
1319 'B' => '<b>$label B</b>',
1321 'A' => '<span foreground=\"red\">$label</span>',
1323 'E' => '<span foreground=\"red\">$label</span>',
1326 sub on_list_client_changed
1328 my ($self, $widget) = @_;
1329 return 0 unless defined $self->{fileview};
1330 my $dbh = $self->{dbh};
1332 $self->{list_backup}->clear();
1334 if ($self->current_client eq $client_list_empty) {
1338 my @endtimes=get_all_endtimes_for_job($dbh,
1339 $self->current_client,
1340 $self->{pref}->{use_ok_bkp_only});
1341 foreach my $endtime (@endtimes)
1343 my $i = $self->{list_backup}->append();
1345 my $label = $endtime->[1] . " (" . $endtime->[4] . ")";
1346 eval "\$label = \"$type_markup{$endtime->[2]}\""; # job type
1347 eval "\$label = \"$type_markup{$endtime->[3]}\""; # job status
1349 $self->{list_backup}->set($i,
1354 $self->{restore_backup_combobox}->set_active(0);
1356 $self->{CurrentJobIds} = [
1357 set_job_ids_for_date($dbh,
1358 $self->current_client,
1359 $self->current_date,
1360 $self->{pref}->{use_ok_bkp_only})
1363 $self->update_brestore_table(@{$self->{CurrentJobIds}});
1366 $self->refresh_fileview();
1370 sub fill_server_list
1372 my ($dbh, $combo, $list) = @_;
1374 my @clients=get_all_clients($dbh);
1378 my $i = $list->append();
1379 $list->set($i, 0, $client_list_empty);
1381 foreach my $client (@clients)
1383 $i = $list->append();
1384 $list->set($i, 0, $client);
1386 $combo->set_active(0);
1389 sub init_server_backup_combobox
1392 fill_server_list($self->{dbh},
1393 $self->{client_combobox},
1394 $self->{list_client}) ;
1397 #----------------------------------------------------------------------
1398 #Refreshes the file-view Redraws everything. The dir data is cached, the file
1399 #data isn't. There is additionnal complexity for dirs (visibility problems),
1400 #so the @CurrentJobIds is not sufficient.
1401 sub refresh_fileview
1404 my $fileview = $self->{fileview};
1405 my $client_combobox = $self->{client_combobox};
1406 my $cwd = $self->{cwd};
1408 @{$fileview->{data}} = ();
1410 $self->clear_infoview();
1412 my $client_name = $self->current_client;
1414 if (!$client_name or ($client_name eq $client_list_empty)) {
1415 $self->set_status("Client list empty");
1419 my @list_dirs = $self->list_dirs($cwd,$client_name);
1420 # [ [listfiles.id, listfiles.Name, File.LStat, File.JobId]..]
1421 my $files = $self->list_files($cwd);
1422 print "CWD : $cwd\n" if ($debug);
1424 my $file_count = 0 ;
1425 my $total_bytes = 0;
1427 # Add directories to view
1428 foreach my $dir_entry (@list_dirs) {
1429 #my $time = localtime($self->dir_attrib("$cwd/$dir",'st_mtime'));
1430 my $time = localtime(lstat_attrib($dir_entry->[1],'st_mtime'));
1431 my $dir = $dir_entry->[0];
1432 $total_bytes += 4096;
1435 listview_push($fileview,
1437 $self->dir_attrib("$cwd/$dir",'jobid'),
1447 foreach my $file (@$files)
1449 my $size = file_attrib($file,'st_size');
1450 my $time = localtime(file_attrib($file,'st_mtime'));
1451 $total_bytes += $size;
1453 # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
1455 listview_push($fileview,
1462 human($size), $time);
1465 $self->set_status("$file_count files/" . human($total_bytes));
1467 # set a decent default selection (makes keyboard nav easy)
1468 $fileview->select(0);
1472 sub on_about_activate
1474 DlgAbout::display();
1479 my ($tree, $path, $data) = @_;
1481 my @items = listview_get_all($tree) ;
1483 foreach my $i (@items)
1485 my @file_info = @{$i};
1488 # Ok, we have a corner case :
1493 $file = pack("u", $file_info[0]);
1497 $file = pack("u", $path . '/' . $file_info[0]);
1499 push @ret, join(" ; ", $file,
1500 $file_info[1], # $jobid
1501 $file_info[2], # $type
1505 my $data_get = join(" :: ", @ret);
1507 $data->set_text($data_get,-1);
1510 sub fileview_data_get
1512 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1513 drag_set_info($widget, $self->{cwd}, $data);
1516 sub fileinfo_data_get
1518 my ($self, $widget, $context, $data, $info, $time,$string) = @_;
1519 drag_set_info($widget, $self->{cwd}, $data);
1522 sub restore_list_data_received
1524 my ($self, $widget, $context, $x, $y, $data, $info, $time) = @_;
1527 if ($info eq 40 || $info eq 0) # patch for display!=:0
1529 foreach my $elt (split(/ :: /, $data->data()))
1532 my ($file, $jobid, $type) =
1534 $file = unpack("u", $file);
1536 $self->add_selected_file_to_list($file, $jobid, $type);
1541 sub on_back_button_clicked {
1545 sub on_location_go_button_clicked
1548 $self->ch_dir($self->{location}->get_text());
1550 sub on_quit_activate {Gtk2->main_quit;}
1551 sub on_preferences_activate
1554 $self->{dlg_pref}->display($self) ;
1556 sub on_main_delete_event {Gtk2->main_quit;}
1557 sub on_bweb_activate
1560 $self->set_status("Open bweb on your browser");
1561 $self->{pref}->go_bweb('', "go on bweb");
1564 # Change to parent directory
1568 if ($self->{cwd} eq '/')
1572 my @dirs = split(/\//, $self->{cwd});
1574 $self->ch_dir(join('/', @dirs));
1577 # Change the current working directory
1578 # * Updates fileview, location, and selection
1583 $self->{cwd} = shift;
1585 $self->refresh_fileview();
1586 $self->{location}->set_text($self->{cwd});
1591 # Handle dialog 'close' (window-decoration induced close)
1592 # * Just hide the dialog, and tell Gtk not to do anything else
1596 my ($self, $w) = @_;
1599 1; # consume this event!
1602 # Handle key presses in location text edit control
1603 # * Translate a Return/Enter key into a 'Go' command
1604 # * All other key presses left for GTK
1606 sub on_location_entry_key_release_event
1612 my $keypress = $event->keyval;
1613 if ($keypress == $Gtk2::Gdk::Keysyms{KP_Enter} ||
1614 $keypress == $Gtk2::Gdk::Keysyms{Return})
1616 $self->ch_dir($widget->get_text());
1618 return 1; # consume keypress
1621 return 0; # let gtk have the keypress
1624 sub on_fileview_key_press_event
1626 my ($self, $widget, $event) = @_;
1630 sub listview_get_first
1633 my @selected = $list->get_selected_indices();
1634 if (@selected > 0) {
1635 my ($name, @other) = @{$list->{data}->[$selected[0]]};
1636 return (unpack('u', $name), @other);
1642 sub listview_get_all
1646 my @selected = $list->get_selected_indices();
1648 for my $i (@selected) {
1649 my ($name, @other) = @{$list->{data}->[$i]};
1650 push @ret, [unpack('u', $name), @other];
1658 my ($list, $name, @other) = @_;
1659 push @{$list->{data}}, [pack('u', $name), @other];
1662 #----------------------------------------------------------------------
1663 # Handle keypress in file-view
1664 # * Translates backspace into a 'cd ..' command
1665 # * All other key presses left for GTK
1667 sub on_fileview_key_release_event
1669 my ($self, $widget, $event) = @_;
1670 if (not $event->keyval)
1674 if ($event->keyval == $Gtk2::Gdk::Keysyms{BackSpace}) {
1676 return 1; # eat keypress
1679 return 0; # let gtk have keypress
1682 sub on_forward_keypress
1687 #----------------------------------------------------------------------
1688 # Handle double-click (or enter) on file-view
1689 # * Translates into a 'cd <dir>' command
1691 sub on_fileview_row_activated
1693 my ($self, $widget) = @_;
1695 my ($name, undef, $type, undef) = listview_get_first($widget);
1699 if ($self->{cwd} eq '')
1701 $self->ch_dir($name);
1703 elsif ($self->{cwd} eq '/')
1705 $self->ch_dir('/' . $name);
1709 $self->ch_dir($self->{cwd} . '/' . $name);
1713 $self->fill_infoview($self->{cwd}, $name);
1716 return 1; # consume event
1721 my ($self, $path, $file) = @_;
1722 $self->clear_infoview();
1723 my @v = get_all_file_versions($self->{dbh},
1726 $self->current_client,
1727 $self->{pref}->{see_all_versions});
1729 my (undef,$fn,$jobid,$fileindex,$mtime,$size,$inchanger,$md5,$volname)
1731 my $icon = ($inchanger)?$yesicon:$noicon;
1733 $mtime = localtime($mtime) ;
1735 listview_push($self->{fileinfo},
1736 $file, $jobid, 'file',
1737 $icon, $volname, $jobid, human($size), $mtime, $md5);
1744 return $self->{restore_backup_combobox}->get_active_text;
1750 return $self->{client_combobox}->get_active_text;
1753 sub on_list_backups_changed
1755 my ($self, $widget) = @_;
1756 return 0 unless defined $self->{fileview};
1758 $self->{CurrentJobIds} = [
1759 set_job_ids_for_date($self->{dbh},
1760 $self->current_client,
1761 $self->current_date,
1762 $self->{pref}->{use_ok_bkp_only})
1764 $self->update_brestore_table(@{$self->{CurrentJobIds}});
1766 $self->refresh_fileview();
1770 sub on_restore_list_keypress
1772 my ($self, $widget, $event) = @_;
1773 if ($event->keyval == $Gtk2::Gdk::Keysyms{Delete})
1775 my @sel = $widget->get_selected_indices;
1776 foreach my $elt (reverse(sort {$a <=> $b} @sel))
1778 splice @{$self->{restore_list}->{data}},$elt,1;
1783 sub on_fileview_button_press_event
1785 my ($self,$widget,$event) = @_;
1786 if ($event->button == 3)
1788 $self->on_right_click_filelist($widget,$event);
1792 if ($event->button == 2)
1794 $self->on_see_all_version();
1801 sub on_see_all_version
1805 my @lst = listview_get_all($self->{fileview});
1808 my ($name, undef) = @{$i};
1810 new DlgFileVersion($self->{dbh},
1811 $self->current_client,
1812 $self->{cwd}, $name);
1816 sub on_right_click_filelist
1818 my ($self,$widget,$event) = @_;
1819 # I need to know what's selected
1820 my @sel = listview_get_all($self->{fileview});
1825 $type = $sel[0]->[2]; # $type
1830 if (@sel >=2 or $type eq 'dir')
1832 # We have selected more than one or it is a directories
1833 $w = $self->{filelist_dir_menu};
1837 $w = $self->{filelist_file_menu};
1843 $event->button, $event->time);
1846 sub context_add_to_filelist
1850 my @sel = listview_get_all($self->{fileview});
1852 foreach my $i (@sel)
1854 my ($file, $jobid, $type, undef) = @{$i};
1855 $file = $self->{cwd} . '/' . $file;
1856 $self->add_selected_file_to_list($file, $jobid, $type);
1860 # Adds a file to the filelist
1861 sub add_selected_file_to_list
1863 my ($self, $name, $jobid, $type)=@_;
1865 my $dbh = $self->{dbh};
1866 my $restore_list = $self->{restore_list};
1868 my $curjobids=join(',', @{$self->{CurrentJobIds}});
1875 if ($name and substr $name,-1 ne '/')
1877 $name .= '/'; # For bacula
1879 my $dirfileindex = get_fileindex_from_dir_jobid($dbh,$name,$jobid);
1880 listview_push($restore_list,
1881 $name, $jobid, 'dir', $curjobids,
1882 $diricon, $name,$curjobids,$dirfileindex);
1884 elsif ($type eq 'file')
1886 my $fileindex = get_fileindex_from_file_jobid($dbh,$name,$jobid);
1888 listview_push($restore_list,
1889 $name, $jobid, 'file', $curjobids,
1890 $fileicon, $name, $jobid, $fileindex );
1894 # TODO : we want be able to restore files from a bad ended backup
1895 # we have JobStatus IN ('T', 'A', 'E') and we must
1897 # Data acces subs from here. Interaction with SGBD and caching
1899 # This sub retrieves the list of jobs corresponding to the jobs selected in the
1900 # GUI and stores them in @CurrentJobIds
1901 sub set_job_ids_for_date
1903 my ($dbh, $client, $date, $only_ok)=@_;
1905 if (!$client or !$date) {
1909 my $status = get_wanted_job_status($only_ok);
1911 # The algorithm : for a client, we get all the backups for each
1912 # fileset, in reverse order Then, for each fileset, we store the 'good'
1913 # incrementals and differentials until we have found a full so it goes
1914 # like this : store all incrementals until we have found a differential
1915 # or a full, then find the full #
1917 my $query = "SELECT JobId, FileSet, Level, JobStatus
1918 FROM Job, Client, FileSet
1919 WHERE Job.ClientId = Client.ClientId
1920 AND FileSet.FileSetId = Job.FileSetId
1921 AND EndTime <= '$date'
1922 AND Client.Name = '$client'
1924 AND JobStatus IN ($status)
1925 ORDER BY FileSet, JobTDate DESC";
1927 print STDERR $query,"\n" if $debug;
1929 my $result = $dbh->selectall_arrayref($query);
1931 foreach my $refrow (@$result)
1933 my $jobid = $refrow->[0];
1934 my $fileset = $refrow->[1];
1935 my $level = $refrow->[2];
1937 defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
1939 next if $progress{$fileset} eq 'F'; # It's over for this fileset...
1943 next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
1944 push @CurrentJobIds,($jobid);
1946 elsif ($level eq 'D')
1948 next if $progress{$fileset} eq 'D'; # We allready have a differential
1949 push @CurrentJobIds,($jobid);
1951 elsif ($level eq 'F')
1953 push @CurrentJobIds,($jobid);
1956 my $status = $refrow->[3] ;
1957 if ($status eq 'T') { # good end of job
1958 $progress{$fileset} = $level;
1961 print Data::Dumper::Dumper(\@CurrentJobIds) if $debug;
1963 return @CurrentJobIds;
1966 # Lists all directories contained inside a directory.
1967 # Uses the current dir, the client name, and CurrentJobIds for visibility.
1968 # Returns an array of dirs
1971 my ($self,$dir,$client)=@_;
1973 print "list_dirs(<$dir>, <$client>)\n" if $debug;
1975 if ($dir ne '' and substr $dir,-1 ne '/')
1977 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
1980 my $dbh = $self->{dbh};
1981 my $query = "SELECT PathId FROM Path WHERE Path = ?
1982 UNION SELECT PathId FROM brestore_missing_path WHERE PATH = ?";
1983 my $sth = $dbh->prepare($query);
1984 $sth->execute($dir,$dir);
1985 my $result = $sth->fetchrow_arrayref();
1987 my $pathid = $result->[0];
1988 my @jobids = @{$self->{CurrentJobIds}};
1989 my $jobclause = join (',', @jobids);
1990 # Let's retrieve the list of the visible dirs in this dir ...
1991 # First, I need the empty filenameid to locate efficiently the dirs in the file table
1992 $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
1993 $sth = $dbh->prepare($query);
1995 $result = $sth->fetchrow_arrayref();
1997 my $dir_filenameid = $result->[0];
1999 # Then we get all the dir entries from File ...
2000 # It's ugly because there are records in brestore_missing_path ...
2002 SELECT Path, JobId, Lstat FROM(
2004 SELECT Path.Path, lower(Path.Path),
2005 listfile.JobId, listfile.Lstat
2007 SELECT DISTINCT brestore_pathhierarchy.PathId
2008 FROM brestore_pathhierarchy
2010 ON (brestore_pathhierarchy.PathId = Path.PathId)
2011 JOIN brestore_pathvisibility
2012 ON (brestore_pathhierarchy.PathId = brestore_pathvisibility.PathId)
2013 WHERE brestore_pathhierarchy.PPathId = $pathid
2014 AND brestore_pathvisibility.jobid IN ($jobclause)) AS listpath
2015 JOIN Path ON (listpath.PathId = Path.PathId)
2017 SELECT File.PathId, File.JobId, File.Lstat FROM File
2018 WHERE File.FilenameId = $dir_filenameid
2019 AND File.JobId IN ($jobclause)) AS listfile
2020 ON (listpath.PathId = listfile.PathId)
2022 SELECT brestore_missing_path.Path, lower(brestore_missing_path.Path),
2023 listfile.JobId, listfile.Lstat
2025 SELECT DISTINCT brestore_pathhierarchy.PathId
2026 FROM brestore_pathhierarchy
2027 JOIN brestore_missing_path
2028 ON (brestore_pathhierarchy.PathId = brestore_missing_path.PathId)
2029 JOIN brestore_pathvisibility
2030 ON (brestore_pathhierarchy.PathId = brestore_pathvisibility.PathId)
2031 WHERE brestore_pathhierarchy.PPathId = $pathid
2032 AND brestore_pathvisibility.jobid IN ($jobclause)) AS listpath
2033 JOIN brestore_missing_path ON (listpath.PathId = brestore_missing_path.PathId)
2035 SELECT File.PathId, File.JobId, File.Lstat FROM File
2036 WHERE File.FilenameId = $dir_filenameid
2037 AND File.JobId IN ($jobclause)) AS listfile
2038 ON (listpath.PathId = listfile.PathId))
2039 ORDER BY 2,3 DESC ) As a";
2040 print STDERR "$query\n" if $debug;
2041 $sth=$dbh->prepare($query);
2043 $result = $sth->fetchall_arrayref();
2046 foreach my $refrow (@{$result})
2048 my $dir = $refrow->[0];
2049 my $jobid = $refrow->[1];
2050 my $lstat = $refrow->[2];
2051 next if ($dir eq $prev_dir);
2052 # We have to clean up this dirname ... we only want it's 'basename'
2056 my @temp = split ('/',$dir);
2057 $return_value = pop @temp;
2061 $return_value = '/';
2063 my @return_array = ($return_value,$lstat);
2064 push @return_list,(\@return_array);
2067 return @return_list;
2071 # List all files in a directory. dir as parameter, CurrentJobIds for visibility
2072 # Returns an array of dirs
2075 my ($self, $dir)=@_;
2076 my $dbh = $self->{dbh};
2080 print "list_files($dir)\n" if $debug;
2082 if ($dir ne '' and substr $dir,-1 ne '/')
2084 $dir .= '/'; # In the db, there is a / at the end of the dirs ...
2087 my $query = "SELECT Path.PathId
2089 WHERE Path.Path = '$dir'
2091 SELECT brestore_missing_path.PathId
2092 FROM brestore_missing_path
2093 WHERE brestore_missing_path.Path = '$dir'";
2094 print $query,"\n" if $debug;
2096 my $result = $dbh->selectall_arrayref($query);
2097 foreach my $refrow (@$result)
2099 push @list_pathid,($refrow->[0]);
2102 if (@list_pathid == 0)
2104 print "No pathid found for $dir\n" if $debug;
2108 my $inlistpath = join (',', @list_pathid);
2109 my $inclause = join (',', @{$self->{CurrentJobIds}});
2110 if ($inclause eq '')
2116 "SELECT listfiles.id, listfiles.Name, File.LStat, File.JobId
2118 (SELECT Filename.Name, max(File.FileId) as id
2120 WHERE File.FilenameId = Filename.FilenameId
2121 AND Filename.Name != ''
2122 AND File.PathId IN ($inlistpath)
2123 AND File.JobId IN ($inclause)
2124 GROUP BY Filename.Name
2125 ORDER BY Filename.Name) AS listfiles,
2127 WHERE File.FileId = listfiles.id";
2129 print STDERR $query,"\n" if $debug;
2130 $result = $dbh->selectall_arrayref($query);
2137 Gtk2->main_iteration while (Gtk2->events_pending);
2140 sub create_brestore_tables
2144 my $verif = "SELECT 1 FROM brestore_knownjobid LIMIT 1";
2146 unless ($self->dbh_do($verif)) {
2147 new DlgWarn("brestore can't find brestore_xxx tables on your database. I will create them.");
2149 $self->{error} = "Creating internal brestore tables";
2151 CREATE TABLE brestore_knownjobid
2153 JobId int4 NOT NULL,
2154 CONSTRAINT brestore_knownjobid_pkey PRIMARY KEY (JobId)
2156 $self->dbh_do($req);
2159 $verif = "SELECT 1 FROM brestore_pathhierarchy LIMIT 1";
2160 unless ($self->dbh_do($verif)) {
2162 CREATE TABLE brestore_pathhierarchy
2164 PathId int4 NOT NULL,
2165 PPathId int4 NOT NULL,
2166 CONSTRAINT brestore_pathhierarchy_pkey PRIMARY KEY (PathId)
2168 $self->dbh_do($req);
2171 $req = "CREATE INDEX brestore_pathhierarchy_ppathid
2172 ON brestore_pathhierarchy (PPathId)";
2173 $self->dbh_do($req);
2176 $verif = "SELECT 1 FROM brestore_pathvisibility LIMIT 1";
2177 unless ($self->dbh_do($verif)) {
2179 CREATE TABLE brestore_pathvisibility
2181 PathId int4 NOT NULL,
2182 JobId int4 NOT NULL,
2183 CONSTRAINT brestore_pathvisibility_pkey PRIMARY KEY (JobId, PathId)
2185 $self->dbh_do($req);
2187 $req = "CREATE INDEX brestore_pathvisibility_jobid
2188 ON brestore_pathvisibility (JobId)";
2189 $self->dbh_do($req);
2192 $verif = "SELECT 1 FROM brestore_missing_path LIMIT 1";
2193 unless ($self->dbh_do($verif)) {
2195 CREATE TABLE brestore_missing_path
2197 PathId int4 NOT NULL,
2199 CONSTRAINT brestore_missing_path_pkey PRIMARY KEY (PathId)
2201 $self->dbh_do($req);
2203 $req = "CREATE INDEX brestore_missing_path_path
2204 ON brestore_missing_path (Path)";
2205 $self->dbh_do($req);
2209 # Recursive function to calculate the visibility of each directory in the cache
2210 # tree Working with references to save time and memory
2211 # For each directory, we want to propagate it's visible jobids onto it's
2212 # parents directory.
2213 # A tree is visible if
2214 # - it's been in a backup pointed by the CurrentJobIds
2215 # - one of it's subdirs is in a backup pointed by the CurrentJobIds
2216 # In the second case, the directory is visible but has no metadata.
2217 # We symbolize this with lstat = 1 for this jobid in the cache.
2219 # Input : reference directory
2220 # Output : visibility of this dir. Has to know visibility of all subdirs
2221 # to know it's visibility, hence the recursing.
2227 # Get the subdirs array references list
2228 my @list_ref_subdirs;
2229 while( my (undef,$ref_subdir) = each (%{$refdir->[0]}))
2231 push @list_ref_subdirs,($ref_subdir);
2234 # Now lets recurse over these subdirs and retrieve the reference of a hash
2235 # containing the jobs where they are visible
2236 foreach my $ref_subdir (@list_ref_subdirs)
2238 my $ref_list_jobs = list_visible($ref_subdir);
2239 foreach my $jobid (keys %$ref_list_jobs)
2241 $visibility{$jobid}=1;
2245 # Ok. Now, we've got the list of those jobs. We are going to update our
2246 # hash (element 1 of the dir array) containing our jobs Do NOT overwrite
2247 # the lstat for the known jobids. Put 1 in the new elements... But first,
2248 # let's store the current jobids
2250 foreach my $jobid (keys %{$refdir->[1]})
2252 push @known_jobids,($jobid);
2256 foreach my $jobid (keys %visibility)
2258 next if ($refdir->[1]->{$jobid});
2259 $refdir->[1]->{$jobid} = 1;
2261 # Add the known_jobids to %visibility
2262 foreach my $jobid (@known_jobids)
2264 $visibility{$jobid}=1;
2266 return \%visibility;
2269 # Returns the list of media required for a list of jobids.
2270 # Input : dbh, jobid1, jobid2...
2271 # Output : reference to array of (joibd, inchanger)
2272 sub get_required_media_from_jobid
2274 my ($dbh, @jobids)=@_;
2275 my $inclause = join(',',@jobids);
2277 SELECT DISTINCT JobMedia.MediaId, Media.InChanger
2278 FROM JobMedia, Media
2279 WHERE JobMedia.MediaId=Media.MediaId
2280 AND JobId In ($inclause)
2282 my $result = $dbh->selectall_arrayref($query);
2286 # Returns the fileindex from dirname and jobid.
2287 # Input : dbh, dirname, jobid
2288 # Output : fileindex
2289 sub get_fileindex_from_dir_jobid
2291 my ($dbh, $dirname, $jobid)=@_;
2293 $query = "SELECT File.FileIndex
2294 FROM File, Filename, Path
2295 WHERE File.FilenameId = Filename.FilenameId
2296 AND File.PathId = Path.PathId
2297 AND Filename.Name = ''
2298 AND Path.Path = '$dirname'
2299 AND File.JobId = '$jobid'
2302 print STDERR $query,"\n" if $debug;
2303 my $result = $dbh->selectall_arrayref($query);
2304 return $result->[0]->[0];
2307 # Returns the fileindex from filename and jobid.
2308 # Input : dbh, filename, jobid
2309 # Output : fileindex
2310 sub get_fileindex_from_file_jobid
2312 my ($dbh, $filename, $jobid)=@_;
2314 my @dirs = split(/\//, $filename);
2315 $filename=pop(@dirs);
2316 my $dirname = join('/', @dirs) . '/';
2321 "SELECT File.FileIndex
2322 FROM File, Filename, Path
2323 WHERE File.FilenameId = Filename.FilenameId
2324 AND File.PathId = Path.PathId
2325 AND Filename.Name = '$filename'
2326 AND Path.Path = '$dirname'
2327 AND File.JobId = '$jobid'";
2329 print STDERR $query,"\n" if $debug;
2330 my $result = $dbh->selectall_arrayref($query);
2331 return $result->[0]->[0];
2335 # Returns list of versions of a file that could be restored
2336 # returns an array of
2337 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2338 # It's the same as entries of restore_list (hidden) + mtime and size and inchanger
2339 # and volname and md5
2340 # and of course, there will be only one jobid in the array of jobids...
2341 sub get_all_file_versions
2343 my ($dbh,$path,$file,$client,$see_all)=@_;
2345 defined $see_all or $see_all=0;
2350 "SELECT File.JobId, File.FileIndex, File.Lstat,
2351 File.Md5, Media.VolumeName, Media.InChanger
2352 FROM File, Filename, Path, Job, Client, JobMedia, Media
2353 WHERE File.FilenameId = Filename.FilenameId
2354 AND File.PathId=Path.PathId
2355 AND File.JobId = Job.JobId
2356 AND Job.ClientId = Client.ClientId
2357 AND Job.JobId = JobMedia.JobId
2358 AND File.FileIndex >= JobMedia.FirstIndex
2359 AND File.FileIndex <= JobMedia.LastIndex
2360 AND JobMedia.MediaId = Media.MediaId
2361 AND Path.Path = '$path'
2362 AND Filename.Name = '$file'
2363 AND Client.Name = '$client'";
2365 print STDERR $query if $debug;
2367 my $result = $dbh->selectall_arrayref($query);
2369 foreach my $refrow (@$result)
2371 my ($jobid, $fileindex, $lstat, $md5, $volname, $inchanger) = @$refrow;
2372 my @attribs = parse_lstat($lstat);
2373 my $mtime = array_attrib('st_mtime',\@attribs);
2374 my $size = array_attrib('st_size',\@attribs);
2376 my @list = ('FILE:', $path.$file, $jobid, $fileindex, $mtime, $size,
2377 $inchanger, $md5, $volname);
2378 push @versions, (\@list);
2381 # We have the list of all versions of this file.
2382 # We'll sort it by mtime desc, size, md5, inchanger desc
2383 # the rest of the algorithm will be simpler
2384 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
2385 @versions = sort { $b->[4] <=> $a->[4]
2386 || $a->[5] <=> $b->[5]
2387 || $a->[7] cmp $a->[7]
2388 || $b->[6] <=> $a->[6]} @versions;
2391 my %allready_seen_by_mtime;
2392 my %allready_seen_by_md5;
2393 # Now we should create a new array with only the interesting records
2394 foreach my $ref (@versions)
2398 # The file has a md5. We compare his md5 to other known md5...
2399 # We take size into account. It may happen that 2 files
2400 # have the same md5sum and are different. size is a supplementary
2403 # If we allready have a (better) version
2404 next if ( (not $see_all)
2405 and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
2407 # we never met this one before...
2408 $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
2410 # Even if it has a md5, we should also work with mtimes
2411 # We allready have a (better) version
2412 next if ( (not $see_all)
2413 and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
2414 $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
2416 # We reached there. The file hasn't been seen.
2417 push @good_versions,($ref);
2420 # To be nice with the user, we re-sort good_versions by
2421 # inchanger desc, mtime desc
2422 @good_versions = sort { $b->[4] <=> $a->[4]
2423 || $b->[2] <=> $a->[2]} @good_versions;
2425 return @good_versions;
2428 # TODO : bsr must use only good backup or not (see use_ok_bkp_only)
2429 # This sub creates a BSR from the information in the restore_list
2430 # Returns the BSR as a string
2435 # This query gets all jobid/jobmedia/media combination.
2437 SELECT Job.JobId, Job.VolsessionId, Job.VolsessionTime, JobMedia.StartFile,
2438 JobMedia.EndFile, JobMedia.FirstIndex, JobMedia.LastIndex,
2439 JobMedia.StartBlock, JobMedia.EndBlock, JobMedia.VolIndex,
2440 Media.Volumename, Media.MediaType
2441 FROM Job, JobMedia, Media
2442 WHERE Job.JobId = JobMedia.JobId
2443 AND JobMedia.MediaId = Media.MediaId
2444 ORDER BY JobMedia.FirstIndex, JobMedia.LastIndex";
2447 my $result = $self->dbh_selectall_arrayref($query);
2449 # We will store everything hashed by jobid.
2451 foreach my $refrow (@$result)
2453 my ($jobid, $volsessionid, $volsessiontime, $startfile, $endfile,
2454 $firstindex, $lastindex, $startblock, $endblock,
2455 $volindex, $volumename, $mediatype) = @{$refrow};
2457 # We just have to deal with the case where starfile != endfile
2458 # In this case, we concatenate both, for the bsr
2459 if ($startfile != $endfile) {
2460 $startfile = $startfile . '-' . $endfile;
2464 ($jobid, $volsessionid, $volsessiontime, $startfile,
2465 $firstindex, $lastindex, $startblock .'-'. $endblock,
2466 $volindex, $volumename, $mediatype);
2468 push @{$mediainfos{$refrow->[0]}},(\@tmparray);
2472 # reminder : restore_list looks like this :
2473 # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2475 # Here, we retrieve every file/dir that could be in the restore
2476 # We do as simple as possible for the SQL engine (no crazy joins,
2477 # no pseudo join (>= FirstIndex ...), etc ...
2478 # We do a SQL union of all the files/dirs specified in the restore_list
2480 foreach my $entry (@{$self->{restore_list}->{data}})
2482 if ($entry->[2] eq 'dir')
2484 my $dir = unpack('u', $entry->[0]);
2485 my $inclause = $entry->[3]; #curjobids
2488 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2489 FROM File, Path, Filename
2490 WHERE Path.PathId = File.PathId
2491 AND File.FilenameId = Filename.FilenameId
2492 AND Path.Path LIKE '$dir%'
2493 AND File.JobId IN ($inclause) )";
2494 push @select_queries,($query);
2498 # It's a file. Great, we allready have most
2499 # of what is needed. Simple and efficient query
2500 my $file = unpack('u', $entry->[0]);
2501 my @file = split '/',$file;
2503 my $dir = join('/',@file);
2505 my $jobid = $entry->[1];
2506 my $fileindex = $entry->[7];
2507 my $inclause = $entry->[3]; # curjobids
2509 "(SELECT Path.Path, Filename.Name, File.FileIndex, File.JobId
2510 FROM File, Path, Filename
2511 WHERE Path.PathId = File.PathId
2512 AND File.FilenameId = Filename.FilenameId
2513 AND Path.Path = '$dir/'
2514 AND Filename.Name = '$file'
2515 AND File.JobId = $jobid)";
2516 push @select_queries,($query);
2519 $query = join("\nUNION ALL\n",@select_queries) . "\nORDER BY FileIndex\n";
2521 print STDERR $query,"\n" if $debug;
2523 #Now we run the query and parse the result...
2524 # there may be a lot of records, so we better be efficient
2525 # We use the bind column method, working with references...
2527 my $sth = $self->dbh_prepare($query);
2530 my ($path,$name,$fileindex,$jobid);
2531 $sth->bind_columns(\$path,\$name,\$fileindex,\$jobid);
2533 # The temp place we're going to save all file
2534 # list to before the real list
2538 while ($sth->fetchrow_arrayref())
2540 # This may look dumb, but we're going to do a join by ourselves,
2541 # to save memory and avoid sending a complex query to mysql
2542 my $complete_path = $path . $name;
2550 # Remove trailing slash (normalize file and dir name)
2551 $complete_path =~ s/\/$//;
2553 # Let's find the ref(s) for the %mediainfo element(s)
2554 # containing the data for this file
2555 # There can be several matches. It is the pseudo join.
2557 my $max_elt=@{$mediainfos{$jobid}}-1;
2559 while($med_idx <= $max_elt)
2561 my $ref = $mediainfos{$jobid}->[$med_idx];
2562 # First, can we get rid of the first elements of the
2563 # array ? (if they don't contain valuable records
2565 if ($fileindex > $ref->[5])
2567 # It seems we don't need anymore
2568 # this entry in %mediainfo (the input data
2571 shift @{$mediainfos{$jobid}};
2575 # We will do work on this elt. We can ++
2576 # $med_idx for next loop
2579 # %mediainfo row looks like :
2580 # (jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2581 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,
2584 # We are in range. We store and continue looping
2586 if ($fileindex >= $ref->[4])
2588 my @data = ($complete_path,$is_dir,
2590 push @temp_list,(\@data);
2594 # We are not in range. No point in continuing looping
2595 # We go to next record.
2599 # Now we have the array.
2600 # We're going to sort it, by
2601 # path, volsessiontime DESC (get the most recent file...)
2602 # The array rows look like this :
2603 # complete_path,is_dir,fileindex,
2604 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,
2605 # LastIndex,StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2606 @temp_list = sort {$a->[0] cmp $b->[0]
2607 || $b->[3]->[2] <=> $a->[3]->[2]
2611 my $prev_complete_path='////'; # Sure not to match
2615 while (my $refrow = shift @temp_list)
2617 # For the sake of readability, we load $refrow
2618 # contents in real scalars
2619 my ($complete_path, $is_dir, $fileindex, $refother)=@{$refrow};
2620 my $jobid= $refother->[0]; # We don't need the rest...
2622 # We skip this entry.
2623 # We allready have a newer one and this
2624 # isn't a continuation of the same file
2625 next if ($complete_path eq $prev_complete_path
2626 and $jobid != $prev_jobid);
2630 and $complete_path =~ m|^\Q$prev_complete_path\E/|)
2632 # We would be recursing inside a file.
2633 # Just what we don't want (dir replaced by file
2634 # between two backups
2640 push @restore_list,($refrow);
2642 $prev_complete_path = $complete_path;
2643 $prev_jobid = $jobid;
2649 push @restore_list,($refrow);
2651 $prev_complete_path = $complete_path;
2652 $prev_jobid = $jobid;
2656 # We get rid of @temp_list... save memory
2659 # Ok everything is in the list. Let's sort it again in another way.
2660 # This time it will be in the bsr file order
2662 # we sort the results by
2663 # volsessiontime, volsessionid, volindex, fileindex
2664 # to get all files in right order...
2665 # Reminder : The array rows look like this :
2666 # complete_path,is_dir,fileindex,
2667 # ref->(jobid,VolsessionId,VolsessionTime,File,FirstIndex,LastIndex,
2668 # StartBlock-EndBlock,VolIndex,Volumename,MediaType)
2670 @restore_list= sort { $a->[3]->[2] <=> $b->[3]->[2]
2671 || $a->[3]->[1] <=> $b->[3]->[1]
2672 || $a->[3]->[7] <=> $b->[3]->[7]
2673 || $a->[2] <=> $b->[2] }
2676 # Now that everything is ready, we create the bsr
2677 my $prev_fileindex=-1;
2678 my $prev_volsessionid=-1;
2679 my $prev_volsessiontime=-1;
2680 my $prev_volumename=-1;
2681 my $prev_volfile=-1;
2685 my $first_of_current_range=0;
2686 my @fileindex_ranges;
2689 foreach my $refrow (@restore_list)
2691 my (undef,undef,$fileindex,$refother)=@{$refrow};
2692 my (undef,$volsessionid,$volsessiontime,$volfile,undef,undef,
2693 $volblocks,undef,$volumename,$mediatype)=@{$refother};
2695 # We can specifiy the number of files in each section of the
2696 # bsr to speedup restore (bacula can then jump over the
2697 # end of tape files.
2701 if ($prev_volumename eq '-1')
2703 # We only have to start the new range...
2704 $first_of_current_range=$fileindex;
2706 elsif ($prev_volsessionid != $volsessionid
2707 or $prev_volsessiontime != $volsessiontime
2708 or $prev_volumename ne $volumename
2709 or $prev_volfile ne $volfile)
2711 # We have to create a new section in the bsr...
2712 # We print the previous one ...
2713 # (before that, save the current range ...)
2714 if ($first_of_current_range != $prev_fileindex)
2717 push @fileindex_ranges,
2718 ("$first_of_current_range-$prev_fileindex");
2722 # We are out of a range,
2723 # but there is only one element in the range
2724 push @fileindex_ranges,
2725 ("$first_of_current_range");
2728 $bsr.=print_bsr_section(\@fileindex_ranges,
2730 $prev_volsessiontime,
2737 # Reset for next loop
2738 @fileindex_ranges=();
2739 $first_of_current_range=$fileindex;
2741 elsif ($fileindex-1 != $prev_fileindex)
2743 # End of a range of fileindexes
2744 if ($first_of_current_range != $prev_fileindex)
2747 push @fileindex_ranges,
2748 ("$first_of_current_range-$prev_fileindex");
2752 # We are out of a range,
2753 # but there is only one element in the range
2754 push @fileindex_ranges,
2755 ("$first_of_current_range");
2757 $first_of_current_range=$fileindex;
2759 $prev_fileindex=$fileindex;
2760 $prev_volsessionid = $volsessionid;
2761 $prev_volsessiontime = $volsessiontime;
2762 $prev_volumename = $volumename;
2763 $prev_volfile=$volfile;
2764 $prev_mediatype=$mediatype;
2765 $prev_volblocks=$volblocks;
2769 # Ok, we're out of the loop. Alas, there's still the last record ...
2770 if ($first_of_current_range != $prev_fileindex)
2773 push @fileindex_ranges,("$first_of_current_range-$prev_fileindex");
2778 # We are out of a range,
2779 # but there is only one element in the range
2780 push @fileindex_ranges,("$first_of_current_range");
2783 $bsr.=print_bsr_section(\@fileindex_ranges,
2785 $prev_volsessiontime,
2795 sub print_bsr_section
2797 my ($ref_fileindex_ranges,$volsessionid,
2798 $volsessiontime,$volumename,$volfile,
2799 $mediatype,$volblocks,$count)=@_;
2802 $bsr .= "Volume=\"$volumename\"\n";
2803 $bsr .= "MediaType=\"$mediatype\"\n";
2804 $bsr .= "VolSessionId=$volsessionid\n";
2805 $bsr .= "VolSessionTime=$volsessiontime\n";
2806 $bsr .= "VolFile=$volfile\n";
2807 $bsr .= "VolBlock=$volblocks\n";
2809 foreach my $range (@{$ref_fileindex_ranges})
2811 $bsr .= "FileIndex=$range\n";
2814 $bsr .= "Count=$count\n";
2818 # This function estimates the size to be restored for an entry of the restore
2820 # In : self,reference to the entry
2821 # Out : size in bytes, number of files
2822 sub estimate_restore_size
2824 # reminder : restore_list looks like this :
2825 # ($name,$jobid,'file',$curjobids, undef, undef, undef, $dirfileindex);
2829 if ($entry->[2] eq 'dir')
2831 my $dir = unpack('u', $entry->[0]);
2832 my $inclause = $entry->[3]; #curjobids
2834 "SELECT Path.Path, File.FilenameId, File.LStat
2835 FROM File, Path, Job
2836 WHERE Path.PathId = File.PathId
2837 AND File.JobId = Job.JobId
2838 AND Path.Path LIKE '$dir%'
2839 AND File.JobId IN ($inclause)
2840 ORDER BY Path.Path, File.FilenameId, Job.StartTime DESC";
2844 # It's a file. Great, we allready have most
2845 # of what is needed. Simple and efficient query
2846 my $file = unpack('u', $entry->[0]);
2847 my @file = split '/',$file;
2849 my $dir = join('/',@file);
2851 my $jobid = $entry->[1];
2852 my $fileindex = $entry->[7];
2853 my $inclause = $entry->[3]; # curjobids
2855 "SELECT Path.Path, File.FilenameId, File.Lstat
2856 FROM File, Path, Filename
2857 WHERE Path.PathId = File.PathId
2858 AND Path.Path = '$dir/'
2859 AND Filename.Name = '$file'
2860 AND File.JobId = $jobid
2861 AND Filename.FilenameId = File.FilenameId";
2864 print STDERR $query,"\n" if $debug;
2865 my ($path,$nameid,$lstat);
2866 my $sth = $self->dbh_prepare($query);
2868 $sth->bind_columns(\$path,\$nameid,\$lstat);
2878 while ($sth->fetchrow_arrayref())
2880 # Only the latest version of a file
2881 next if ($nameid eq $old_nameid and $path eq $old_path);
2883 if ($rcount > 15000) {
2890 # We get the size of this file
2891 my $size=lstat_attrib($lstat,'st_size');
2892 $total_size += $size;
2895 $old_nameid=$nameid;
2897 return ($total_size,$total_files);
2900 sub update_brestore_table
2902 my ($self, @jobs) = @_;
2903 my $dbh = $self->{dbh};
2905 foreach my $job (sort {$a <=> $b} @jobs)
2907 my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
2908 my $retour = $self->dbh_selectrow_arrayref($query);
2909 next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
2911 print STDERR "Inserting path records for JobId $job\n";
2912 $query = "INSERT INTO brestore_pathvisibility (PathId, JobId)
2913 (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
2915 $self->dbh_do($query);
2917 # Now we have to do the directory recursion stuff to determine missing visibility
2918 # We try to avoid recursion, to be as fast as possible
2919 # We also only work on not allready hierarchised directories...
2921 print STDERR "Creating missing recursion paths for $job\n";
2923 $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility
2924 JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
2925 LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
2926 WHERE brestore_pathvisibility.JobId = $job
2927 AND brestore_pathhierarchy.PathId IS NULL
2930 my $sth = $self->dbh_prepare($query);
2932 my $pathid; my $path;
2933 $sth->bind_columns(\$pathid,\$path);
2937 $self->build_path_hierarchy($path,$pathid);
2941 # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
2942 # This query gives all parent pathids for a given jobid that aren't stored.
2943 # It has to be called until no record is updated ...
2945 INSERT INTO brestore_pathvisibility (PathId, JobId) (
2946 SELECT a.PathId,$job
2948 (SELECT DISTINCT h.PPathId AS PathId
2949 FROM brestore_pathhierarchy AS h
2950 JOIN brestore_pathvisibility AS p ON (h.PathId=p.PathId)
2951 WHERE p.JobId=$job) AS a
2954 FROM brestore_pathvisibility
2955 WHERE JobId=$job) AS b
2956 ON (a.PathId = b.PathId)
2957 WHERE b.PathId IS NULL)";
2958 print STDERR $query,"\n" if ($debug);
2960 while (($rows_affected = $dbh->do($query)) and ($rows_affected !~ /^0/))
2962 print STDERR "Recursively adding $rows_affected records from $job\n";
2965 $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
2970 sub cleanup_brestore_table
2973 my $dbh = $self->{dbh};
2975 my $query = "SELECT JobId from brestore_knownjobid";
2976 my @jobs = @{$dbh->selectall_arrayref($query)};
2978 foreach my $jobentry (@jobs)
2980 my $job = $jobentry->[0];
2981 $query = "SELECT FileId from File WHERE JobId = $job LIMIT 1";
2982 my $result = $dbh->selectall_arrayref($query);
2983 if (scalar(@{$result}))
2985 # There are still files for this jobid
2986 print STDERR "$job still exists. Not cleaning...\n";
2989 $query = "DELETE FROM brestore_pathvisibility WHERE JobId = $job";
2991 $query = "DELETE FROM brestore_knownjobid WHERE JobId = $job";
2997 sub build_path_hierarchy
2999 my ($self, $path,$pathid)=@_;
3000 # Does the ppathid exist for this ? we use a memory cache...
3001 # In order to avoid the full loop, we consider that if a dir is allready in the
3002 # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
3005 #print STDERR "$path\n" if $debug;
3006 if (! $self->{cache_ppathid}->{$pathid})
3008 my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
3009 my $sth2 = $self->{dbh}->prepare_cached($query);
3010 $sth2->execute($pathid);
3011 # Do we have a result ?
3012 if (my $refrow = $sth2->fetchrow_arrayref)
3014 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
3016 # This dir was in the db ...
3017 # It means we can leave, the tree has allready been built for
3022 # We have to create the record ...
3023 # What's the current p_path ?
3024 my $ppath = parent_dir($path);
3025 my $ppathid = $self->return_pathid_from_path($ppath);
3026 $self->{cache_ppathid}->{$pathid}= $ppathid;
3028 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
3029 $sth2 = $self->{dbh}->prepare_cached($query);
3030 $sth2->execute($pathid,$ppathid);
3036 # It's allready in the cache.
3037 # We can leave, no time to waste here, all the parent dirs have allready
3045 sub return_pathid_from_path
3047 my ($self, $path) = @_;
3048 my $query = "SELECT PathId FROM Path WHERE Path = ?
3050 SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3051 #print STDERR $query,"\n" if $debug;
3052 my $sth = $self->{dbh}->prepare_cached($query);
3053 $sth->execute($path,$path);
3054 my $result =$sth->fetchrow_arrayref();
3056 if (defined $result)
3058 return $result->[0];
3061 # A bit dirty : we insert into path AND missing_path, to be sure
3062 # we aren't deleted by a purge. We still need to insert into path to get
3063 # the pathid, because of mysql
3064 $query = "INSERT INTO Path (Path) VALUES (?)";
3065 #print STDERR $query,"\n" if $debug;
3066 $sth = $self->{dbh}->prepare_cached($query);
3067 $sth->execute($path);
3070 $query = " INSERT INTO brestore_missing_path (PathId,Path)
3071 SELECT PathId,Path FROM Path WHERE Path = ?";
3072 #print STDERR $query,"\n" if $debug;
3073 $sth = $self->{dbh}->prepare_cached($query);
3074 $sth->execute($path);
3076 $query = " DELETE FROM Path WHERE Path = ?";
3077 #print STDERR $query,"\n" if $debug;
3078 $sth = $self->{dbh}->prepare_cached($query);
3079 $sth->execute($path);
3081 $query = "SELECT PathId FROM brestore_missing_path WHERE Path = ?";
3082 #print STDERR $query,"\n" if $debug;
3083 $sth = $self->{dbh}->prepare_cached($query);
3084 $sth->execute($path);
3085 $result = $sth->fetchrow_arrayref();
3087 return $result->[0];
3099 # Root Windows case :
3100 if ($path =~ /^[a-z]+:\/$/i)
3105 my @tmp = split('/',$path);
3106 # We remove the last ...
3108 my $tmp = join ('/',@tmp) . '/';
3114 my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
3115 'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
3116 'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
3117 'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
3118 'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
3119 'data_stream' => 15);;
3122 my ($attrib,$ref_attrib)=@_;
3123 return $ref_attrib->[$attrib_name_id{$attrib}];
3127 { # $file = [listfiles.id, listfiles.Name, File.LStat, File.JobId]
3129 my ($file, $attrib)=@_;
3131 if (defined $attrib_name_id{$attrib}) {
3133 my @d = split(' ', $file->[2]) ; # TODO : cache this
3135 return from_base64($d[$attrib_name_id{$attrib}]);
3137 } elsif ($attrib eq 'jobid') {
3141 } elsif ($attrib eq 'name') {
3146 die "Attribute not known : $attrib.\n";
3150 # Return the jobid or attribute asked for a dir
3153 my ($self,$dir,$attrib)=@_;
3155 my @dir = split('/',$dir,-1);
3156 my $refdir=$self->{dirtree}->{$self->current_client};
3158 if (not defined $attrib_name_id{$attrib} and $attrib ne 'jobid')
3160 die "Attribute not known : $attrib.\n";
3163 foreach my $subdir (@dir)
3165 $refdir = $refdir->[0]->{$subdir};
3168 # $refdir is now the reference to the dir's array
3169 # Is the a jobid in @CurrentJobIds where the lstat is
3170 # defined (we'll search in reverse order)
3171 foreach my $jobid (reverse(sort {$a <=> $b } @{$self->{CurrentJobIds}}))
3173 if (defined $refdir->[1]->{$jobid} and $refdir->[1]->{$jobid} ne '1')
3175 if ($attrib eq 'jobid')
3181 my @attribs = parse_lstat($refdir->[1]->{$jobid});
3182 return $attribs[$attrib_name_id{$attrib}+1];
3187 return 0; # We cannot get a good attribute.
3188 # This directory is here for the sake of visibility
3193 my ($lstat,$attrib)=@_;
3194 if ($lstat and defined $attrib_name_id{$attrib})
3196 my @d = split(' ', $lstat) ; # TODO : cache this
3197 return from_base64($d[$attrib_name_id{$attrib}]);
3204 # Base 64 functions, directly from recover.pl.
3206 # Karl Hakimian <hakimian@aha.com>
3207 # This section is also under GPL v2 or later.
3214 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
3215 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
3216 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
3217 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
3218 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
3220 @base64_map = (0) x 128;
3222 for (my $i=0; $i<64; $i++) {
3223 $base64_map[ord($base64_digits[$i])] = $i;
3238 if (substr($where, 0, 1) eq '-') {
3240 $where = substr($where, 1);
3243 while ($where ne '') {
3245 my $d = substr($where, 0, 1);
3246 $val += $base64_map[ord(substr($where, 0, 1))];
3247 $where = substr($where, 1);
3255 my @attribs = split(' ',$lstat);
3256 foreach my $element (@attribs)
3258 $element = from_base64($element);
3267 ################################################################
3270 use base qw/DlgResto/;
3274 my ($class, $conf) = @_;
3275 my $self = bless {info => $conf}, $class;
3277 $self->{dbh} = $conf->{dbh};
3286 my $query = "SELECT JobId from Job WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) order by JobId";
3287 my $jobs = $self->dbh_selectall_arrayref($query);
3289 $self->update_brestore_table(map { $_->[0] } @$jobs);
3300 print STDERR "Usage: $0 [--conf=brestore.conf] [--batch] [--debug]\n";
3304 my $file_conf = "$ENV{HOME}/.brestore.conf" ;
3307 GetOptions("conf=s" => \$file_conf,
3308 "batch" => \$batch_mod,
3310 "help" => \&HELP_MESSAGE) ;
3312 my $p = new Pref($file_conf);
3314 if (! -f $file_conf) {
3319 my $b = new Batch($p);
3320 if ($p->connect_db()) {
3321 $b->set_dbh($p->{dbh});
3327 $glade_file = $p->{glade_file};
3329 foreach my $path ('','.','/usr/share/brestore','/usr/local/share/brestore') {
3330 if (-f "$path/$glade_file") {
3331 $glade_file = "$path/$glade_file" ;
3336 # gtk have lots of warning on stderr
3337 if ($^O eq 'MSWin32')
3340 open(STDERR, ">stderr.log");
3345 if ( -f $glade_file) {
3346 my $w = new DlgResto($p);
3349 my $widget = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'close',
3350 "Can't find your brestore.glade (glade_file => '$glade_file')
3351 Please, edit your $file_conf to setup it." );
3353 $widget->signal_connect('destroy', sub { Gtk2->main_quit() ; });
3358 Gtk2->main; # Start Gtk2 main loop
3370 # Code pour trier les colonnes
3371 my $mod = $fileview->get_model();
3372 $mod->set_default_sort_func(sub {
3373 my ($model, $item1, $item2) = @_;
3374 my $a = $model->get($item1, 1); # récupération de la valeur de la 2ème
3375 my $b = $model->get($item2, 1); # colonne (indice 1)
3380 $fileview->set_headers_clickable(1);
3381 my $col = $fileview->get_column(1); # la colonne NOM, colonne numéro 2
3382 $col->signal_connect('clicked', sub {
3383 my ($colonne, $model) = @_;
3384 $model->set_sort_column_id (1, 'ascending');