]> git.sur5r.net Git - bacula/bacula/blob - gui/bweb/cgi/bresto.pl
Merge branch 'bwebII'
[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    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
457     # Let's retrieve the list of the visible dirs in this dir ...
458     # First, I need the empty filenameid to locate efficiently
459     # the dirs in the file table
460     my $dir_filenameid = $self->get_dir_filenameid();
461
462     # Then we get all the dir entries from File ...
463     my $query = "
464 SELECT PathId, Path, JobId, Lstat FROM (
465
466     SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
467            listfile1.JobId, listfile1.Lstat
468     FROM (
469        SELECT DISTINCT brestore_pathhierarchy1.PathId
470        FROM brestore_pathhierarchy AS brestore_pathhierarchy1
471        JOIN Path AS Path2
472            ON (brestore_pathhierarchy1.PathId = Path2.PathId)
473        JOIN brestore_pathvisibility AS brestore_pathvisibility1
474            ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
475        WHERE brestore_pathhierarchy1.PPathId = $pathid
476        AND brestore_pathvisibility1.jobid IN ($jobclause)) AS listpath1
477    JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
478
479    LEFT JOIN ( -- get attributes if any
480        SELECT File1.PathId, File1.JobId, File1.Lstat FROM File AS File1
481        WHERE File1.FilenameId = $dir_filenameid
482        AND File1.JobId IN ($jobclause)) AS listfile1
483        ON (listpath1.PathId = listfile1.PathId)
484      ) AS A ORDER BY 2,3 DESC LIMIT $self->{limit} OFFSET $self->{offset} 
485 ";
486 #    print STDERR $query;
487     my $sth=$self->dbh_prepare($query);
488     $sth->execute();
489     my $result = $sth->fetchall_arrayref();
490     my @return_list;
491     my $prev_dir='';
492     foreach my $refrow (@{$result})
493     {
494         my $dirid = $refrow->[0];
495         my $dir = $refrow->[1];
496         my $lstat = $refrow->[3];
497         my $jobid = $refrow->[2] || 0;
498         next if ($dirid eq $prev_dir);
499         # We have to clean up this dirname ... we only want it's 'basename'
500         my $return_value;
501         if ($dir ne '/')
502         {
503             my @temp = split ('/',$dir);
504             $return_value = pop @temp;
505         }
506         else
507         {
508             $return_value = '/';
509         }
510         my @return_array = ($dirid,$return_value,$lstat,$jobid);
511         push @return_list,(\@return_array);
512         $prev_dir = $dirid;
513     }
514     $self->debug(\@return_list);
515     return \@return_list;
516 }
517
518 # TODO : we want be able to restore files from a bad ended backup
519 # we have JobStatus IN ('T', 'A', 'E') and we must
520
521 # Data acces subs from here. Interaction with SGBD and caching
522
523 # This sub retrieves the list of jobs corresponding to the jobs selected in the
524 # GUI and stores them in @CurrentJobIds.
525 # date must be quoted
526 sub set_job_ids_for_date
527 {
528     my ($self, $client, $date)=@_;
529
530     if (!$client or !$date) {
531         return ();
532     }
533     my $filter = $self->get_client_filter();
534     # The algorithm : for a client, we get all the backups for each
535     # fileset, in reverse order Then, for each fileset, we store the 'good'
536     # incrementals and differentials until we have found a full so it goes
537     # like this : store all incrementals until we have found a differential
538     # or a full, then find the full
539     my $query = "
540 SELECT JobId, FileSet, Level, JobStatus
541   FROM Job 
542        JOIN FileSet USING (FileSetId)
543        JOIN Client USING (ClientId) $filter
544  WHERE EndTime <= $date
545    AND Client.Name = '$client'
546    AND Type IN ('B')
547    AND JobStatus IN ('T')
548  ORDER BY FileSet, JobTDate DESC";
549
550     my @CurrentJobIds;
551     my $result = $self->dbh_selectall_arrayref($query);
552     my %progress;
553     foreach my $refrow (@$result)
554     {
555         my $jobid = $refrow->[0];
556         my $fileset = $refrow->[1];
557         my $level = $refrow->[2];
558
559         defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
560
561         next if $progress{$fileset} eq 'F'; # It's over for this fileset...
562
563         if ($level eq 'I')
564         {
565             next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
566             push @CurrentJobIds,($jobid);
567         }
568         elsif ($level eq 'D')
569         {
570             next if $progress{$fileset} eq 'D'; # We allready have a differential
571             push @CurrentJobIds,($jobid);
572         }
573         elsif ($level eq 'F')
574         {
575             push @CurrentJobIds,($jobid);
576         }
577
578         my $status = $refrow->[3] ;
579         if ($status eq 'T') {              # good end of job
580             $progress{$fileset} = $level;
581         }
582     }
583
584     return @CurrentJobIds;
585 }
586
587 sub dbh_selectrow_arrayref
588 {
589     my ($self, $query) = @_;
590     $self->debug($query, up => 1);
591     return $self->{dbh}->selectrow_arrayref($query);
592 }
593
594 # Returns list of versions of a file that could be restored
595 # returns an array of
596 # (jobid,fileindex,mtime,size,inchanger,md5,volname,fileid)
597 # there will be only one jobid in the array of jobids...
598 sub get_all_file_versions
599 {
600     my ($self,$pathid,$fileid,$client,$see_all,$see_copies)=@_;
601
602     defined $see_all or $see_all=0;
603     my $backup_type=" AND Job.Type = 'B' ";
604     if ($see_copies) {
605         $backup_type=" AND Job.Type IN ('C', 'B') ";
606     }
607
608     my @versions;
609     my $query;
610     $query =
611 "SELECT File.JobId, File.FileId, File.Lstat,
612         File.Md5, Media.VolumeName, Media.InChanger
613  FROM File, Job, Client, JobMedia, Media
614  WHERE File.FilenameId = $fileid
615    AND File.PathId=$pathid
616    AND File.JobId = Job.JobId
617    AND Job.ClientId = Client.ClientId
618    AND Job.JobId = JobMedia.JobId
619    AND File.FileIndex >= JobMedia.FirstIndex
620    AND File.FileIndex <= JobMedia.LastIndex
621    AND JobMedia.MediaId = Media.MediaId
622    AND Client.Name = '$client'
623    $backup_type
624 ";
625
626     $self->debug($query);
627     my $result = $self->dbh_selectall_arrayref($query);
628
629     foreach my $refrow (@$result)
630     {
631         my ($jobid, $fid, $lstat, $md5, $volname, $inchanger) = @$refrow;
632         my @attribs = parse_lstat($lstat);
633         my $mtime = array_attrib('st_mtime',\@attribs);
634         my $size = array_attrib('st_size',\@attribs);
635
636         my @list = ($pathid,$fileid,$jobid,
637                     $fid, $mtime, $size, $inchanger,
638                     $md5, $volname);
639         push @versions, (\@list);
640     }
641
642     # We have the list of all versions of this file.
643     # We'll sort it by mtime desc, size, md5, inchanger desc, FileId
644     # the rest of the algorithm will be simpler
645     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
646     @versions = sort { $b->[4] <=> $a->[4]
647                     || $a->[5] <=> $b->[5]
648                     || $a->[7] cmp $a->[7]
649                     || $b->[6] <=> $a->[6]} @versions;
650
651     my @good_versions;
652     my %allready_seen_by_mtime;
653     my %allready_seen_by_md5;
654     # Now we should create a new array with only the interesting records
655     foreach my $ref (@versions)
656     {
657         if ($ref->[7])
658         {
659             # The file has a md5. We compare his md5 to other known md5...
660             # We take size into account. It may happen that 2 files
661             # have the same md5sum and are different. size is a supplementary
662             # criterion
663
664             # If we allready have a (better) version
665             next if ( (not $see_all)
666                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
667
668             # we never met this one before...
669             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
670         }
671         # Even if it has a md5, we should also work with mtimes
672         # We allready have a (better) version
673         next if ( (not $see_all)
674                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
675         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
676
677         # We reached there. The file hasn't been seen.
678         push @good_versions,($ref);
679     }
680
681     # To be nice with the user, we re-sort good_versions by
682     # inchanger desc, mtime desc
683     @good_versions = sort { $b->[4] <=> $a->[4]
684                          || $b->[2] <=> $a->[2]} @good_versions;
685
686     return \@good_versions;
687 }
688 {
689     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
690                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
691                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
692                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
693                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
694                           'data_stream' => 15);;
695     sub array_attrib
696     {
697         my ($attrib,$ref_attrib)=@_;
698         return $ref_attrib->[$attrib_name_id{$attrib}];
699     }
700
701     sub file_attrib
702     {   # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
703
704         my ($file, $attrib)=@_;
705
706         if (defined $attrib_name_id{$attrib}) {
707
708             my @d = split(' ', $file->[3]) ; # TODO : cache this
709
710             return from_base64($d[$attrib_name_id{$attrib}]);
711
712         } elsif ($attrib eq 'jobid') {
713
714             return $file->[4];
715
716         } elsif ($attrib eq 'name') {
717
718             return $file->[2];
719
720         } else  {
721             die "Attribute not known : $attrib.\n";
722         }
723     }
724
725     sub lstat_attrib
726     {
727         my ($lstat,$attrib)=@_;
728         if ($lstat and defined $attrib_name_id{$attrib})
729         {
730             my @d = split(' ', $lstat) ; # TODO : cache this
731             return from_base64($d[$attrib_name_id{$attrib}]);
732         }
733         return 0;
734     }
735 }
736
737 {
738     # Base 64 functions, directly from recover.pl.
739     # Thanks to
740     # Karl Hakimian <hakimian@aha.com>
741     # This section is also under GPL v2 or later.
742     my @base64_digits;
743     my @base64_map;
744     my $is_init=0;
745     sub init_base64
746     {
747         @base64_digits = (
748         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
749         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
750         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
751         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
752         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
753                           );
754         @base64_map = (0) x 128;
755
756         for (my $i=0; $i<64; $i++) {
757             $base64_map[ord($base64_digits[$i])] = $i;
758         }
759         $is_init = 1;
760     }
761
762     sub from_base64 {
763         if(not $is_init)
764         {
765             init_base64();
766         }
767         my $where = shift;
768         my $val = 0;
769         my $i = 0;
770         my $neg = 0;
771
772         if (substr($where, 0, 1) eq '-') {
773             $neg = 1;
774             $where = substr($where, 1);
775         }
776
777         while ($where ne '') {
778             $val *= 64;
779             my $d = substr($where, 0, 1);
780             $val += $base64_map[ord(substr($where, 0, 1))];
781             $where = substr($where, 1);
782         }
783
784         return $val;
785     }
786
787     sub parse_lstat {
788         my ($lstat)=@_;
789         my @attribs = split(' ',$lstat);
790         foreach my $element (@attribs)
791         {
792             $element = from_base64($element);
793         }
794         return @attribs;
795     }
796 }
797
798 # get jobids that the current user can view (ACL)
799 sub get_jobids
800 {
801   my ($self, @jobid) = @_;
802   my $filter = $self->get_client_filter();
803   if ($filter) {
804     my $jobids = $self->dbh_join(@jobid);
805     my $q="
806 SELECT JobId 
807   FROM Job JOIN Client USING (ClientId) $filter 
808  WHERE Jobid IN ($jobids)";
809     my $res = $self->dbh_selectall_arrayref($q);
810     @jobid = map { $_->[0] } @$res;
811   }
812   return @jobid;
813 }
814
815 ################################################################
816
817
818 package main;
819 use strict;
820 use POSIX qw/strftime/;
821 use Bweb;
822
823 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
824 $conf->load();
825
826 my $bvfs = new Bvfs(info => $conf);
827 $bvfs->connect_db();
828
829 my $action = CGI::param('action') || '';
830
831 my $args = $bvfs->get_form('pathid', 'filenameid', 'fileid', 'qdate',
832                            'limit', 'offset', 'client', 'qpattern');
833
834 if ($action eq 'batch') {
835     $bvfs->update_cache();
836     exit 0;
837 }
838
839 # All these functions are returning JSON compatible data
840 # for javascript parsing
841
842 if ($action eq 'list_client') { # list all client [ ['c1'],['c2']..]
843     print CGI::header('application/x-javascript');
844
845     my $filter = $bvfs->get_client_filter();
846     my $q = "SELECT Name FROM Client $filter";
847     my $ret = $bvfs->dbh_selectall_arrayref($q);
848
849     print "[";
850     print join(',', map { "['$_->[0]']" } @$ret);
851     print "]\n";
852     exit 0;
853     
854 } elsif ($action eq 'list_job') {
855     # list jobs for a client [[jobid,endtime,'desc'],..]
856
857     print CGI::header('application/x-javascript');
858     
859     my $filter = $bvfs->get_client_filter();
860     my $query = "
861  SELECT Job.JobId,Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus
862   FROM Job JOIN FileSet USING (FileSetId) JOIN Client USING (ClientId) $filter
863  WHERE Client.Name = '$args->{client}'
864    AND Job.Type = 'B'
865    AND JobStatus IN ('f', 'T')
866  ORDER BY EndTime desc";
867     my $result = $bvfs->dbh_selectall_arrayref($query);
868
869     print "[";
870
871     print join(',', map {
872       "[$_->[0], '$_->[1]', '$_->[1] $_->[2] $_->[3] ($_->[4]) $_->[0]']"
873       } @$result);
874
875     print "]\n";
876     exit 0;
877 } elsif ($action eq 'list_storage') { # TODO: use .storage here
878     print CGI::header('application/x-javascript');
879
880     my $q="SELECT Name FROM Storage";
881     my $lst = $bvfs->dbh_selectall_arrayref($q);
882     print "[";
883     print join(',', map { "[ '$_->[0]' ]" } @$lst);
884     print "]\n";
885     exit 0;
886 }
887
888 sub fill_table_for_restore
889 {
890     my (@jobid) = @_;
891
892     # in "force" mode, we need the FileId to compute media list
893     my $FileId = CGI::param('force')?",FileId":"";
894
895     my $fileid = join(',', grep { /^\d+$/ } CGI::param('fileid'));
896     # can get dirid=("10,11", 10, 11)
897     my @dirid = grep { /^\d+$/ } map { split(/,/) } CGI::param('dirid') ;
898     my $inclause = join(',', @jobid);
899
900     my @union;
901
902     if ($fileid) {
903       push @union,
904       "(SELECT JobId, FileIndex, FilenameId, PathId $FileId
905           FROM File WHERE FileId IN ($fileid))";
906     }
907
908     # using this is not good because the sql engine doesn't know
909     # what LIKE will use. It will be better to get Path% in perl
910     # but it doesn't work with accents... :(
911     foreach my $dirid (@dirid) {
912       push @union, "
913   (SELECT File.JobId, File.FileIndex, File.FilenameId, File.PathId $FileId
914     FROM Path JOIN File USING (PathId)
915    WHERE Path.Path LIKE
916         (SELECT ". $bvfs->dbh_strcat('Path',"'\%'") ." FROM Path
917           WHERE PathId = $dirid
918         )
919      AND File.JobId IN ($inclause))";
920     }
921
922     return unless scalar(@union);
923
924     my $u = join(" UNION ", @union);
925
926     $bvfs->dbh_do("CREATE TEMPORARY TABLE btemp AS $u");
927     # TODO: remove FilenameId et PathId
928
929     # now we have to choose the file with the max(jobid)
930     # for each file of btemp
931     if ($bvfs->dbh_is_mysql()) {
932        $bvfs->dbh_do("CREATE TABLE b2$$ AS (
933 SELECT max(JobId) as JobId, FileIndex $FileId
934   FROM btemp
935  GROUP BY PathId, FilenameId
936  HAVING FileIndex > 0
937 )");
938    } else { # postgresql have distinct with more than one criteria
939         $bvfs->dbh_do("CREATE TABLE b2$$ AS (
940 SELECT JobId, FileIndex $FileId
941 FROM (
942  SELECT DISTINCT ON (PathId, FilenameId) JobId, FileIndex $FileId
943    FROM btemp
944   ORDER BY PathId, FilenameId, JobId DESC
945  ) AS T
946  WHERE FileIndex > 0
947 )");
948     }
949
950     return "b2$$";
951 }
952
953 sub get_media_list_with_dir
954 {
955     my ($table) = @_;
956     my $q="
957  SELECT DISTINCT VolumeName, Enabled, InChanger
958    FROM $table,
959     ( -- Get all media from this job
960       SELECT MIN(FirstIndex) AS FirstIndex, MAX(LastIndex) AS LastIndex,
961              VolumeName, Enabled, Inchanger
962         FROM JobMedia JOIN Media USING (MediaId)
963        WHERE JobId IN (SELECT DISTINCT JobId FROM $table)
964        GROUP BY VolumeName,Enabled,InChanger
965     ) AS allmedia
966   WHERE $table.FileIndex >= allmedia.FirstIndex
967     AND $table.FileIndex <= allmedia.LastIndex
968 ";
969     my $lst = $bvfs->dbh_selectall_arrayref($q);
970     return $lst;
971 }
972
973 sub get_media_list
974 {
975     my ($jobid, $fileid) = @_;
976     my $q="
977  SELECT DISTINCT VolumeName, Enabled, InChanger
978    FROM File,
979     ( -- Get all media from this job
980       SELECT MIN(FirstIndex) AS FirstIndex, MAX(LastIndex) AS LastIndex,
981              VolumeName, Enabled, Inchanger
982         FROM JobMedia JOIN Media USING (MediaId)
983        WHERE JobId IN ($jobid)
984        GROUP BY VolumeName,Enabled,InChanger
985     ) AS allmedia
986   WHERE File.FileId IN ($fileid)
987     AND File.FileIndex >= allmedia.FirstIndex
988     AND File.FileIndex <= allmedia.LastIndex
989 ";
990     my $lst = $bvfs->dbh_selectall_arrayref($q);
991     return $lst;
992 }
993
994 # get jobid param and apply user filter
995 my @jobid = $bvfs->get_jobids(grep { /^\d+(,\d+)*$/ } CGI::param('jobid'));
996
997 # get jobid from date arg
998 if (!scalar(@jobid) and $args->{qdate} and $args->{client}) {
999     @jobid = $bvfs->set_job_ids_for_date($args->{client}, $args->{qdate});
1000 }
1001
1002 $bvfs->set_curjobids(@jobid);
1003 $bvfs->set_limits($args->{offset}, $args->{limit});
1004
1005 if (!scalar(@jobid)) {
1006     exit 0;
1007 }
1008
1009 if (CGI::param('init')) { # used when choosing a job
1010     $bvfs->update_brestore_table(@jobid);
1011 }
1012
1013 my $pathid = CGI::param('node') || CGI::param('pathid') || '';
1014 my $path = CGI::param('path');
1015
1016 if ($pathid =~ /^(\d+)$/) {
1017     $pathid = $1;
1018 } elsif ($path) {
1019     $pathid = $bvfs->get_pathid($path);
1020 } else {
1021     $pathid = $bvfs->get_root();
1022 }
1023 $bvfs->ch_dir($pathid);
1024
1025 #print STDERR "pathid=$pathid\n";
1026
1027 # permit to use a regex filter
1028 if ($args->{qpattern}) {
1029     $bvfs->set_pattern($args->{qpattern});
1030 }
1031
1032 if ($action eq 'restore') {
1033
1034     # TODO: pouvoir choisir le replace et le jobname
1035     my $arg = $bvfs->get_form(qw/client storage regexwhere where/);
1036
1037     if (!$arg->{client}) {
1038         print "ERROR: missing client\n";
1039         exit 1;
1040     }
1041
1042     my $table = fill_table_for_restore(@jobid);
1043     if (!$table) {
1044         exit 1;
1045     }
1046
1047     my $bconsole = $bvfs->get_bconsole();
1048     # TODO: pouvoir choisir le replace et le jobname
1049     my $jobid = $bconsole->run(client    => $arg->{client},
1050                                storage   => $arg->{storage},
1051                                where     => $arg->{where},
1052                                regexwhere=> $arg->{regexwhere},
1053                                restore   => 1,
1054                                file      => "?$table");
1055     
1056     $bvfs->dbh_do("DROP TABLE $table");
1057
1058     if (!$jobid) {
1059         print CGI::header('text/html');
1060         $bvfs->display_begin();
1061         $bvfs->error("Can't start your job:<br/>" . $bconsole->before());
1062         $bvfs->display_end();
1063         exit 0;
1064     }
1065     sleep(2);
1066     print CGI::redirect("bweb.pl?action=dsp_cur_job;jobid=$jobid") ;
1067     exit 0;
1068 }
1069
1070 sub escape_quote
1071 {
1072     my ($str) = @_;
1073     if (!$str) {
1074         return '';
1075     }
1076     $str =~ s/'/\\'/g;
1077     return $str;
1078 }
1079
1080 print CGI::header('application/x-javascript');
1081
1082
1083 if ($action eq 'list_files_dirs') {
1084 # fileid, filenameid, pathid, jobid, name, size, mtime
1085     my $jids = join(",", @jobid);
1086
1087     my $files = $bvfs->ls_special_dirs();
1088     # return ($dirid,$dir_basename,$lstat,$jobid)
1089     print "[\n";
1090     print join(',',
1091                map { my @p=Bvfs::parse_lstat($_->[3]); 
1092                      '[' . join(',', 
1093                                 0, # fileid
1094                                 0, # filenameid
1095                                 $_->[0], # pathid
1096                                 "'$jids'", # jobid
1097                                 "'" . escape_quote($_->[1]) . "'", # name
1098                                 "'" . $p[7] . "'",                 # size
1099                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
1100                     ']'; 
1101                } @$files);
1102
1103     print "," if (@$files);
1104
1105     $files = $bvfs->ls_dirs();
1106     # return ($dirid,$dir_basename,$lstat,$jobid)
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])) .  "'") .
1117                     ']'; 
1118                } @$files);
1119
1120     print "," if (@$files);
1121  
1122     $files = $bvfs->ls_files();
1123     print join(',',
1124                map { my @p=Bvfs::parse_lstat($_->[3]); 
1125                      '[' . join(',', 
1126                                 $_->[1],
1127                                 $_->[0],
1128                                 $pathid,
1129                                 $_->[4],
1130                                 "'" . escape_quote($_->[2]) . "'",
1131                                 "'" . $p[7] . "'",
1132                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
1133                     ']'; 
1134                } @$files);
1135     print "]\n";
1136
1137 } elsif ($action eq 'list_files') {
1138     print "[[0,0,0,0,'.',4096,'1970-01-01 00:00:00'],";
1139     my $files = $bvfs->ls_files();
1140 #       [ 1, 2, 3, "Bill",  10, '2007-01-01 00:00:00'],
1141 #   File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
1142
1143     print join(',',
1144                map { my @p=Bvfs::parse_lstat($_->[3]); 
1145                      '[' . join(',', 
1146                                 $_->[1],
1147                                 $_->[0],
1148                                 $pathid,
1149                                 $_->[4],
1150                                 "'" . escape_quote($_->[2]) . "'",
1151                                 "'" . $p[7] . "'",
1152                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
1153                     ']'; 
1154                } @$files);
1155     print "]\n";
1156
1157 } elsif ($action eq 'list_dirs') {
1158
1159     print "[";
1160     my $dirs = $bvfs->ls_dirs();
1161     # return ($dirid,$dir_basename,$lstat,$jobid)
1162
1163     print join(',',
1164                map { "{ 'jobid': '$bvfs->{curjobids}', 'id': '$_->[0]'," . 
1165                         "'text': '" . escape_quote($_->[1]) . "', 'cls':'folder'}" }
1166                @$dirs);
1167     print "]\n";
1168
1169 } elsif ($action eq 'list_versions') {
1170
1171     my $vafv = CGI::param('vafv') || 'false'; # view all file versions
1172     $vafv = ($vafv eq 'false')?0:1;
1173
1174     my $vcopies = CGI::param('vcopies') || 'false'; # view copies file versions
1175     $vcopies = ($vcopies eq 'false')?0:1;
1176
1177     print "[";
1178     #   0       1       2        3   4       5      6           7      8
1179     #($pathid,$fileid,$jobid, $fid, $mtime, $size, $inchanger, $md5, $volname);
1180     my $files = $bvfs->get_all_file_versions($args->{pathid}, $args->{filenameid}, $args->{client}, $vafv, $vcopies);
1181     print join(',',
1182                map { "[ $_->[3], $_->[1], $_->[0], $_->[2], '$_->[8]', $_->[6], '$_->[7]', $_->[5],'" . strftime('%Y-%m-%d %H:%m:%S', localtime($_->[4])) . "']" }
1183                @$files);
1184     print "]\n";
1185
1186 # this action is used when the restore box appear, we can display
1187 # the media list that will be needed for restore
1188 } elsif ($action eq 'get_media') {
1189     my ($jobid, $fileid, $table);
1190     my $lst;
1191
1192     # in this mode, we compute the result to get all needed media
1193 #    print STDERR "force=", CGI::param('force'), "\n";
1194     if (CGI::param('force')) {
1195         $table = fill_table_for_restore(@jobid);
1196         if (!$table) {
1197             exit 1;
1198         }
1199         # mysql is very slow without this index...
1200         if ($bvfs->dbh_is_mysql()) {
1201             $bvfs->dbh_do("CREATE INDEX idx_$table ON $table (JobId)");
1202         }
1203         $lst = get_media_list_with_dir($table);
1204     } else {
1205         $jobid = join(',', @jobid);
1206         $fileid = join(',', grep { /^\d+(,\d+)*$/ } CGI::param('fileid'));
1207         $lst = get_media_list($jobid, $fileid);
1208     }        
1209     
1210     if ($lst) {
1211         print "[";
1212         print join(',', map { "['$_->[0]',$_->[1],$_->[2]]" } @$lst);
1213         print "]\n";
1214     }
1215
1216     if ($table) {
1217         #$bvfs->dbh_do("DROP TABLE $table");
1218     }
1219
1220 }
1221
1222 __END__
1223
1224 CREATE VIEW files AS
1225  SELECT path || name AS name,pathid,filenameid,fileid,jobid
1226    FROM File JOIN FileName USING (FilenameId) JOIN Path USING (PathId);
1227
1228 SELECT 'drop table ' || tablename || ';'
1229     FROM pg_tables WHERE tablename ~ '^b[0-9]';