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