4 die "bresto is not enabled" if (not $bresto_enable);
8 Bweb - A Bacula web interface
9 Bacula® - The Network Backup Solution
11 Copyright (C) 2000-2006 Free Software Foundation Europe e.V.
13 The main author of Bweb is Eric Bollengier.
14 The main author of Bacula is Kern Sibbald, with contributions from
15 many others, a complete list can be found in the file AUTHORS.
17 This program is Free Software; you can redistribute it and/or
18 modify it under the terms of version two of the GNU General Public
19 License as published by the Free Software Foundation plus additions
20 that are listed in the file LICENSE.
22 This program is distributed in the hope that it will be useful, but
23 WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25 General Public License for more details.
27 You should have received a copy of the GNU General Public License
28 along with this program; if not, write to the Free Software
29 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
32 Bacula® is a registered trademark of John Walker.
33 The licensor of Bacula is the Free Software Foundation Europe
34 (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zurich,
35 Switzerland, email:ftf@fsfeurope.org.
51 return $self->get_pathid('');
56 my ($self, $pathid) = @_;
57 $self->{cwdid} = $pathid;
65 FROM brestore_pathhierarchy
66 WHERE PathId IN ($self->{cwdid}) ";
68 my $all = $self->dbh_selectall_arrayref($query);
69 return unless ($all); # already at root
71 my $dir = join(',', map { $_->[0] } @$all);
80 return $self->get_path($self->{cwdid});
85 my ($self, $pathid) = @_;
86 $self->debug("Call with pathid = $pathid");
88 "SELECT Path FROM Path WHERE PathId IN (?)";
90 my $sth = $self->dbh_prepare($query);
91 $sth->execute($pathid);
92 my $result = $sth->fetchrow_arrayref();
99 my ($self, @jobids) = @_;
100 $self->{curjobids} = join(',', @jobids);
101 # $self->update_brestore_table(@jobids);
106 my ($self, $dir) = @_;
108 "SELECT PathId FROM Path WHERE Path = ?";
109 my $sth = $self->dbh_prepare($query);
111 my $result = $sth->fetchall_arrayref();
114 return join(',', map { $_->[0] } @$result);
119 my ($self, $offset, $limit) = @_;
120 $self->{limit} = $limit || 100;
121 $self->{offset} = $offset || 0;
128 $self->{dbh}->begin_work();
131 SELECT JobId from Job
132 WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) AND JobStatus IN ('T', 'f', 'A') ORDER BY JobId";
133 my $jobs = $self->dbh_selectall_arrayref($query);
135 $self->update_brestore_table(map { $_->[0] } @$jobs);
137 print STDERR "Cleaning path visibility\n";
139 my $nb = $self->dbh_do("
140 DELETE FROM brestore_pathvisibility
142 (SELECT 1 FROM Job WHERE JobId=brestore_pathvisibility.JobId)");
144 print STDERR "$nb rows affected\n";
145 print STDERR "Cleaning known jobid\n";
147 $nb = $self->dbh_do("
148 DELETE FROM brestore_knownjobid
150 (SELECT 1 FROM Job WHERE JobId=brestore_knownjobid.JobId)");
152 print STDERR "$nb rows affected\n";
154 $self->{dbh}->commit();
157 sub update_brestore_table
159 my ($self, @jobs) = @_;
161 $self->debug(\@jobs);
163 foreach my $job (sort {$a <=> $b} @jobs)
165 my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
166 my $retour = $self->dbh_selectrow_arrayref($query);
167 next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
169 print STDERR "Inserting path records for JobId $job\n";
170 $query = "INSERT INTO brestore_pathvisibility (PathId, JobId)
171 (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
173 $self->dbh_do($query);
175 # Now we have to do the directory recursion stuff to determine missing visibility
176 # We try to avoid recursion, to be as fast as possible
177 # We also only work on not allready hierarchised directories...
179 print STDERR "Creating missing recursion paths for $job\n";
181 $query = "SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility
182 JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
183 LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
184 WHERE brestore_pathvisibility.JobId = $job
185 AND brestore_pathhierarchy.PathId IS NULL
188 my $sth = $self->dbh_prepare($query);
190 my $pathid; my $path;
191 $sth->bind_columns(\$pathid,\$path);
195 $self->build_path_hierarchy($path,$pathid);
199 # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
200 # This query gives all parent pathids for a given jobid that aren't stored.
201 # It has to be called until no record is updated ...
203 INSERT INTO brestore_pathvisibility (PathId, JobId) (
206 (SELECT DISTINCT h.PPathId AS PathId
207 FROM brestore_pathhierarchy AS h
208 JOIN brestore_pathvisibility AS p ON (h.PathId=p.PathId)
209 WHERE p.JobId=$job) AS a
212 FROM brestore_pathvisibility
213 WHERE JobId=$job) AS b
214 ON (a.PathId = b.PathId)
215 WHERE b.PathId IS NULL)";
218 while (($rows_affected = $self->dbh_do($query)) and ($rows_affected !~ /^0/))
220 print STDERR "Recursively adding $rows_affected records from $job\n";
223 $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
224 $self->dbh_do($query);
236 # Root Windows case :
237 if ($path =~ /^[a-z]+:\/$/i)
242 my @tmp = split('/',$path);
243 # We remove the last ...
245 my $tmp = join ('/',@tmp) . '/';
249 sub build_path_hierarchy
251 my ($self, $path,$pathid)=@_;
252 # Does the ppathid exist for this ? we use a memory cache...
253 # In order to avoid the full loop, we consider that if a dir is allready in the
254 # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
257 if (! $self->{cache_ppathid}->{$pathid})
259 my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
260 my $sth2 = $self->{dbh}->prepare_cached($query);
261 $sth2->execute($pathid);
262 # Do we have a result ?
263 if (my $refrow = $sth2->fetchrow_arrayref)
265 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
267 # This dir was in the db ...
268 # It means we can leave, the tree has allready been built for
273 # We have to create the record ...
274 # What's the current p_path ?
275 my $ppath = parent_dir($path);
276 my $ppathid = $self->return_pathid_from_path($ppath);
277 $self->{cache_ppathid}->{$pathid}= $ppathid;
279 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
280 $sth2 = $self->{dbh}->prepare_cached($query);
281 $sth2->execute($pathid,$ppathid);
287 # It's allready in the cache.
288 # We can leave, no time to waste here, all the parent dirs have allready
296 sub return_pathid_from_path
298 my ($self, $path) = @_;
299 my $query = "SELECT PathId FROM Path WHERE Path = ?";
301 #print STDERR $query,"\n" if $debug;
302 my $sth = $self->{dbh}->prepare_cached($query);
303 $sth->execute($path);
304 my $result =$sth->fetchrow_arrayref();
311 # A bit dirty : we insert into path, and we have to be sure
312 # we aren't deleted by a purge. We still need to insert into path to get
313 # the pathid, because of mysql
314 $query = "INSERT INTO Path (Path) VALUES (?)";
315 #print STDERR $query,"\n" if $debug;
316 $sth = $self->{dbh}->prepare_cached($query);
317 $sth->execute($path);
320 $query = "SELECT PathId FROM Path WHERE Path = ?";
321 #print STDERR $query,"\n" if $debug;
322 $sth = $self->{dbh}->prepare_cached($query);
323 $sth->execute($path);
324 $result = $sth->fetchrow_arrayref();
334 return undef unless ($self->{curjobids});
336 my $inclause = $self->{curjobids};
337 my $inlistpath = $self->{cwdid};
340 "SELECT File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
342 (SELECT Filename.Name, max(File.FileId) as id
344 WHERE File.FilenameId = Filename.FilenameId
345 AND Filename.Name != ''
346 AND File.PathId IN ($inlistpath)
347 AND File.JobId IN ($inclause)
348 GROUP BY Filename.Name
349 ORDER BY Filename.Name) AS listfiles,
351 WHERE File.FileId = listfiles.id";
353 $self->debug($query);
354 my $result = $self->dbh_selectall_arrayref($query);
355 $self->debug($result);
361 # return ($dirid,$dir_basename,$lstat,$jobid)
366 return undef unless ($self->{curjobids});
368 my $pathid = $self->{cwdid};
369 my $jobclause = $self->{curjobids};
371 # Let's retrieve the list of the visible dirs in this dir ...
372 # First, I need the empty filenameid to locate efficiently the dirs in the file table
373 my $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
374 my $sth = $self->dbh_prepare($query);
376 my $result = $sth->fetchrow_arrayref();
378 my $dir_filenameid = $result->[0];
380 # Then we get all the dir entries from File ...
382 SELECT PathId, Path, JobId, Lstat FROM (
384 SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
385 listfile1.JobId, listfile1.Lstat
387 SELECT DISTINCT brestore_pathhierarchy1.PathId
388 FROM brestore_pathhierarchy AS brestore_pathhierarchy1
390 ON (brestore_pathhierarchy1.PathId = Path2.PathId)
391 JOIN brestore_pathvisibility AS brestore_pathvisibility1
392 ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
393 WHERE brestore_pathhierarchy1.PPathId = $pathid
394 AND brestore_pathvisibility1.jobid IN ($jobclause)) AS listpath1
395 JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
397 SELECT File1.PathId, File1.JobId, File1.Lstat FROM File AS File1
398 WHERE File1.FilenameId = $dir_filenameid
399 AND File1.JobId IN ($jobclause)) AS listfile1
400 ON (listpath1.PathId = listfile1.PathId)
401 ) AS A ORDER BY 2,3 DESC
403 $self->debug($query);
404 $sth=$self->dbh_prepare($query);
406 $result = $sth->fetchall_arrayref();
409 foreach my $refrow (@{$result})
411 my $dirid = $refrow->[0];
412 my $dir = $refrow->[1];
413 my $lstat = $refrow->[3];
414 my $jobid = $refrow->[2] || 0;
415 next if ($dirid eq $prev_dir);
416 # We have to clean up this dirname ... we only want it's 'basename'
420 my @temp = split ('/',$dir);
421 $return_value = pop @temp;
427 my @return_array = ($dirid,$return_value,$lstat,$jobid);
428 push @return_list,(\@return_array);
431 $self->debug(\@return_list);
432 return \@return_list;
435 # TODO : we want be able to restore files from a bad ended backup
436 # we have JobStatus IN ('T', 'A', 'E') and we must
438 # Data acces subs from here. Interaction with SGBD and caching
440 # This sub retrieves the list of jobs corresponding to the jobs selected in the
441 # GUI and stores them in @CurrentJobIds.
442 # date must be quoted
443 sub set_job_ids_for_date
445 my ($self, $client, $date)=@_;
447 if (!$client or !$date) {
451 # The algorithm : for a client, we get all the backups for each
452 # fileset, in reverse order Then, for each fileset, we store the 'good'
453 # incrementals and differentials until we have found a full so it goes
454 # like this : store all incrementals until we have found a differential
455 # or a full, then find the full #
457 my $query = "SELECT JobId, FileSet, Level, JobStatus
458 FROM Job, Client, FileSet
459 WHERE Job.ClientId = Client.ClientId
460 AND FileSet.FileSetId = Job.FileSetId
462 AND Client.Name = '$client'
464 AND JobStatus IN ('T')
465 ORDER BY FileSet, JobTDate DESC";
468 my $result = $self->dbh_selectall_arrayref($query);
470 foreach my $refrow (@$result)
472 my $jobid = $refrow->[0];
473 my $fileset = $refrow->[1];
474 my $level = $refrow->[2];
476 defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
478 next if $progress{$fileset} eq 'F'; # It's over for this fileset...
482 next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
483 push @CurrentJobIds,($jobid);
485 elsif ($level eq 'D')
487 next if $progress{$fileset} eq 'D'; # We allready have a differential
488 push @CurrentJobIds,($jobid);
490 elsif ($level eq 'F')
492 push @CurrentJobIds,($jobid);
495 my $status = $refrow->[3] ;
496 if ($status eq 'T') { # good end of job
497 $progress{$fileset} = $level;
501 return @CurrentJobIds;
504 sub dbh_selectrow_arrayref
506 my ($self, $query) = @_;
507 $self->debug($query, up => 1);
508 return $self->{dbh}->selectrow_arrayref($query);
511 # Returns list of versions of a file that could be restored
512 # returns an array of
513 # (jobid,fileindex,mtime,size,inchanger,md5,volname,fileid)
514 # there will be only one jobid in the array of jobids...
515 sub get_all_file_versions
517 my ($self,$pathid,$fileid,$client,$see_all)=@_;
519 defined $see_all or $see_all=0;
524 "SELECT File.JobId, File.FileId, File.Lstat,
525 File.Md5, Media.VolumeName, Media.InChanger
526 FROM File, Job, Client, JobMedia, Media
527 WHERE File.FilenameId = $fileid
528 AND File.PathId=$pathid
529 AND File.JobId = Job.JobId
530 AND Job.ClientId = Client.ClientId
531 AND Job.JobId = JobMedia.JobId
532 AND File.FileIndex >= JobMedia.FirstIndex
533 AND File.FileIndex <= JobMedia.LastIndex
534 AND JobMedia.MediaId = Media.MediaId
535 AND Client.Name = '$client'";
537 $self->debug($query);
539 my $result = $self->dbh_selectall_arrayref($query);
541 foreach my $refrow (@$result)
543 my ($jobid, $fid, $lstat, $md5, $volname, $inchanger) = @$refrow;
544 my @attribs = parse_lstat($lstat);
545 my $mtime = array_attrib('st_mtime',\@attribs);
546 my $size = array_attrib('st_size',\@attribs);
548 my @list = ($pathid,$fileid,$jobid,
549 $fid, $mtime, $size, $inchanger,
551 push @versions, (\@list);
554 # We have the list of all versions of this file.
555 # We'll sort it by mtime desc, size, md5, inchanger desc
556 # the rest of the algorithm will be simpler
557 # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
558 @versions = sort { $b->[4] <=> $a->[4]
559 || $a->[5] <=> $b->[5]
560 || $a->[7] cmp $a->[7]
561 || $b->[6] <=> $a->[6]} @versions;
565 my %allready_seen_by_mtime;
566 my %allready_seen_by_md5;
567 # Now we should create a new array with only the interesting records
568 foreach my $ref (@versions)
572 # The file has a md5. We compare his md5 to other known md5...
573 # We take size into account. It may happen that 2 files
574 # have the same md5sum and are different. size is a supplementary
577 # If we allready have a (better) version
578 next if ( (not $see_all)
579 and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
581 # we never met this one before...
582 $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
584 # Even if it has a md5, we should also work with mtimes
585 # We allready have a (better) version
586 next if ( (not $see_all)
587 and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
588 $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
590 # We reached there. The file hasn't been seen.
591 push @good_versions,($ref);
594 # To be nice with the user, we re-sort good_versions by
595 # inchanger desc, mtime desc
596 @good_versions = sort { $b->[4] <=> $a->[4]
597 || $b->[2] <=> $a->[2]} @good_versions;
599 return \@good_versions;
602 my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
603 'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
604 'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
605 'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
606 'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
607 'data_stream' => 15);;
610 my ($attrib,$ref_attrib)=@_;
611 return $ref_attrib->[$attrib_name_id{$attrib}];
615 { # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
617 my ($file, $attrib)=@_;
619 if (defined $attrib_name_id{$attrib}) {
621 my @d = split(' ', $file->[3]) ; # TODO : cache this
623 return from_base64($d[$attrib_name_id{$attrib}]);
625 } elsif ($attrib eq 'jobid') {
629 } elsif ($attrib eq 'name') {
634 die "Attribute not known : $attrib.\n";
640 my ($lstat,$attrib)=@_;
641 if ($lstat and defined $attrib_name_id{$attrib})
643 my @d = split(' ', $lstat) ; # TODO : cache this
644 return from_base64($d[$attrib_name_id{$attrib}]);
651 # Base 64 functions, directly from recover.pl.
653 # Karl Hakimian <hakimian@aha.com>
654 # This section is also under GPL v2 or later.
661 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
662 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
663 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
664 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
665 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
667 @base64_map = (0) x 128;
669 for (my $i=0; $i<64; $i++) {
670 $base64_map[ord($base64_digits[$i])] = $i;
685 if (substr($where, 0, 1) eq '-') {
687 $where = substr($where, 1);
690 while ($where ne '') {
692 my $d = substr($where, 0, 1);
693 $val += $base64_map[ord(substr($where, 0, 1))];
694 $where = substr($where, 1);
702 my @attribs = split(' ',$lstat);
703 foreach my $element (@attribs)
705 $element = from_base64($element);
712 ################################################################
719 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
722 my $bvfs = new Bvfs(info => $conf);
725 my $action = CGI::param('action') || '';
727 my $args = $bvfs->get_form('pathid', 'filenameid', 'fileid', 'qdate',
728 'limit', 'offset', 'client');
730 my @jobid = grep { /^\d+$/ } CGI::param('jobid');
732 if (!scalar(@jobid) and $args->{qdate} and $args->{client}) {
733 @jobid = $bvfs->set_job_ids_for_date($args->{client}, $args->{qdate});
736 $bvfs->set_curjobids(@jobid);
738 $bvfs->set_limits($args->{limit}, $args->{offset});
740 print CGI::header('application/x-javascript');
742 if ($action eq 'list_client') {
744 my $q = 'SELECT Name FROM Client';
745 my $ret = $bvfs->dbh_selectall_arrayref($q);
748 print join(',', map { "['$_->[0]']" } @$ret);
752 } elsif ($action eq 'list_job') {
755 SELECT Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus, Job.JobId
756 FROM Job,Client,FileSet
757 WHERE Job.ClientId=Client.ClientId
758 AND Client.Name = '$args->{client}'
760 AND JobStatus IN ('f', 'T')
761 AND Job.FileSetId = FileSet.FileSetId
762 ORDER BY EndTime desc";
763 my $result = $bvfs->dbh_selectall_arrayref($query);
767 print join(',', map {
768 "[$_->[4], '$_->[0]', '$_->[0] $_->[1] $_->[2] ($_->[3])']"
774 if (CGI::param('init')) {
775 $bvfs->update_brestore_table(@jobid);
778 my $pathid = CGI::param('node') || '';
779 my $path = CGI::param('path');
781 if ($pathid =~ /^(\d+)$/) {
784 $pathid = $bvfs->get_pathid($path);
786 $pathid = $bvfs->get_root();
789 $bvfs->ch_dir($pathid);
791 if ($action eq 'list_files') {
793 my $files = $bvfs->ls_files();
794 # [ 1, 2, 3, "Bill", 10, '2007-01-01 00:00:00'],
795 # File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
798 map { "[$_->[1], $_->[0], $pathid, \"$_->[2]\", 10, \"2007-01-01 00:00:00\"]" }
802 } elsif ($action eq 'list_dirs') {
805 my $dirs = $bvfs->ls_dirs();
806 # return ($dirid,$dir_basename,$lstat,$jobid)
809 map { "{ 'id': '$_->[0]', 'text': '$_->[1]', 'cls':'folder'}" }
814 } elsif ($action eq 'list_versions') {
816 my $vafv = CGI::param('vafv') || 'false'; # view all file versions
817 $vafv = ($vafv eq 'false')?0:1;
821 #($pathid,$fileid,$jobid, $fid, $mtime, $size, $inchanger, $md5, $volname);
822 my $files = $bvfs->get_all_file_versions($args->{pathid}, $args->{filenameid}, $args->{client}, $vafv);
824 map { "[ $_->[3], $_->[1], $_->[0], $_->[2], '$_->[8]', $_->[6], '$_->[7]', $_->[5], $_->[4] ]" }