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