]> git.sur5r.net Git - bacula/bacula/blob - gui/bweb/cgi/bresto.pl
ebl Update statistics usage
[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 # fill brestore_xxx tables for speedup
129 sub update_cache
130 {
131     my ($self) = @_;
132
133     $self->{dbh}->begin_work();
134
135     # getting all Jobs to "cache"
136     my $query = "
137   SELECT JobId from Job
138    WHERE JobId NOT IN (SELECT JobId FROM brestore_knownjobid) 
139      AND Type IN ('B') AND JobStatus IN ('T', 'f', 'A') 
140    ORDER BY JobId";
141     my $jobs = $self->dbh_selectall_arrayref($query);
142
143     $self->update_brestore_table(map { $_->[0] } @$jobs);
144
145     $self->{dbh}->commit();
146     $self->{dbh}->begin_work(); # we can break here
147
148     print STDERR "Cleaning path visibility\n";
149
150     my $nb = $self->dbh_do("
151   DELETE FROM brestore_pathvisibility
152       WHERE NOT EXISTS
153    (SELECT 1 FROM Job WHERE JobId=brestore_pathvisibility.JobId)");
154
155     print STDERR "$nb rows affected\n";
156     print STDERR "Cleaning known jobid\n";
157
158     $nb = $self->dbh_do("
159   DELETE FROM brestore_knownjobid
160       WHERE NOT EXISTS
161    (SELECT 1 FROM Job WHERE JobId=brestore_knownjobid.JobId)");
162
163     print STDERR "$nb rows affected\n";
164
165     $self->{dbh}->commit();
166 }
167
168 sub update_brestore_table
169 {
170     my ($self, @jobs) = @_;
171
172     $self->debug(\@jobs);
173
174     foreach my $job (sort {$a <=> $b} @jobs)
175     {
176         my $query = "SELECT 1 FROM brestore_knownjobid WHERE JobId = $job";
177         my $retour = $self->dbh_selectrow_arrayref($query);
178         next if ($retour and ($retour->[0] == 1)); # We have allready done this one ...
179
180         print STDERR "Inserting path records for JobId $job\n";
181         $query = "INSERT INTO brestore_pathvisibility (PathId, JobId)
182                    (SELECT DISTINCT PathId, JobId FROM File WHERE JobId = $job)";
183
184         $self->dbh_do($query);
185
186         # Now we have to do the directory recursion stuff to determine missing visibility
187         # We try to avoid recursion, to be as fast as possible
188         # We also only work on not allready hierarchised directories...
189
190         print STDERR "Creating missing recursion paths for $job\n";
191
192         $query = "
193 SELECT brestore_pathvisibility.PathId, Path FROM brestore_pathvisibility
194   JOIN Path ON( brestore_pathvisibility.PathId = Path.PathId)
195        LEFT JOIN brestore_pathhierarchy ON (brestore_pathvisibility.PathId = brestore_pathhierarchy.PathId)
196  WHERE brestore_pathvisibility.JobId = $job
197    AND brestore_pathhierarchy.PathId IS NULL
198  ORDER BY Path";
199
200         my $sth = $self->dbh_prepare($query);
201         $sth->execute();
202         my $pathid; my $path;
203         $sth->bind_columns(\$pathid,\$path);
204
205         while ($sth->fetch)
206         {
207             $self->build_path_hierarchy($path,$pathid);
208         }
209         $sth->finish();
210
211         # Great. We have calculated all dependancies. We can use them to add the missing pathids ...
212         # This query gives all parent pathids for a given jobid that aren't stored.
213         # It has to be called until no record is updated ...
214         $query = "
215 INSERT INTO brestore_pathvisibility (PathId, JobId) (
216  SELECT a.PathId,$job
217    FROM (
218      SELECT DISTINCT h.PPathId AS PathId
219        FROM brestore_pathhierarchy AS h
220        JOIN  brestore_pathvisibility AS p ON (h.PathId=p.PathId)
221       WHERE p.JobId=$job) AS a LEFT JOIN
222        (SELECT PathId
223           FROM brestore_pathvisibility
224          WHERE JobId=$job) AS b ON (a.PathId = b.PathId)
225   WHERE b.PathId IS NULL)";
226
227         my $rows_affected;
228         while (($rows_affected = $self->dbh_do($query)) and ($rows_affected !~ /^0/))
229         {
230             print STDERR "Recursively adding $rows_affected records from $job\n";
231         }
232         # Job's done
233         $query = "INSERT INTO brestore_knownjobid (JobId) VALUES ($job)";
234         $self->dbh_do($query);
235     }
236 }
237
238 # compute the parent directorie
239 sub parent_dir
240 {
241     my ($path) = @_;
242     # Root Unix case :
243     if ($path eq '/')
244     {
245         return '';
246     }
247     # Root Windows case :
248     if ($path =~ /^[a-z]+:\/$/i)
249     {
250         return '';
251     }
252     # Split
253     my @tmp = split('/',$path);
254     # We remove the last ...
255     pop @tmp;
256     my $tmp = join ('/',@tmp) . '/';
257     return $tmp;
258 }
259
260 sub build_path_hierarchy
261 {
262     my ($self, $path,$pathid)=@_;
263     # Does the ppathid exist for this ? we use a memory cache...
264     # In order to avoid the full loop, we consider that if a dir is allready in the
265     # brestore_pathhierarchy table, then there is no need to calculate all the hierarchy
266     while ($path ne '')
267     {
268         if (! $self->{cache_ppathid}->{$pathid})
269         {
270             my $query = "SELECT PPathId FROM brestore_pathhierarchy WHERE PathId = ?";
271             my $sth2 = $self->{dbh}->prepare_cached($query);
272             $sth2->execute($pathid);
273             # Do we have a result ?
274             if (my $refrow = $sth2->fetchrow_arrayref)
275             {
276                 $self->{cache_ppathid}->{$pathid}=$refrow->[0];
277                 $sth2->finish();
278                 # This dir was in the db ...
279                 # It means we can leave, the tree has allready been built for
280                 # this dir
281                 return 1;
282             } else {
283                 $sth2->finish();
284                 # We have to create the record ...
285                 # What's the current p_path ?
286                 my $ppath = parent_dir($path);
287                 my $ppathid = $self->return_pathid_from_path($ppath);
288                 $self->{cache_ppathid}->{$pathid}= $ppathid;
289
290                 $query = "INSERT INTO brestore_pathhierarchy (pathid, ppathid) VALUES (?,?)";
291                 $sth2 = $self->{dbh}->prepare_cached($query);
292                 $sth2->execute($pathid,$ppathid);
293                 $sth2->finish();
294                 $path = $ppath;
295                 $pathid = $ppathid;
296             }
297         } else {
298            # It's allready in the cache.
299            # We can leave, no time to waste here, all the parent dirs have allready
300            # been done
301            return 1;
302         }
303     }
304     return 1;
305 }
306
307 sub return_pathid_from_path
308 {
309     my ($self, $path) = @_;
310     my $query = "SELECT PathId FROM Path WHERE Path = ?";
311
312     #print STDERR $query,"\n" if $debug;
313     my $sth = $self->{dbh}->prepare_cached($query);
314     $sth->execute($path);
315     my $result =$sth->fetchrow_arrayref();
316     $sth->finish();
317     if (defined $result)
318     {
319         return $result->[0];
320
321     } else {
322         # A bit dirty : we insert into path, and we have to be sure
323         # we aren't deleted by a purge. We still need to insert into path to get
324         # the pathid, because of mysql
325         $query = "INSERT INTO Path (Path) VALUES (?)";
326         #print STDERR $query,"\n" if $debug;
327         $sth = $self->{dbh}->prepare_cached($query);
328         $sth->execute($path);
329         $sth->finish();
330
331         $query = "SELECT PathId FROM Path WHERE Path = ?";
332         #print STDERR $query,"\n" if $debug;
333         $sth = $self->{dbh}->prepare_cached($query);
334         $sth->execute($path);
335         $result = $sth->fetchrow_arrayref();
336         $sth->finish();
337         return $result->[0];
338     }
339 }
340
341 # list all files in a directory, accross curjobids
342 sub ls_files
343 {
344     my ($self) = @_;
345
346     return undef unless ($self->{curjobids});
347
348     my $inclause   = $self->{curjobids};
349     my $inpath = $self->{cwdid};
350
351     my $query =
352 "SELECT File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
353  FROM File, (
354        SELECT Filename.Name, max(File.FileId) as id
355          FROM File, Filename
356         WHERE File.FilenameId = Filename.FilenameId
357           AND Filename.Name != ''
358           AND File.PathId = $inpath
359           AND File.JobId IN ($inclause)
360         GROUP BY Filename.Name
361         ORDER BY Filename.Name) AS listfiles
362 WHERE File.FileId = listfiles.id";
363
364     $self->debug($query);
365     my $result = $self->dbh_selectall_arrayref($query);
366     $self->debug($result);
367
368     return $result;
369 }
370
371 # list all directories in a directory, accross curjobids
372 # return ($dirid,$dir_basename,$lstat,$jobid)
373 sub ls_dirs
374 {
375     my ($self) = @_;
376
377     return undef unless ($self->{curjobids});
378
379     my $pathid = $self->{cwdid};
380     my $jobclause = $self->{curjobids};
381
382     # Let's retrieve the list of the visible dirs in this dir ...
383     # First, I need the empty filenameid to locate efficiently
384     # the dirs in the file table
385     my $query = "SELECT FilenameId FROM Filename WHERE Name = ''";
386     my $sth = $self->dbh_prepare($query);
387     $sth->execute();
388     my $result = $sth->fetchrow_arrayref();
389     $sth->finish();
390     my $dir_filenameid = $result->[0];
391
392     # Then we get all the dir entries from File ...
393     $query = "
394 SELECT PathId, Path, JobId, Lstat FROM (
395
396     SELECT Path1.PathId, Path1.Path, lower(Path1.Path),
397            listfile1.JobId, listfile1.Lstat
398     FROM (
399        SELECT DISTINCT brestore_pathhierarchy1.PathId
400        FROM brestore_pathhierarchy AS brestore_pathhierarchy1
401        JOIN Path AS Path2
402            ON (brestore_pathhierarchy1.PathId = Path2.PathId)
403        JOIN brestore_pathvisibility AS brestore_pathvisibility1
404            ON (brestore_pathhierarchy1.PathId = brestore_pathvisibility1.PathId)
405        WHERE brestore_pathhierarchy1.PPathId = $pathid
406        AND brestore_pathvisibility1.jobid IN ($jobclause)) AS listpath1
407    JOIN Path AS Path1 ON (listpath1.PathId = Path1.PathId)
408    LEFT JOIN (
409        SELECT File1.PathId, File1.JobId, File1.Lstat FROM File AS File1
410        WHERE File1.FilenameId = $dir_filenameid
411        AND File1.JobId IN ($jobclause)) AS listfile1
412        ON (listpath1.PathId = listfile1.PathId)
413      ) AS A ORDER BY 2,3 DESC
414 ";
415     $self->debug($query);
416     $sth=$self->dbh_prepare($query);
417     $sth->execute();
418     $result = $sth->fetchall_arrayref();
419     my @return_list;
420     my $prev_dir='';
421     foreach my $refrow (@{$result})
422     {
423         my $dirid = $refrow->[0];
424         my $dir = $refrow->[1];
425         my $lstat = $refrow->[3];
426         my $jobid = $refrow->[2] || 0;
427         next if ($dirid eq $prev_dir);
428         # We have to clean up this dirname ... we only want it's 'basename'
429         my $return_value;
430         if ($dir ne '/')
431         {
432             my @temp = split ('/',$dir);
433             $return_value = pop @temp;
434         }
435         else
436         {
437             $return_value = '/';
438         }
439         my @return_array = ($dirid,$return_value,$lstat,$jobid);
440         push @return_list,(\@return_array);
441         $prev_dir = $dirid;
442     }
443     $self->debug(\@return_list);
444     return \@return_list;
445 }
446
447 # TODO : we want be able to restore files from a bad ended backup
448 # we have JobStatus IN ('T', 'A', 'E') and we must
449
450 # Data acces subs from here. Interaction with SGBD and caching
451
452 # This sub retrieves the list of jobs corresponding to the jobs selected in the
453 # GUI and stores them in @CurrentJobIds.
454 # date must be quoted
455 sub set_job_ids_for_date
456 {
457     my ($self, $client, $date)=@_;
458
459     if (!$client or !$date) {
460         return ();
461     }
462     my $filter = $self->get_client_filter();
463     # The algorithm : for a client, we get all the backups for each
464     # fileset, in reverse order Then, for each fileset, we store the 'good'
465     # incrementals and differentials until we have found a full so it goes
466     # like this : store all incrementals until we have found a differential
467     # or a full, then find the full
468     my $query = "
469 SELECT JobId, FileSet, Level, JobStatus
470   FROM Job 
471        JOIN FileSet USING (FileSetId)
472        JOIN Client USING (ClientId) $filter
473  WHERE EndTime <= $date
474    AND Client.Name = '$client'
475    AND Type IN ('B')
476    AND JobStatus IN ('T')
477  ORDER BY FileSet, JobTDate DESC";
478
479     my @CurrentJobIds;
480     my $result = $self->dbh_selectall_arrayref($query);
481     my %progress;
482     foreach my $refrow (@$result)
483     {
484         my $jobid = $refrow->[0];
485         my $fileset = $refrow->[1];
486         my $level = $refrow->[2];
487
488         defined $progress{$fileset} or $progress{$fileset}='U'; # U for unknown
489
490         next if $progress{$fileset} eq 'F'; # It's over for this fileset...
491
492         if ($level eq 'I')
493         {
494             next unless ($progress{$fileset} eq 'U' or $progress{$fileset} eq 'I');
495             push @CurrentJobIds,($jobid);
496         }
497         elsif ($level eq 'D')
498         {
499             next if $progress{$fileset} eq 'D'; # We allready have a differential
500             push @CurrentJobIds,($jobid);
501         }
502         elsif ($level eq 'F')
503         {
504             push @CurrentJobIds,($jobid);
505         }
506
507         my $status = $refrow->[3] ;
508         if ($status eq 'T') {              # good end of job
509             $progress{$fileset} = $level;
510         }
511     }
512
513     return @CurrentJobIds;
514 }
515
516 sub dbh_selectrow_arrayref
517 {
518     my ($self, $query) = @_;
519     $self->debug($query, up => 1);
520     return $self->{dbh}->selectrow_arrayref($query);
521 }
522
523 # Returns list of versions of a file that could be restored
524 # returns an array of
525 # (jobid,fileindex,mtime,size,inchanger,md5,volname,fileid)
526 # there will be only one jobid in the array of jobids...
527 sub get_all_file_versions
528 {
529     my ($self,$pathid,$fileid,$client,$see_all)=@_;
530
531     defined $see_all or $see_all=0;
532
533     my @versions;
534     my $query;
535     $query =
536 "SELECT File.JobId, File.FileId, File.Lstat,
537         File.Md5, Media.VolumeName, Media.InChanger
538  FROM File, Job, Client, JobMedia, Media
539  WHERE File.FilenameId = $fileid
540    AND File.PathId=$pathid
541    AND File.JobId = Job.JobId
542    AND Job.ClientId = Client.ClientId
543    AND Job.JobId = JobMedia.JobId
544    AND File.FileIndex >= JobMedia.FirstIndex
545    AND File.FileIndex <= JobMedia.LastIndex
546    AND JobMedia.MediaId = Media.MediaId
547    AND Client.Name = '$client'";
548
549     $self->debug($query);
550     my $result = $self->dbh_selectall_arrayref($query);
551
552     foreach my $refrow (@$result)
553     {
554         my ($jobid, $fid, $lstat, $md5, $volname, $inchanger) = @$refrow;
555         my @attribs = parse_lstat($lstat);
556         my $mtime = array_attrib('st_mtime',\@attribs);
557         my $size = array_attrib('st_size',\@attribs);
558
559         my @list = ($pathid,$fileid,$jobid,
560                     $fid, $mtime, $size, $inchanger,
561                     $md5, $volname);
562         push @versions, (\@list);
563     }
564
565     # We have the list of all versions of this file.
566     # We'll sort it by mtime desc, size, md5, inchanger desc
567     # the rest of the algorithm will be simpler
568     # ('FILE:',filename,jobid,fileindex,mtime,size,inchanger,md5,volname)
569     @versions = sort { $b->[4] <=> $a->[4]
570                     || $a->[5] <=> $b->[5]
571                     || $a->[7] cmp $a->[7]
572                     || $b->[6] <=> $a->[6]} @versions;
573
574     my @good_versions;
575     my %allready_seen_by_mtime;
576     my %allready_seen_by_md5;
577     # Now we should create a new array with only the interesting records
578     foreach my $ref (@versions)
579     {
580         if ($ref->[7])
581         {
582             # The file has a md5. We compare his md5 to other known md5...
583             # We take size into account. It may happen that 2 files
584             # have the same md5sum and are different. size is a supplementary
585             # criterion
586
587             # If we allready have a (better) version
588             next if ( (not $see_all)
589                       and $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]});
590
591             # we never met this one before...
592             $allready_seen_by_md5{$ref->[7] .'-'. $ref->[5]}=1;
593         }
594         # Even if it has a md5, we should also work with mtimes
595         # We allready have a (better) version
596         next if ( (not $see_all)
597                   and $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5]});
598         $allready_seen_by_mtime{$ref->[4] .'-'. $ref->[5] . '-' . $ref->[7]}=1;
599
600         # We reached there. The file hasn't been seen.
601         push @good_versions,($ref);
602     }
603
604     # To be nice with the user, we re-sort good_versions by
605     # inchanger desc, mtime desc
606     @good_versions = sort { $b->[4] <=> $a->[4]
607                          || $b->[2] <=> $a->[2]} @good_versions;
608
609     return \@good_versions;
610 }
611 {
612     my %attrib_name_id = ( 'st_dev' => 0,'st_ino' => 1,'st_mode' => 2,
613                           'st_nlink' => 3,'st_uid' => 4,'st_gid' => 5,
614                           'st_rdev' => 6,'st_size' => 7,'st_blksize' => 8,
615                           'st_blocks' => 9,'st_atime' => 10,'st_mtime' => 11,
616                           'st_ctime' => 12,'LinkFI' => 13,'st_flags' => 14,
617                           'data_stream' => 15);;
618     sub array_attrib
619     {
620         my ($attrib,$ref_attrib)=@_;
621         return $ref_attrib->[$attrib_name_id{$attrib}];
622     }
623
624     sub file_attrib
625     {   # $file = [filenameid,listfiles.id,listfiles.Name, File.LStat, File.JobId]
626
627         my ($file, $attrib)=@_;
628
629         if (defined $attrib_name_id{$attrib}) {
630
631             my @d = split(' ', $file->[3]) ; # TODO : cache this
632
633             return from_base64($d[$attrib_name_id{$attrib}]);
634
635         } elsif ($attrib eq 'jobid') {
636
637             return $file->[4];
638
639         } elsif ($attrib eq 'name') {
640
641             return $file->[2];
642
643         } else  {
644             die "Attribute not known : $attrib.\n";
645         }
646     }
647
648     sub lstat_attrib
649     {
650         my ($lstat,$attrib)=@_;
651         if ($lstat and defined $attrib_name_id{$attrib})
652         {
653             my @d = split(' ', $lstat) ; # TODO : cache this
654             return from_base64($d[$attrib_name_id{$attrib}]);
655         }
656         return 0;
657     }
658 }
659
660 {
661     # Base 64 functions, directly from recover.pl.
662     # Thanks to
663     # Karl Hakimian <hakimian@aha.com>
664     # This section is also under GPL v2 or later.
665     my @base64_digits;
666     my @base64_map;
667     my $is_init=0;
668     sub init_base64
669     {
670         @base64_digits = (
671         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
672         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
673         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
674         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
675         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
676                           );
677         @base64_map = (0) x 128;
678
679         for (my $i=0; $i<64; $i++) {
680             $base64_map[ord($base64_digits[$i])] = $i;
681         }
682         $is_init = 1;
683     }
684
685     sub from_base64 {
686         if(not $is_init)
687         {
688             init_base64();
689         }
690         my $where = shift;
691         my $val = 0;
692         my $i = 0;
693         my $neg = 0;
694
695         if (substr($where, 0, 1) eq '-') {
696             $neg = 1;
697             $where = substr($where, 1);
698         }
699
700         while ($where ne '') {
701             $val *= 64;
702             my $d = substr($where, 0, 1);
703             $val += $base64_map[ord(substr($where, 0, 1))];
704             $where = substr($where, 1);
705         }
706
707         return $val;
708     }
709
710     sub parse_lstat {
711         my ($lstat)=@_;
712         my @attribs = split(' ',$lstat);
713         foreach my $element (@attribs)
714         {
715             $element = from_base64($element);
716         }
717         return @attribs;
718     }
719 }
720
721 # get jobids that the current user can view (ACL)
722 sub get_jobids
723 {
724   my ($self, @jobid) = @_;
725   my $filter = $self->get_client_filter();
726   if ($filter) {
727     my $jobids = $self->dbh_join(@jobid);
728     my $q="
729 SELECT JobId 
730   FROM Job JOIN Client USING (ClientId) $filter 
731  WHERE Jobid IN ($jobids)";
732     my $res = $self->dbh_selectall_arrayref($q);
733     @jobid = map { $_->[0] } @$res;
734   }
735   return @jobid;
736 }
737
738 ################################################################
739
740
741 package main;
742 use strict;
743 use POSIX qw/strftime/;
744 use Bweb;
745
746 my $conf = new Bweb::Config(config_file => $Bweb::config_file);
747 $conf->load();
748
749 my $bvfs = new Bvfs(info => $conf);
750 $bvfs->connect_db();
751
752 my $action = CGI::param('action') || '';
753
754 my $args = $bvfs->get_form('pathid', 'filenameid', 'fileid', 'qdate',
755                            'limit', 'offset', 'client');
756
757 if ($action eq 'batch') {
758     $bvfs->update_cache();
759     exit 0;
760 }
761
762 # All these functions are returning JSON compatible data
763 # for javascript parsing
764
765 if ($action eq 'list_client') { # list all client [ ['c1'],['c2']..]
766     print CGI::header('application/x-javascript');
767
768     my $filter = $bvfs->get_client_filter();
769     my $q = "SELECT Name FROM Client $filter";
770     my $ret = $bvfs->dbh_selectall_arrayref($q);
771
772     print "[";
773     print join(',', map { "['$_->[0]']" } @$ret);
774     print "]\n";
775     exit 0;
776     
777 } elsif ($action eq 'list_job') { # list jobs for a client [[jobid,endtime,'desc'],..]
778     print CGI::header('application/x-javascript');
779     
780     my $filter = $bvfs->get_client_filter();
781     my $query = "
782  SELECT Job.JobId,Job.EndTime, FileSet.FileSet, Job.Level, Job.JobStatus
783   FROM Job JOIN FileSet USING (FileSetId) JOIN Client USING (ClientId) $filter
784  WHERE Client.Name = '$args->{client}'
785    AND Job.Type = 'B'
786    AND JobStatus IN ('f', 'T')
787  ORDER BY EndTime desc";
788     my $result = $bvfs->dbh_selectall_arrayref($query);
789
790     print "[";
791
792     print join(',', map {
793       "[$_->[0], '$_->[1]', '$_->[1] $_->[2] $_->[3] ($_->[4]) $_->[0]']"
794       } @$result);
795
796     print "]\n";
797     exit 0;
798 } elsif ($action eq 'list_storage') { # TODO: use .storage hier
799     print CGI::header('application/x-javascript');
800
801     my $q="SELECT Name FROM Storage";
802     my $lst = $bvfs->dbh_selectall_arrayref($q);
803     print "[";
804     print join(',', map { "[ '$_->[0]' ]" } @$lst);
805     print "]\n";
806     exit 0;
807 }
808
809 # get jobid param and apply user filter
810 my @jobid = $bvfs->get_jobids(grep { /^\d+(,\d+)*$/ } CGI::param('jobid'));
811 # get jobid from date arg
812 if (!scalar(@jobid) and $args->{qdate} and $args->{client}) {
813     @jobid = $bvfs->set_job_ids_for_date($args->{client}, $args->{qdate});
814 }
815 $bvfs->set_curjobids(@jobid);
816 $bvfs->set_limits($args->{limit}, $args->{offset});
817
818 if (!scalar(@jobid)) {
819     exit 0;
820 }
821
822 if (CGI::param('init')) { # used when choosing a job
823     $bvfs->update_brestore_table(@jobid);
824 }
825
826 my $pathid = CGI::param('node') || '';
827 my $path = CGI::param('path');
828
829 if ($pathid =~ /^(\d+)$/) {
830     $pathid = $1;
831 } elsif ($path) {
832     $pathid = $bvfs->get_pathid($path);
833 } else {
834     $pathid = $bvfs->get_root();
835 }
836 $bvfs->ch_dir($pathid);
837
838 if ($action eq 'restore') {
839
840     # TODO: pouvoir choisir le replace et le jobname
841     my $arg = $bvfs->get_form(qw/client storage regexwhere where/);
842
843     if (!$arg->{client}) {
844         print "ERROR: missing client\n";
845         exit 1;
846     }
847
848     my $fileid = join(',', grep { /^\d+$/ } CGI::param('fileid'));
849     my @dirid = grep { /^\d+$/ } CGI::param('dirid');
850     my $inclause = join(',', @jobid);
851
852     my @union;
853
854     if ($fileid) {
855       push @union,
856       "(SELECT JobId, FileIndex, FilenameId, PathId FROM File WHERE FileId IN ($fileid))";
857     }
858
859     # using this is not good because the sql engine doesn't know
860     # what LIKE will use. It will be better to get Path% in perl
861     # but it doesn't work with accents... :(
862     foreach my $dirid (@dirid) {
863       push @union, "
864   (SELECT File.JobId, File.FileIndex, File.FilenameId, File.PathId
865     FROM Path JOIN File USING (PathId)
866    WHERE Path.Path LIKE
867         (SELECT ". $bvfs->dbh_strcat('Path',"'\%'") ." FROM Path
868           WHERE PathId = $dirid
869         )
870      AND File.JobId IN ($inclause))";
871     }
872
873     return unless scalar(@union);
874
875     my $u = join(" UNION ", @union);
876
877     $bvfs->dbh_do("CREATE TEMPORARY TABLE btemp AS $u");
878     # TODO: remove FilenameId et PathId
879
880     # now we have to choose the file with the max(jobid)
881     # for each file of btemp
882     if ($bvfs->dbh_is_mysql()) {
883        $bvfs->dbh_do("CREATE TEMPORARY TABLE btemp2 AS (
884 SELECT max(JobId) as JobId, PathId, FilenameId
885   FROM btemp
886  GROUP BY PathId, FilenameId
887  HAVING FileIndex > 0
888 )");
889        $bvfs->dbh_do("CREATE TABLE b2$$ AS (
890 SELECT btemp.JobId, btemp.FileIndex, btemp.FilenameId, btemp.PathId
891   FROM btemp, btemp2
892   WHERE btemp2.JobId = btemp.JobId
893     AND btemp2.PathId= btemp.PathId
894     AND btemp2.FilenameId = btemp.FilenameId
895 )");
896    } else { # postgresql have distinct with more than one criteria...
897         $bvfs->dbh_do("CREATE TABLE b2$$ AS (
898 SELECT JobId, FileIndex
899 FROM (
900  SELECT DISTINCT ON (PathId, FilenameId) JobId, FileIndex
901    FROM btemp
902   ORDER BY PathId, FilenameId, JobId DESC
903  ) AS T
904  WHERE FileIndex > 0
905 )");
906     }
907
908     my $bconsole = $bvfs->get_bconsole();
909     # TODO: pouvoir choisir le replace et le jobname
910     my $jobid = $bconsole->run(client    => $arg->{client},
911                                storage   => $arg->{storage},
912                                where     => $arg->{where},
913                                regexwhere=> $arg->{regexwhere},
914                                restore   => 1,
915                                file      => "?b2$$");
916     
917     $bvfs->dbh_do("DROP TABLE b2$$");
918
919     if (!$jobid) {
920         print CGI::header('text/html');
921         $bvfs->display_begin();
922         $bvfs->error("Can't start your job:<br/>" . $bconsole->before());
923         $bvfs->display_end();
924         exit 0;
925     }
926     sleep(2);
927     print CGI::redirect("bweb.pl?action=dsp_cur_job;jobid=$jobid") ;
928     exit 0;
929 }
930
931 sub escape_quote
932 {
933     my ($str) = @_;
934     $str =~ s/'/\\'/g;
935     return $str;
936 }
937
938 print CGI::header('application/x-javascript');
939
940 if ($action eq 'list_files') {
941     print "[[0,0,0,0,'.',4096,'1970-01-01 00:00:00'],";
942     my $files = $bvfs->ls_files();
943 #       [ 1, 2, 3, "Bill",  10, '2007-01-01 00:00:00'],
944 #   File.FilenameId, listfiles.id, listfiles.Name, File.LStat, File.JobId
945
946     print join(',',
947                map { my @p=Bvfs::parse_lstat($_->[3]); 
948                      '[' . join(',', 
949                                 $_->[1],
950                                 $_->[0],
951                                 $pathid,
952                                 $_->[4],
953                                 "'" . escape_quote($_->[2]) . "'",
954                                 "'" . $p[7] . "'",
955                                 "'" . strftime('%Y-%m-%d %H:%m:%S', localtime($p[11])) .  "'") .
956                     ']'; 
957                } @$files);
958     print "]\n";
959
960 } elsif ($action eq 'list_dirs') {
961
962     print "[";
963     my $dirs = $bvfs->ls_dirs();
964     # return ($dirid,$dir_basename,$lstat,$jobid)
965
966     print join(',',
967                map { "{ 'jobid': '$bvfs->{curjobids}', 'id': '$_->[0]'," . 
968                         "'text': '" . escape_quote($_->[1]) . "', 'cls':'folder'}" }
969                @$dirs);
970     print "]\n";
971
972 } elsif ($action eq 'list_versions') {
973
974     my $vafv = CGI::param('vafv') || 'false'; # view all file versions
975     $vafv = ($vafv eq 'false')?0:1;
976
977     print "[";
978     #   0       1       2        3   4       5      6           7      8
979     #($pathid,$fileid,$jobid, $fid, $mtime, $size, $inchanger, $md5, $volname);
980     my $files = $bvfs->get_all_file_versions($args->{pathid}, $args->{filenameid}, $args->{client}, $vafv);
981     print join(',',
982                map { "[ $_->[3], $_->[1], $_->[0], $_->[2], '$_->[8]', $_->[6], '$_->[7]', $_->[5],'" . strftime('%Y-%m-%d %H:%m:%S', localtime($_->[4])) . "']" }
983                @$files);
984     print "]\n";
985
986 } elsif ($action eq 'get_media') {
987
988     my $jobid = join(',', @jobid);
989     my $fileid = join(',', grep { /^\d+(,\d+)*$/ } CGI::param('fileid'));
990
991     my $q="
992  SELECT DISTINCT VolumeName, Enabled, InChanger
993    FROM File,
994     ( -- Get all media from this job
995       SELECT MIN(FirstIndex) AS FirstIndex, MAX(LastIndex) AS LastIndex,
996              VolumeName, Enabled, Inchanger
997         FROM JobMedia JOIN Media USING (MediaId)
998        WHERE JobId IN ($jobid)
999        GROUP BY VolumeName,Enabled,InChanger
1000     ) AS allmedia
1001   WHERE File.FileId IN ($fileid)
1002     AND File.FileIndex >= allmedia.FirstIndex
1003     AND File.FileIndex <= allmedia.LastIndex
1004 ";
1005     my $lst = $bvfs->dbh_selectall_arrayref($q);
1006     print "[";
1007     print join(',', map { "['$_->[0]',$_->[1],$_->[2]]" } @$lst);
1008     print "]\n";
1009
1010 }
1011
1012 __END__
1013
1014 CREATE VIEW files AS
1015  SELECT path || name AS name,pathid,filenameid,fileid,jobid
1016    FROM File JOIN FileName USING (FilenameId) JOIN Path USING (PathId);
1017
1018 SELECT 'drop table ' || tablename || ';'
1019     FROM pg_tables WHERE tablename ~ '^b[0-9]';