]> git.sur5r.net Git - bacula/bacula/blob - gui/bweb/cgi/bresto.pl
bweb: Update some GPL2 notice to AGPL
[bacula/bacula] / gui / bweb / cgi / bresto.pl
1 #!/usr/bin/perl -w
2 use strict;
3 my $bresto_enable = 1;
4 die "bresto is not enabled" if (not $bresto_enable);
5
6 =head1 LICENSE
7
8    Bweb - A Bacula web interface
9    Bacula® - The Network Backup Solution
10
11    Copyright (C) 2000-2006 Free Software Foundation Europe e.V.
12
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.
16
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.
21
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    Affero General Public License for more details.
26
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
30    02110-1301, USA.
31
32    Bacula® is a registered trademark of Kern Sibbald.
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.
36
37 =head1 VERSION
38
39     $Id$
40
41 =cut
42
43 use Bweb;
44
45 package Bvfs;
46 use base qw/Bweb/;
47
48 sub get_root
49 {
50     my ($self) = @_;
51     return $self->get_pathid('');
52 }
53
54 # change the current directory
55 sub ch_dir
56 {
57     my ($self, $pathid) = @_;
58     $self->{cwdid} = $pathid;
59 }
60
61 # do a cd ..
62 sub up_dir
63 {
64     my ($self) = @_ ;
65     my $query = "
66   SELECT PPathId
67     FROM brestore_pathhierarchy
68    WHERE PathId IN ($self->{cwdid}) ";
69
70     my $all = $self->dbh_selectall_arrayref($query);
71     return unless ($all);       # already at root
72
73     my $dir = join(',', map { $_->[0] } @$all);
74     if ($dir) {
75         $self->ch_dir($dir);
76     }
77 }
78
79 # return the current PWD
80 sub pwd
81 {
82     my ($self) = @_;
83     return $self->get_path($self->{cwdid});
84 }
85
86 # get the Path from a PathId
87 sub get_path
88 {
89     my ($self, $pathid) = @_;
90     $self->debug("Call with pathid = $pathid");
91     my $query = "SELECT Path FROM Path WHERE PathId = ?";
92     my $sth = $self->dbh_prepare($query);
93     $sth->execute($pathid);
94     my $result = $sth->fetchrow_arrayref();
95     $sth->finish();
96     return $result->[0];
97 }
98
99 # we are working with these jobids
100 sub set_curjobids
101 {
102     my ($self, @jobids) = @_;
103     $self->{curjobids} = join(',', @jobids);
104 #    $self->update_brestore_table(@jobids);
105 }
106
107 # get the PathId from a Path
108 sub get_pathid
109 {
110     my ($self, $dir) = @_;
111     my $query =
112         "SELECT PathId FROM Path WHERE Path = ?";
113     my $sth = $self->dbh_prepare($query);
114     $sth->execute($dir);
115     my $result = $sth->fetchrow_arrayref();
116     $sth->finish();
117
118     return $result->[0];
119 }
120
121 sub set_limits
122 {
123     my ($self, $offset, $limit) = @_;
124     $self->{limit}  = $limit  || 100;
125     $self->{offset} = $offset || 0;
126 }
127
128 sub set_pattern
129 {
130     my ($self, $pattern) = @_;
131     $self->{pattern} = $pattern;
132 }
133
134 # fill brestore_xxx tables for speedup
135 sub update_cache
136 {
137     my ($self) = @_;
138
139     $self->{dbh}->begin_work();
140
141     # getting all Jobs to "cache"
142     my $query = "
143   SELECT JobId from Job
144    WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) 
145      AND Type IN ('B') AND JobStatus IN ('T', 'f', 'A') 
146    ORDER BY JobId";
147     my $jobs = $self->dbh_selectall_arrayref($query);
148
149     $self->update_brestore_table(map { $_->[0] } @$jobs);
150
151     $self->{dbh}->commit();
152     $self->{dbh}->begin_work(); # we can break here
153
154     print STDERR "Cleaning path visibility\n";
155
156     my $nb = $self->dbh_do("
157   DELETE FROM brestore_pathvisibility
158       WHERE NOT EXISTS
159    (SELECT 1 FROM Job WHERE JobId=brestore_pathvisibility.JobId)");
160
161     print STDERR "$nb rows affected\n";
162     print STDERR "Cleaning known jobid\n";
163
164     $nb = $self->dbh_do("
165   DELETE FROM brestore_knownjobid
166       WHERE NOT EXISTS
167    (SELECT 1 FROM Job WHERE JobId=brestore_knownjobid.JobId)");
168
169     print STDERR "$nb rows affected\n";
170
171     $self->{dbh}->commit();
172 }
173
174 sub update_brestore_table
175 {
176     my ($self, @jobs) = @_;
177
178     $self->debug(\@jobs);
179
180     foreach my $job (sort {$a <=> $b} @jobs)
181     {
182         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
183         my $retour = $self->dbh_selectrow_arrayref($query);
184         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
185
186         print STDERR "Inserting path records for JobId $job\n";
187         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId)
188                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
189
190         $self->dbh_do($query);
191
192         # Now we have to do the directory recursion stuff to determine missing visibility
193         # We try to avoid recursion, to be as fast as possible
194         # We also only work on not allready hierarchised directories...
195
196         print STDERR "Creating missing recursion paths for $job\n";
197
198         $query = "
199 SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility
200   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
201        LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
202  WHERE brestore_pathvisibility.JobId = $job
203    AND brestore_pathhierarchy.PathId IS NULL
204  ORDER BY Path";
205
206         my $sth = $self->dbh_prepare($query);
207         $sth->execute();
208         my $pathid; my $path;
209         $sth->bind_columns(\$pathid,\$path);
210
211         while ($sth->fetch)
212         {
213             $self->build_path_hierarchy($path,$pathid);
214         }
215         $sth->finish();
216
217         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
218         # This query gives all parent pathids for a given jobid that aren't stored.
219         # It has to be called until no record is updated ...
220         $query = "
221 INSERT INTO brestore_pathvisibility (PathId, JobId) (
222  SELECT a.PathId,$job
223    FROM (
224      SELECT DISTINCT h.PPathId AS PathId
225        FROM brestore_pathhierarchy AS h
226        JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
227       WHERE p.JobId=$job) AS a LEFT JOIN
228        (SELECT PathId
229           FROM brestore_pathvisibility
230          WHERE JobId=$job) AS b ON (a.PathId = b.PathId)
231   WHERE b.PathId IS NULL)";
232
233         my $rows_affected;
234         while (($rows_affected = $self->dbh_do($query)) and ($rows_affected !~ /^0/))
235         {
236             print STDERR "Recursively adding $rows_affected records from $job\n";
237         }
238         # Job's done
239         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
240         $self->dbh_do($query);
241     }
242 }
243
244 # compute the parent directory
245 sub parent_dir
246 {
247     my ($path) = @_;
248     # Root Unix case :
249     if ($path eq '/')
250     {
251         return '';
252     }
253     # Root Windows case :
254     if ($path =~ /^[a-z]+:\/$/i)
255     {
256         return '';
257     }
258     # Split
259     my @tmp = split('/',$path);
260     # We remove the last ...
261     pop @tmp;
262     my $tmp = join ('/',@tmp) . '/';
263     return $tmp;
264 }
265
266 sub build_path_hierarchy
267 {
268     my ($self, $path,$pathid)=@_;
269     # Does the ppathid exist for this ? we use a memory cache...
270     # In order to avoid the full loop, we consider that if a dir is allready in the
271     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
272     while ($path ne '')
273     {
274         if (! $self->{cache_ppathid}->{$pathid})
275         {
276             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
277             my $sth2 = $self->{dbh}->prepare_cached($query);
278             $sth2->execute($pathid);
279             # Do we have a result ?
280             if (my $refrow = $sth2->fetchrow_arrayref)
281             {
282                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
283                 $sth2->finish();
284                 # This dir was in the db ...
285                 # It means we can leave, the tree has allready been built for
286                 # this dir
287                 return 1;
288             } else {
289                 $sth2->finish();
290                 # We have to create the record ...
291                 # What's the current p_path ?
292                 my $ppath = parent_dir($path);
293                 my $ppathid = $self->return_pathid_from_path($ppath);
294                 $self->{cache_ppathid}->{$pathid}= $ppathid;
295
296                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
297                 $sth2 = $self->{dbh}->prepare_cached($query);
298                 $sth2->execute($pathid,$ppathid);
299                 $sth2->finish();
300                 $path = $ppath;
301                 $pathid = $ppathid;
302             }
303         } else {
304            # It's allready in the cache.
305            # We can leave, no time to waste here, all the parent dirs have allready
306            # been done
307            return 1;
308         }
309     }
310     return 1;
311 }
312
313 sub return_pathid_from_path
314 {
315     my ($self, $path) = @_;
316     my $query = "SELECT PathId FROM Path WHERE Path = ?";
317
318     #print STDERR $query,"\n" if $debug;
319     my $sth = $self->{dbh}->prepare_cached($query);
320     $sth->execute($path);
321     my $result =$sth->fetchrow_arrayref();
322     $sth->finish();
323     if (defined $result)
324     {
325         return $result->[0];
326
327     } else {
328         # A bit dirty : we insert into path, and we have to be sure
329         # we aren't deleted by a purge. We still need to insert into path to get
330         # the pathid, because of mysql
331         $query = "INSERT INTO Path (Path) VALUES (?)";
332         #print STDERR $query,"\n" if $debug;
333         $sth = $self->{dbh}->prepare_cached($query);
334         $sth->execute($path);
335         $sth->finish();
336
337         $query = "SELECT PathId FROM Path WHERE Path = ?";
338         #print STDERR $query,"\n" if $debug;
339         $sth = $self->{dbh}->prepare_cached($query);
340         $sth->execute($path);
341         $result = $sth->fetchrow_arrayref();
342         $sth->finish();
343         return $result->[0];
344     }
345 }
346
347 # list all files in a directory, accross curjobids
348 sub ls_files
349 {
350     my ($self) = @_;
351
352     return undef unless ($self->{curjobids});
353
354     my $inclause   = $self->{curjobids};
355     my $inpath = $self->{cwdid};
356     my $filter = '';
357     if ($self->{pattern}) {
358         $filter = " AND Filename.Name $self->{sql}->{MATCH} $self->{pattern} ";
359     }
360
361     my $query =
362 "SELECT File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
363  FROM File, (
364        SELECT Filename.Name, max(File.FileId) as id
365          FROM File, Filename
366         WHERE File.FilenameId = Filename.FilenameId
367           AND Filename.Name != ''
368           AND File.PathId = $inpath
369           AND File.JobId IN ($inclause)
370           $filter
371         GROUP BY Filename.Name
372         ORDER BY Filename.Name LIMIT $self->{limit} OFFSET $self->{offset}
373      ) AS listfiles
374 WHERE File.FileId = listfiles.id";
375
376 #    print STDERR $query;
377     $self->debug($query);
378     my $result = $self->dbh_selectall_arrayref($query);
379     $self->debug($result);
380
381     return $result;
382 }
383
384 sub ls_special_dirs
385 {
386     my ($self) = @_;
387     return undef unless ($self->{curjobids});
388
389     my $pathid = $self->{cwdid};
390     my $jobclause = $self->{curjobids};
391     my $dir_filenameid = $self->get_dir_filenameid();
392
393     my $sq1 =  
394 "((SELECT PPathId AS PathId, '..' AS Path
395     FROM  brestore_pathhierarchy 
396    WHERE  PathId = $pathid)
397 UNION
398  (SELECT $pathid AS PathId, '.' AS Path))";
399
400     my $sq2 = "
401 SELECT tmp.PathId, tmp.Path, LStat, JobId 
402   FROM $sq1 AS tmp  LEFT JOIN ( -- get attributes if any
403        SELECT File1.PathId, File1.JobId, File1.LStat FROM File AS File1
404        WHERE File1.FilenameId = $dir_filenameid
405        AND File1.JobId IN ($jobclause)) AS listfile1
406   ON (tmp.PathId = listfile1.PathId)
407   ORDER BY tmp.Path, JobId DESC
408 ";
409
410     my $result = $self->dbh_selectall_arrayref($sq2);
411
412     my @return_list;
413     my $prev_dir='';
414     foreach my $refrow (@{$result})
415     {
416         my $dirid = $refrow->[0];
417         my $dir = $refrow->[1];
418         my $lstat = $refrow->[3];
419         my $jobid = $refrow->[2] || 0;
420         next if ($dirid eq $prev_dir);
421         my @return_array = ($dirid,$dir,$lstat,$jobid);
422         push @return_list,(\@return_array);
423         $prev_dir = $dirid;
424     }
425  
426     return \@return_list;
427 }
428
429 # Let's retrieve the list of the visible dirs in this dir ...
430 # First, I need the empty filenameid to locate efficiently
431 # the dirs in the file table
432 sub get_dir_filenameid
433 {
434     my ($self) = @_;
435     if ($self->{dir_filenameid}) {
436         return $self->{dir_filenameid};
437     }
438     my $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
439     my $sth = $self->dbh_prepare($query);
440     $sth->execute();
441     my $result = $sth->fetchrow_arrayref();
442     $sth->finish();
443     return $self->{dir_filenameid} = $result->[0];
444 }
445
446 # list all directories in a directory, accross curjobids
447 # return ($dirid,$dir_basename,$lstat,$jobid)
448 sub ls_dirs
449 {
450     my ($self) = @_;
451
452     return undef unless ($self->{curjobids});
453
454     my $pathid = $self->{cwdid};
455     my $jobclause = $self->{curjobids};
456     my $filter ='';
457
458     if ($self->{pattern}) {
459         $filter = " AND Path2.Path $self->{sql}->{MATCH} $self->{pattern} ";
460     }
461
462     # Let's retrieve the list of the visible dirs in this dir ...
463     # First, I need the empty filenameid to locate efficiently
464     # the dirs in the file table
465     my $dir_filenameid = $self->get_dir_filenameid();
466
467     # Then we get all the dir entries from File ...
468     my $query = "
469 SELECT PathId, Path, JobId, LStat FROM (
470
471     SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
472            listfile1.JobId, listfile1.LStat
473     FROM (
474        SELECT DISTINCT brestore_pathhierarchy1.PathId
475        FROM brestore_pathhierarchy AS brestore_pathhierarchy1
476        JOIN Path AS Path2
477            ON (brestore_pathhierarchy1.PathId = Path2.PathId)
478        JOIN brestore_pathvisibility AS brestore_pathvisibility1
479            ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
480        WHERE brestore_pathhierarchy1.PPathId = $pathid
481        AND brestore_pathvisibility1.jobid IN ($jobclause)
482            $filter
483      ) AS listpath1
484    JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
485
486    LEFT JOIN ( -- get attributes if any
487        SELECT File1.PathId, File1.JobId, File1.LStat FROM File AS File1
488        WHERE File1.FilenameId = $dir_filenameid
489        AND File1.JobId IN ($jobclause)) AS listfile1
490        ON (listpath1.PathId = listfile1.PathId)
491      ) AS A ORDER BY 2,3 DESC LIMIT $self->{limit} OFFSET $self->{offset} 
492 ";
493 #    print STDERR $query;
494     my $sth=$self->dbh_prepare($query);
495     $sth->execute();
496     my $result = $sth->fetchall_arrayref();
497     my @return_list;
498     my $prev_dir='';
499     foreach my $refrow (@{$result})
500     {
501         my $dirid = $refrow->[0];
502         my $dir = $refrow->[1];
503         my $lstat = $refrow->[3];
504         my $jobid = $refrow->[2] || 0;
505         next if ($dirid eq $prev_dir);
506         # We have to clean up this dirname ... we only want it's 'basename'
507         my $return_value;
508         if ($dir ne '/')
509         {
510             my @temp = split ('/',$dir);
511             $return_value = pop @temp;
512         }
513         else
514         {
515             $return_value = '/';
516         }
517         my @return_array = ($dirid,$return_value,$lstat,$jobid);
518         push @return_list,(\@return_array);
519         $prev_dir = $dirid;
520     }
521     $self->debug(\@return_list);
522     return \@return_list;
523 }
524
525 # TODO : we want be able to restore files from a bad ended backup
526 # we have JobStatus IN ('T', 'A', 'E') and we must
527
528 # Data acces subs from here. Interaction with SGBD and caching
529
530 # This sub retrieves the list of jobs corresponding to the jobs selected in the
531 # GUI and stores them in @CurrentJobIds.
532 # date must be quoted
533 sub set_job_ids_for_date
534 {
535     my ($self, $client, $date)=@_;
536
537     if (!$client or !$date) {
538         return ();
539     }
540     my $filter = $self->get_client_filter();
541     # The algorithm : for a client, we get all the backups for each
542     # fileset, in reverse order Then, for each fileset, we store the 'good'
543     # incrementals and differentials until we have found a full so it goes
544     # like this : store all incrementals until we have found a differential
545     # or a full, then find the full
546     my $query = "
547 SELECT JobId, FileSet, Level, JobStatus
548   FROM Job 
549        JOIN FileSet USING (FileSetId)
550        JOIN Client USING (ClientId) $filter
551  WHERE EndTime <= $date
552    AND Client.Name = '$client'
553    AND Type IN ('B')
554    AND JobStatus IN ('T')
555  ORDER BY FileSet, JobTDate DESC";
556
557     my @CurrentJobIds;
558     my $result = $self->dbh_selectall_arrayref($query);
559     my %progress;
560     foreach my $refrow (@$result)
561     {
562         my $jobid = $refrow->[0];
563         my $fileset = $refrow->[1];
564         my $level = $refrow->[2];
565
566         defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
567
568         next if $progress{$fileset} eq 'F'; # It's over for this fileset...
569
570         if ($level eq 'I')
571         {
572             next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
573             push @CurrentJobIds,($jobid);
574         }
575         elsif ($level eq 'D')
576         {
577             next if $progress{$fileset} eq 'D'; # We allready have a differential
578             push @CurrentJobIds,($jobid);
579         }
580         elsif ($level eq 'F')
581         {
582             push @CurrentJobIds,($jobid);
583         }
584
585         my $status = $refrow->[3] ;
586         if ($status eq 'T') {              # good end of job
587             $progress{$fileset} = $level;
588         }
589     }
590
591     return @CurrentJobIds;
592 }
593
594 sub dbh_selectrow_arrayref
595 {
596     my ($self, $query) = @_;
597     $self->debug($query, up => 1);
598     return $self->{dbh}->selectrow_arrayref($query);
599 }
600
601 # Returns list of versions of a file that could be restored
602 # returns an array of
603 # (jobid,fileindex,mtime,size,inchanger,md5,volname,fileid)
604 # there will be only one jobid in the array of jobids...
605 sub get_all_file_versions
606 {
607     my ($self,$pathid,$fileid,$client,$see_all,$see_copies)=@_;
608
609     defined $see_all or $see_all=0;
610     my $backup_type=" AND Job.Type = 'B' ";
611     if ($see_copies) {
612         $backup_type=" AND Job.Type IN ('C', 'B') ";
613     }
614
615     my @versions;
616     my $query;
617     $query =
618 "SELECT File.JobId, File.FileId, File.LStat,
619         File.Md5, Media.VolumeName, Media.InChanger
620  FROM File, Job, Client, JobMedia, Media
621  WHERE File.FilenameId = $fileid
622    AND File.PathId=$pathid
623    AND File.JobId = Job.JobId
624    AND Job.ClientId = Client.ClientId
625    AND Job.JobId = JobMedia.JobId
626    AND File.FileIndex >= JobMedia.FirstIndex
627    AND File.FileIndex <= JobMedia.LastIndex
628    AND JobMedia.MediaId = Media.MediaId
629    AND Client.Name = '$client'
630    $backup_type
631 ";
632
633     $self->debug($query);
634     my $result = $self->dbh_selectall_arrayref($query);
635
636     foreach my $refrow (@$result)
637     {
638         my ($jobid, $fid, $lstat, $md5, $volname, $inchanger) = @$refrow;
639         my @attribs = parse_lstat($lstat);
640         my $mtime = array_attrib('st_mtime',\@attribs);
641         my $size = array_attrib('st_size',\@attribs);
642
643         my @list = ($pathid,$fileid,$jobid,
644                     $fid, $mtime, $size, $inchanger,
645                     $md5, $volname);
646         push @versions, (\@list);
647     }
648
649     # We have the list of all versions of this file.
650     # We'll sort it by mtime desc, size, md5, inchanger desc, FileId
651     # the rest of the algorithm will be simpler
652     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
653     @versions = sort { $b->[4] <=> $a->[4]
654                     || $a->[5] <=> $b->[5]
655                     || $a->[7] cmp $a->[7]
656                     || $b->[6] <=> $a->[6]} @versions;
657
658     my @good_versions;
659     my %allready_seen_by_mtime;
660     my %allready_seen_by_md5;
661     # Now we should create a new array with only the interesting records
662     foreach my $ref (@versions)
663     {
664         if ($ref->[7])
665         {
666             # The file has a md5. We compare his md5 to other known md5...
667             # We take size into account. It may happen that 2 files
668             # have the same md5sum and are different. size is a supplementary
669             # criterion
670
671             # If we allready have a (better) version
672             next if ( (not $see_all)
673                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
674
675             # we never met this one before...
676             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
677         }
678         # Even if it has a md5, we should also work with mtimes
679         # We allready have a (better) version
680         next if ( (not $see_all)
681                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
682         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
683
684         # We reached there. The file hasn't been seen.
685         push @good_versions,($ref);
686     }
687
688     # To be nice with the user, we re-sort good_versions by
689     # inchanger desc, mtime desc
690     @good_versions = sort { $b->[4] <=> $a->[4]
691                          || $b->[2] <=> $a->[2]} @good_versions;
692
693     return \@good_versions;
694 }
695 {
696     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
697                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
698                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
699                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
700                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
701                           'data_stream' => 15);;
702     sub array_attrib
703     {
704         my ($attrib,$ref_attrib)=@_;
705         return $ref_attrib->[$attrib_name_id{$attrib}];
706     }
707
708     sub file_attrib
709     {   # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
710
711         my ($file, $attrib)=@_;
712
713         if (defined $attrib_name_id{$attrib}) {
714
715             my @d = split(' ', $file->[3]) ; # TODO : cache this
716
717             return from_base64($d[$attrib_name_id{$attrib}]);
718
719         } elsif ($attrib eq 'jobid') {
720
721             return $file->[4];
722
723         } elsif ($attrib eq 'name') {
724
725             return $file->[2];
726
727         } else  {
728             die "Attribute not known : $attrib.\n";
729         }
730     }
731
732     sub lstat_attrib
733     {
734         my ($lstat,$attrib)=@_;
735         if ($lstat and defined $attrib_name_id{$attrib})
736         {
737             my @d = split(' ', $lstat) ; # TODO : cache this
738             return from_base64($d[$attrib_name_id{$attrib}]);
739         }
740         return 0;
741     }
742 }
743
744 {
745     # Base 64 functions, directly from recover.pl.
746     # Thanks to
747     # Karl Hakimian <hakimian@aha.com>
748     # This section is also under GPL v2 or later.
749     my @base64_digits;
750     my @base64_map;
751     my $is_init=0;
752     sub init_base64
753     {
754         @base64_digits = (
755         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
756         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
757         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
758         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
759         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
760                           );
761         @base64_map = (0) x 128;
762
763         for (my $i=0; $i<64; $i++) {
764             $base64_map[ord($base64_digits[$i])] = $i;
765         }
766         $is_init = 1;
767     }
768
769     sub from_base64 {
770         if(not $is_init)
771         {
772             init_base64();
773         }
774         my $where = shift;
775         my $val = 0;
776         my $i = 0;
777         my $neg = 0;
778
779         if (substr($where, 0, 1) eq '-') {
780             $neg = 1;
781             $where = substr($where, 1);
782         }
783
784         while ($where ne '') {
785             $val *= 64;
786             my $d = substr($where, 0, 1);
787             $val += $base64_map[ord(substr($where, 0, 1))];
788             $where = substr($where, 1);
789         }
790
791         return $val;
792     }
793
794     sub parse_lstat {
795         my ($lstat)=@_;
796         my @attribs = split(' ',$lstat);
797         foreach my $element (@attribs)
798         {
799             $element = from_base64($element);
800         }
801         return @attribs;
802     }
803 }
804
805 # get jobids that the current user can view (ACL)
806 sub get_jobids
807 {
808   my ($self, @jobid) = @_;
809   my $filter = $self->get_client_filter();
810   if ($filter) {
811     my $jobids = $self->dbh_join(@jobid);
812     my $q="
813 SELECT JobId 
814   FROM Job JOIN Client USING (ClientId) $filter 
815  WHERE Jobid IN ($jobids)";
816     my $res = $self->dbh_selectall_arrayref($q);
817     @jobid = map { $_->[0] } @$res;
818   }
819   return @jobid;
820 }
821
822 ################################################################
823
824
825 package main;
826 use strict;
827 use POSIX qw/strftime/;
828 use Bweb;
829
830 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
831 $conf->load();
832
833 my $bvfs = new Bvfs(info => $conf);
834 $bvfs->connect_db();
835
836 my $action = CGI::param('action') || '';
837
838 my $args = $bvfs->get_form('pathid', 'filenameid', 'fileid', 'qdate',
839                            'limit', 'offset', 'client', 'qpattern');
840
841 if ($action eq 'batch') {
842     $bvfs->update_cache();
843     exit 0;
844 }
845
846 # All these functions are returning JSON compatible data
847 # for javascript parsing
848
849 if ($action eq 'list_client') { # list all client [ ['c1'],['c2']..]
850     print CGI::header('application/x-javascript');
851
852     my $filter = $bvfs->get_client_filter();
853     my $q = "SELECT Name FROM Client $filter";
854     my $ret = $bvfs->dbh_selectall_arrayref($q);
855
856     print "[";
857     print join(',', map { "['$_->[0]']" } @$ret);
858     print "]\n";
859     exit 0;
860     
861 } elsif ($action eq 'list_job') {
862     # list jobs for a client [[jobid,endtime,'desc'],..]
863
864     print CGI::header('application/x-javascript');
865     
866     my $filter = $bvfs->get_client_filter();
867     my $query = "
868  SELECT Job.JobId,Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus
869   FROM Job JOIN FileSet USING (FileSetId) JOIN Client USING (ClientId) $filter
870  WHERE Client.Name = '$args->{client}'
871    AND Job.Type = 'B'
872    AND JobStatus IN ('f', 'T')
873  ORDER BY EndTime desc";
874     my $result = $bvfs->dbh_selectall_arrayref($query);
875
876     print "[";
877
878     print join(',', map {
879       "[$_->[0], '$_->[1]', '$_->[1] $_->[2] $_->[3] ($_->[4]) $_->[0]']"
880       } @$result);
881
882     print "]\n";
883     exit 0;
884 } elsif ($action eq 'list_storage') { # TODO: use .storage here
885     print CGI::header('application/x-javascript');
886
887     my $q="SELECT Name FROM Storage";
888     my $lst = $bvfs->dbh_selectall_arrayref($q);
889     print "[";
890     print join(',', map { "[ '$_->[0]' ]" } @$lst);
891     print "]\n";
892     exit 0;
893 }
894
895 sub fill_table_for_restore
896 {
897     my (@jobid) = @_;
898
899     # in "force" mode, we need the FileId to compute media list
900     my $FileId = CGI::param('force')?",FileId":"";
901
902     my $fileid = join(',', grep { /^\d+$/ } CGI::param('fileid'));
903     # can get dirid=("10,11", 10, 11)
904     my @dirid = grep { /^\d+$/ } map { split(/,/) } CGI::param('dirid') ;
905     my $inclause = join(',', @jobid);
906
907     my @union;
908
909     if ($fileid) {
910       push @union,
911       "(SELECT JobId, FileIndex, FilenameId, PathId $FileId
912           FROM File WHERE FileId IN ($fileid))";
913     }
914
915     foreach my $dirid (@dirid) {
916         my $p = $bvfs->get_path($dirid);
917         $p =~ s/([%_\\])/\\$1/g;  # Escape % and _ for LIKE search
918         $p = $bvfs->dbh_quote($p);
919         push @union, "
920   (SELECT File.JobId, File.FileIndex, File.FilenameId, File.PathId $FileId
921     FROM Path JOIN File USING (PathId)
922    WHERE Path.Path LIKE " . $bvfs->dbh_strcat($p, "'%'") . "
923      AND File.JobId IN ($inclause))";
924     }
925
926     return unless scalar(@union);
927
928     my $u = join(" UNION ", @union);
929
930     $bvfs->dbh_do("CREATE TEMPORARY TABLE btemp AS $u");
931     # TODO: remove FilenameId et PathId
932
933     # now we have to choose the file with the max(jobid)
934     # for each file of btemp
935     if ($bvfs->dbh_is_mysql()) {
936        $bvfs->dbh_do("CREATE TABLE b2$$ AS (
937 SELECT max(JobId) as JobId, FileIndex $FileId
938   FROM btemp
939  GROUP BY PathId, FilenameId
940  HAVING FileIndex > 0
941 )");
942    } else { # postgresql have distinct with more than one criteria
943         $bvfs->dbh_do("CREATE TABLE b2$$ AS (
944 SELECT JobId, FileIndex $FileId
945 FROM (
946  SELECT DISTINCT ON (PathId, FilenameId) JobId, FileIndex $FileId
947    FROM btemp
948   ORDER BY PathId, FilenameId, JobId DESC
949  ) AS T
950  WHERE FileIndex > 0
951 )");
952     }
953
954     return "b2$$";
955 }
956
957 sub get_media_list_with_dir
958 {
959     my ($table) = @_;
960     my $q="
961  SELECT DISTINCT VolumeName, Enabled, InChanger
962    FROM $table,
963     ( -- Get all media from this job
964       SELECT MIN(FirstIndex) AS FirstIndex, MAX(LastIndex) AS LastIndex,
965              VolumeName, Enabled, Inchanger
966         FROM JobMedia JOIN Media USING (MediaId)
967        WHERE JobId IN (SELECT DISTINCT JobId FROM $table)
968        GROUP BY VolumeName,Enabled,InChanger
969     ) AS allmedia
970   WHERE $table.FileIndex >= allmedia.FirstIndex
971     AND $table.FileIndex <= allmedia.LastIndex
972 ";
973     my $lst = $bvfs->dbh_selectall_arrayref($q);
974     return $lst;
975 }
976
977 sub get_media_list
978 {
979     my ($jobid, $fileid) = @_;
980     my $q="
981  SELECT DISTINCT VolumeName, Enabled, InChanger
982    FROM File,
983     ( -- Get all media from this job
984       SELECT MIN(FirstIndex) AS FirstIndex, MAX(LastIndex) AS LastIndex,
985              VolumeName, Enabled, Inchanger
986         FROM JobMedia JOIN Media USING (MediaId)
987        WHERE JobId IN ($jobid)
988        GROUP BY VolumeName,Enabled,InChanger
989     ) AS allmedia
990   WHERE File.FileId IN ($fileid)
991     AND File.FileIndex >= allmedia.FirstIndex
992     AND File.FileIndex <= allmedia.LastIndex
993 ";
994     my $lst = $bvfs->dbh_selectall_arrayref($q);
995     return $lst;
996 }
997
998 # get jobid param and apply user filter
999 my @jobid = $bvfs->get_jobids(grep { /^\d+(,\d+)*$/ } CGI::param('jobid'));
1000
1001 # get jobid from date arg
1002 if (!scalar(@jobid) and $args->{qdate} and $args->{client}) {
1003     @jobid = $bvfs->set_job_ids_for_date($args->{client}, $args->{qdate});
1004 }
1005
1006 $bvfs->set_curjobids(@jobid);
1007 $bvfs->set_limits($args->{offset}, $args->{limit});
1008
1009 if (!scalar(@jobid)) {
1010     exit 0;
1011 }
1012
1013 if (CGI::param('init')) { # used when choosing a job
1014     $bvfs->update_brestore_table(@jobid);
1015 }
1016
1017 my $pathid = CGI::param('node') || CGI::param('pathid') || '';
1018 my $path = CGI::param('path');
1019
1020 if ($pathid =~ /^(\d+)$/) {
1021     $pathid = $1;
1022 } elsif ($path) {
1023     $pathid = $bvfs->get_pathid($path);
1024 } else {
1025     $pathid = $bvfs->get_root();
1026 }
1027 $bvfs->ch_dir($pathid);
1028
1029 #print STDERR "pathid=$pathid\n";
1030
1031 # permit to use a regex filter
1032 if ($args->{qpattern}) {
1033     $bvfs->set_pattern($args->{qpattern});
1034 }
1035
1036 if ($action eq 'restore') {
1037
1038     # TODO: pouvoir choisir le replace et le jobname
1039     my $arg = $bvfs->get_form(qw/client storage regexwhere where/);
1040
1041     if (!$arg->{client}) {
1042         print "ERROR: missing client\n";
1043         exit 1;
1044     }
1045
1046     my $table = fill_table_for_restore(@jobid);
1047     if (!$table) {
1048         exit 1;
1049     }
1050
1051     my $bconsole = $bvfs->get_bconsole();
1052     # TODO: pouvoir choisir le replace et le jobname
1053     my $jobid = $bconsole->run(client    => $arg->{client},
1054                                storage   => $arg->{storage},
1055                                where     => $arg->{where},
1056                                regexwhere=> $arg->{regexwhere},
1057                                restore   => 1,
1058                                file      => "?$table");
1059     
1060     $bvfs->dbh_do("DROP TABLE $table");
1061
1062     if (!$jobid) {
1063         print CGI::header('text/html');
1064         $bvfs->display_begin();
1065         $bvfs->error("Can't start your job:<br/>" . $bconsole->before());
1066         $bvfs->display_end();
1067         exit 0;
1068     }
1069     sleep(2);
1070     print CGI::redirect("bweb.pl?action=dsp_cur_job;jobid=$jobid") ;
1071     exit 0;
1072 }
1073 sub escape_quote
1074 {
1075     my ($str) = @_;
1076     my %esc = (
1077         "\n" => '\n',
1078         "\r" => '\r',
1079         "\t" => '\t',
1080         "\f" => '\f',
1081         "\b" => '\b',
1082         "\"" => '\"',
1083         "\\" => '\\\\',
1084         "\'" => '\\\'',
1085     );
1086
1087     if (!$str) {
1088         return '';
1089     }
1090
1091     $str =~ s/([\x22\x5c\n\r\t\f\b])/$esc{$1}/g;
1092     $str =~ s/\//\\\//g;
1093     $str =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg;
1094     return $str;
1095 }
1096
1097 print CGI::header('application/x-javascript');
1098
1099
1100 if ($action eq 'list_files_dirs') {
1101 # fileid, filenameid, pathid, jobid, name, size, mtime
1102     my $jids = join(",", @jobid);
1103
1104     my $files = $bvfs->ls_special_dirs();
1105     # return ($dirid,$dir_basename,$lstat,$jobid)
1106     print "[\n";
1107     print join(',',
1108                map { my @p=Bvfs::parse_lstat($_->[3]); 
1109                      '[' . join(',', 
1110                                 0, # fileid
1111                                 0, # filenameid
1112                                 $_->[0], # pathid
1113                                 "'$jids'", # jobid
1114                                 '"' . escape_quote($_->[1]) . '"', # name
1115                                 "'" . $p[7] . "'",                 # size
1116                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11]||0)) .  "'") .
1117                     ']'; 
1118                } @$files);
1119     print "," if (@$files);
1120
1121     $files = $bvfs->ls_dirs();
1122     # return ($dirid,$dir_basename,$lstat,$jobid)
1123     print join(',',
1124                map { my @p=Bvfs::parse_lstat($_->[3]); 
1125                      '[' . join(',', 
1126                                 0, # fileid
1127                                 0, # filenameid
1128                                 $_->[0], # pathid
1129                                 "'$jids'", # jobid
1130                                 '"' . escape_quote($_->[1]) . '"', # name
1131                                 "'" . $p[7] . "'",                 # size
1132                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11]||0)) .  "'") .
1133                     ']'; 
1134                } @$files);
1135
1136     print "," if (@$files);
1137  
1138     $files = $bvfs->ls_files();
1139     print join(',',
1140                map { my @p=Bvfs::parse_lstat($_->[3]); 
1141                      '[' . join(',', 
1142                                 $_->[1],
1143                                 $_->[0],
1144                                 $pathid,
1145                                 $_->[4],
1146                                 '"' . escape_quote($_->[2]) . '"', # name
1147                                 "'" . $p[7] . "'",
1148                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
1149                     ']'; 
1150                } @$files);
1151     print "]\n";
1152
1153 } elsif ($action eq 'list_files') {
1154     print "[[0,0,0,0,'.',4096,'1970-01-01 00:00:00'],";
1155     my $files = $bvfs->ls_files();
1156 #       [ 1, 2, 3, "Bill",  10, '2007-01-01 00:00:00'],
1157 #   File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
1158
1159     print join(',',
1160                map { my @p=Bvfs::parse_lstat($_->[3]); 
1161                      '[' . join(',', 
1162                                 $_->[1],
1163                                 $_->[0],
1164                                 $pathid,
1165                                 $_->[4],
1166                                 '"' . escape_quote($_->[2]) . '"', # name
1167                                 "'" . $p[7] . "'",
1168                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
1169                     ']'; 
1170                } @$files);
1171     print "]\n";
1172
1173 } elsif ($action eq 'list_dirs') {
1174
1175     print "[";
1176     my $dirs = $bvfs->ls_dirs();
1177     # return ($dirid,$dir_basename,$lstat,$jobid)
1178
1179     print join(',',
1180                map { "{ 'jobid': '$bvfs->{curjobids}', 'id': '$_->[0]'," . 
1181                         "'text': '" . escape_quote($_->[1]) . "', 'cls':'folder'}" }
1182                @$dirs);
1183     print "]\n";
1184
1185 } elsif ($action eq 'list_versions') {
1186
1187     my $vafv = CGI::param('vafv') || 'false'; # view all file versions
1188     $vafv = ($vafv eq 'false')?0:1;
1189
1190     my $vcopies = CGI::param('vcopies') || 'false'; # view copies file versions
1191     $vcopies = ($vcopies eq 'false')?0:1;
1192
1193     print "[";
1194     #   0       1       2        3   4       5      6           7      8
1195     #($pathid,$fileid,$jobid, $fid, $mtime, $size, $inchanger, $md5, $volname);
1196     my $files = $bvfs->get_all_file_versions($args->{pathid}, $args->{filenameid}, $args->{client}, $vafv, $vcopies);
1197     print join(',',
1198                map { "[ $_->[3], $_->[1], $_->[0], $_->[2], '$_->[8]', $_->[6], '$_->[7]', $_->[5],'" . strftime('%Y-%m-%d %H:%m:%S', localtime($_->[4])) . "']" }
1199                @$files);
1200     print "]\n";
1201
1202 # this action is used when the restore box appear, we can display
1203 # the media list that will be needed for restore
1204 } elsif ($action eq 'get_media') {
1205     my ($jobid, $fileid, $table);
1206     my $lst;
1207
1208     # in this mode, we compute the result to get all needed media
1209 #    print STDERR "force=", CGI::param('force'), "\n";
1210     if (CGI::param('force')) {
1211         $table = fill_table_for_restore(@jobid);
1212         if (!$table) {
1213             exit 1;
1214         }
1215         # mysql is very slow without this index...
1216         if ($bvfs->dbh_is_mysql()) {
1217             $bvfs->dbh_do("CREATE INDEX idx_$table ON $table (JobId)");
1218         }
1219         $lst = get_media_list_with_dir($table);
1220     } else {
1221         $jobid = join(',', @jobid);
1222         $fileid = join(',', grep { /^\d+(,\d+)*$/ } CGI::param('fileid'));
1223         $lst = get_media_list($jobid, $fileid);
1224     }        
1225     
1226     if ($lst) {
1227         print "[";
1228         print join(',', map { "['$_->[0]',$_->[1],$_->[2]]" } @$lst);
1229         print "]\n";
1230     }
1231
1232     if ($table) {
1233         $bvfs->dbh_do("DROP TABLE $table");
1234     }
1235
1236 }
1237
1238 __END__
1239
1240 CREATE VIEW files AS
1241  SELECT path || name AS name,pathid,filenameid,fileid,jobid
1242    FROM File JOIN FileName USING (FilenameId) JOIN Path USING (PathId);
1243
1244 SELECT 'drop table ' || tablename || ';'
1245     FROM pg_tables WHERE tablename ~ '^b[0-9]';