/[BackupPC]/trunk/lib/BackupPC/SearchLib.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Diff of /trunk/lib/BackupPC/SearchLib.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 9 by dpavlin, Thu Jun 23 12:36:22 2005 UTC revision 211 by dpavlin, Sun Oct 16 10:57:55 2005 UTC
# Line 4  package BackupPC::SearchLib; Line 4  package BackupPC::SearchLib;
4  use strict;  use strict;
5  use BackupPC::CGI::Lib qw(:all);  use BackupPC::CGI::Lib qw(:all);
6  use BackupPC::Attrib qw(:all);  use BackupPC::Attrib qw(:all);
 use Data::Dumper;  
7  use DBI;  use DBI;
8    use DateTime;
9    use vars qw(%In $MyURL);
10    use Time::HiRes qw/time/;
11    use XML::Writer;
12    use IO::File;
13    
14    my $on_page = 100;
15    my $pager_pages = 10;
16    
17    my $dsn = $Conf{SearchDSN};
18    my $db_user = $Conf{SearchUser} || '';
19    
20    my $hest_index_path = $Conf{HyperEstraierIndex};
21    
22    my $dbh;
23    
24    sub get_dbh {
25            $dbh ||= DBI->connect($dsn, $db_user, "", { RaiseError => 1, AutoCommit => 1 } );
26            return $dbh;
27    }
28    
29  sub getUnits() {  sub getUnits() {
30      my @ret = ();          my @ret;
31      my $tmp;  
32      my $dbh = DBI->connect( "dbi:SQLite:dbname=${TopDir}/$Conf{SearchDB}",          my $dbh = get_dbh();
33          "", "", { RaiseError => 1, AutoCommit => 1 } );          my $sth = $dbh->prepare(qq{
34      my $st =                  SELECT
35        $dbh->prepare(                          shares.id       as id,
36          " SELECT shares.ID AS ID, shares.share AS name FROM shares;");                          hosts.name || ':' || shares.name as share
37      $st->execute();                  FROM shares
38      push (@ret, { 'ID' => '', 'name' => '-'});                  JOIN hosts on hostid = hosts.id
39      while ( $tmp = $st->fetchrow_hashref() ) {                  ORDER BY share
40          push( @ret, { 'ID' => $tmp->{'ID'}, 'name' => $tmp->{'name'} } );          } );
41      }          $sth->execute();
42      $dbh->disconnect();          push @ret, { 'id' => '', 'share' => '-'};       # dummy any
43      return @ret;  
44            while ( my $row = $sth->fetchrow_hashref() ) {
45                    push @ret, $row;
46            }
47            return @ret;
48    }
49    
50    sub epoch_to_iso {
51            my $t = shift || return;
52            my $iso = BackupPC::Lib::timeStamp(undef, $t);
53            $iso =~ s/\s/ /g;
54            return $iso;
55    }
56    
57    sub dates_from_form($) {
58            my $param = shift || return;
59    
60            sub mk_epoch_date($$) {
61                    my ($name,$suffix) = @_;
62    
63                    my $yyyy = $param->{ $name . '_year_' . $suffix} || return undef;
64                    my $mm .= $param->{ $name . '_month_' . $suffix} ||
65                            ( $suffix eq 'from' ? 1 : 12);
66                    my $dd .= $param->{ $name . '_day_' . $suffix} ||
67                            ( $suffix eq 'from' ? 1 : 31);
68    
69                    $yyyy =~ s/\D//g;
70                    $mm =~ s/\D//g;
71                    $dd =~ s/\D//g;
72    
73                    my $h = my $m = my $s = 0;
74                    if ($suffix eq 'to') {
75                            $h = 23;
76                            $m = 59;
77                            $s = 59;
78                    }
79    
80                    my $dt = new DateTime(
81                            year => $yyyy,
82                            month => $mm,
83                            day => $dd,
84                            hour => $h,
85                            minute => $m,
86                            second => $s,
87                    );
88                    print STDERR "mk_epoch_date($name,$suffix) [$yyyy-$mm-$dd] = " . $dt->ymd . " " . $dt->hms . "\n";
89                    return $dt->epoch || 'NULL';
90            }
91    
92            my @ret = (
93                    mk_epoch_date('search_backup', 'from'),
94                    mk_epoch_date('search_backup', 'to'),
95                    mk_epoch_date('search', 'from'),
96                    mk_epoch_date('search', 'to'),
97            );
98    
99            return @ret;
100    
101  }  }
102    
103    
104  sub getWhere($) {  sub getWhere($) {
105      my ($param)    = @_;          my $param = shift || return;
106      my $retSQL     = "";  
107      my @conditions = ();          my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
108      my $cond;  
109            my @conditions;
110                push @conditions, qq{ backups.date >= $backup_from } if ($backup_from);
111              push @conditions, qq{ backups.date <= $backup_to } if ($backup_to);
112                push @conditions, qq{ files.date >= $files_from } if ($files_from);
113      if ( defined( $param->{'search_backup_day_from'} ) && $param->{'search_backup_day_from'} ne "") {          push @conditions, qq{ files.date <= $files_to } if ($files_to);
114          push( @conditions,  
115              ' strftime("%d", datetime(backups.date, "unixepoch","localtime")) >= "'          print STDERR "backup: $backup_from - $backup_to files: $files_from - $files_to cond:" . join(" and ",@conditions);
116                . $param->{'search_backup_day_from'} ."\"");  
117      }          push( @conditions, ' files.shareid = ' . $param->{'search_share'} ) if ($param->{'search_share'});
118      if ( defined( $param->{'search_backup_day_to'} ) && $param->{'search_backup_day_to'} ne "") {          push (@conditions, " upper(files.path) LIKE upper('%".$param->{'search_filename'}."%')") if ($param->{'search_filename'});
119          push( @conditions,  
120              ' strftime("%d", datetime(backups.date, "unixepoch","localtime")) <= "'          return join(" and ", @conditions);
121                . $param->{'search_backup_day_from'}  ."\"");  }
122      }  
123      if ( defined( $param->{'search_backup_month_from'} ) && $param->{'search_backup_month_from'} ne "") {  my $sort_def = {
124          push( @conditions,          default => 'date_a',
125              ' strftime("%m", datetime(backups.date, "unixepoch","localtime")) >= "'          sql => {
126                . $param->{'search_backup_month_from'}  ."\"");                  share_d => 'shares.name DESC',
127      }                  share_a => 'shares.name ASC',
128      if ( defined( $param->{'search_backup_month_to'} ) && $param->{'search_backup_month_to'} ne "") {                  path_d => 'files.path DESC',
129          push( @conditions,                  path_a => 'files.path ASC',
130              ' strftime("%m", datetime(backups.date, "unixepoch","localtime")) <= "'                  num_d => 'files.backupnum DESC',
131                . $param->{'search_backup_month_to'}  ."\"");                  num_a => 'files.backupnum ASC',
132      }                  size_d => 'files.size DESC',
133      if ( defined( $param->{'search_backup_year_from'} ) && $param->{'search_backup_year_from'} ne "") {                  size_a => 'files.size ASC',
134          push( @conditions,                  date_d => 'files.date DESC',
135              ' strftime("%Y", datetime(backups.date, "unixepoch","localtime")) >= "'                  date_a => 'files.date ASC',
136                . $param->{'search_backup_year_from'}  ."\"");          },
137      }          est => {
138      if ( defined( $param->{'search_backup_year_to'} ) && $param->{'search_backup_year_to'} ne "") {                  share_d => 'sname STRD',
139          push( @conditions,                  share_a => 'sname STRA',
140              ' strftime("%Y", datetime(backups.date, "unixepoch","localtime")) <= "'                  path_d => 'filepath STRD',
141                . $param->{'search_backup_year_to'}  ."\"");                  path_a => 'filepath STRA',
142      }                  num_d => 'backupnum NUMD',
143                    num_a => 'backupnum NUMA',
144      if ( defined( $param->{'search_day_from'} )   && $param->{'search_day_from'} ne "" ) {                  size_d => 'size NUMD',
145          push( @conditions,                  size_a => 'size NUMA',
146              ' strftime("%d", datetime(files.date, "unixepoch","localtime")) >= "'                  date_d => 'date NUMD',
147                . $param->{'search_day_from'}  ."\"");                  date_a => 'date NUMA',
148      }          }
149      if ( defined( $param->{'search_month_from'} ) && $param->{'search_month_from'} ne "") {  };
150          push( @conditions,  
151              ' strftime("%m", datetime(files.date, "unixepoch","localtime")) >= "'  sub getSort($$) {
152                . $param->{'search_month_from'}  ."\"");          my ($type, $sort_order) = @_;
153      }  
154      if ( defined( $param->{'search_year_from'} ) && $param->{'search_year_from'} ne "") {          $sort_order ||= $sort_def->{'default'};
155          push( @conditions,  
156              ' strftime("%Y", datetime(files.date, "unixepoch","localtime")) >= "'          die "unknown type: $type" unless ($sort_def->{$type});
157                . $param->{'search_year_from'}  ."\"");  
158      }          if (my $ret = $sort_def->{$type}->{$sort_order}) {
159      if ( defined( $param->{'search_day_to'} )   && $param->{'search_day_to'} ne "" ) {                  return $ret;
160          push( @conditions,          } else {
161              ' strftime("%d", datetime(files.date, "unixepoch","localtime")) <= "'                  # fallback to default sort order
162                . $param->{'search_day_to'}  ."\"");                  return $sort_def->{$type}->{ $sort_def->{'default'} };
163      }          }
164      if ( defined( $param->{'search_month_to'} ) && $param->{'search_month_to'} ne "" ) {  }
165          push( @conditions,  
166              ' strftime("%m", datetime(files.date, "unixepoch","localtime")) <= "'  sub getFiles($) {
167                . $param->{'search_month_to'} ."\"" );          my ($param) = @_;
168      }  
169      if ( defined( $param->{'search_year_to'} )&& $param->{'search_year_to'} ne "" )  {          my $offset = $param->{'offset'} || 0;
170          push( @conditions,          $offset *= $on_page;
171              ' strftime("%Y", datetime(files.date, "unixepoch","localtime")) <= "'  
172                . $param->{'search_year_to'} ."\"");          my $dbh = get_dbh();
173      }  
174            my $sql_cols = qq{
175      if ( defined( $param->{'search_host'} ) && $param->{'search_host'} ne "") {                  files.id                        AS fid,
176        push( @conditions, ' backups.hostID = ' . $param->{'search_host'} );                  hosts.name                      AS hname,
177      }                  shares.name                     AS sname,
178                    files.backupnum                 AS backupnum,
179      if ( defined ($param->{'search_filename'}) && $param->{'search_filename'} ne "") {                  files.path                      AS filepath,
180          push (@conditions, " files.name LIKE '".$param->{'search_filename'}."%'");                  files.date                      AS date,
181          }                  files.type                      AS type,
182                        files.size                      AS size
     $retSQL = "";  
     foreach $cond(@conditions)  
       {  
           if ($retSQL ne "")  
             {  
                 $retSQL .= " AND ";  
             }  
           $retSQL .= $cond;  
       }        
   
       
     return $retSQL;  
 }  
   
 sub getFiles($$)  
   {  
       my ($where, $offset) = @_;  
         
         
       my $dbh = DBI->connect( "dbi:SQLite:dbname=${TopDir}/$Conf{SearchDB}",  
         "", "", { RaiseError => 1, AutoCommit => 1 } );  
       my $sql =            
         q{    
               SELECT files.id                       AS fid,  
                      hosts.name                     AS hname,  
                      shares.name                    AS sname,  
                      shares.share                   AS sharename,  
                      backups.num                    AS backupNum,  
                      files.name                     AS filename,  
                      files.path                     AS filepath,  
                      shares.share||files.fullpath AS networkPath,  
                      date(files.date, 'unixepoch', 'localtime') AS date,  
                      files.type                     AS filetype,  
                      files.size                     AS size,  
                      dvds.name                      AS dvd  
                   FROM  
                      files  
                         INNER JOIN shares  ON files.shareID=shares.ID  
                         INNER JOIN hosts   ON hosts.ID = shares.hostID  
                         INNER JOIN backups ON backups.hostID = hosts.ID  
                         LEFT  JOIN dvds    ON dvds.ID = files.dvdid  
               
           };  
   
       if (defined($where) && $where ne "")  
         {  
             $sql .= " WHERE ". $where;        
         }  
   
       $sql .=  
         q{            
             ORDER BY files.id  
               LIMIT 100  
               OFFSET ? * 100 + 1  
183          };          };
184          
185                  my $sql_from = qq{
186                          FROM files
187        my $st = $dbh->prepare(                          INNER JOIN shares       ON files.shareID=shares.ID
188            $sql                          INNER JOIN hosts        ON hosts.ID = shares.hostID
189            );                              INNER JOIN backups      ON backups.num = files.backupnum and backups.hostID = hosts.ID AND backups.shareID = files.shareID
190        if (!defined($offset) && $offset ne "")          };
191        {  
192          $st->bind_param(1, $offset);          my $sql_where;
193        }          my $where = getWhere($param);
194        else          $sql_where = " WHERE ". $where if ($where);
195        {  
196          $st->bind_param(1,0);          my $order = getSort('sql', $param->{'sort'});
197        }  
198        $st->execute;          my $sql_order = qq{
199                          ORDER BY $order
200        my @ret = ();                  LIMIT $on_page
201        my $tmp;                  OFFSET ?
202                  };
203        while ($tmp = $st->fetchrow_hashref())  
204          {          my $sql_count = qq{ select count(files.id) $sql_from $sql_where };
205              push(@ret, {          my $sql_results = qq{ select $sql_cols $sql_from $sql_where $sql_order };
206                             'hname'       => $tmp->{'hname'},  
207                             'sname'       => $tmp->{'sname'},          my $sth = $dbh->prepare($sql_count);
208                             'sharename'   => $tmp->{'sharename'},          $sth->execute();
209                             'backupno'    => $tmp->{'backupNum'},          my ($results) = $sth->fetchrow_array();
210                             'fname'       => $tmp->{'filename'},  
211                             'fpath'       => $tmp->{'filepath'},          $sth = $dbh->prepare($sql_results);
212                             'networkpath' => $tmp->{'networkPath'},          $sth->execute( $offset );
213                             'date'        => $tmp->{'date'},  
214                             'type'        => $tmp->{'filetype'},          if ($sth->rows != $results) {
215                             'size'        => $tmp->{'size'},                  my $bug = "$0 BUG: [[ $sql_count ]] = $results while [[ $sql_results ]] = " . $sth->rows;
216                             'id'          => $tmp->{'fid'},                  $bug =~ s/\s+/ /gs;
217                             'dvd'         => $tmp->{'dvd'}                  print STDERR "$bug\n";
                        }  
             );  
                                   
218          }          }
219    
220            my @ret;
221                
222        $st->finish();          while (my $row = $sth->fetchrow_hashref()) {
223        $dbh->disconnect();                  push @ret, $row;
       return @ret;  
   }  
   
 sub getBackupsNotBurned()  
   {  
       my $dbh = DBI->connect( "dbi:SQLite:dbname=${TopDir}/$Conf{SearchDB}",  
         "", "", { RaiseError => 1, AutoCommit => 1 } );        
       my $sql = q{  
           SELECT  
             hosts.ID         AS hostID,  
             hosts.name       AS host,  
             backups.num      AS backupno,  
             backups.type     AS type,  
             backups.date     AS date  
           FROM backups, shares, files, hosts  
           WHERE  
             backups.num    = files.backupNum  AND  
             shares.ID      = files.shareID    AND            
             backups.hostID = shares.hostID    AND  
             hosts.ID       = backups.hostID   AND  
             files.dvdid    IS NULL  
           GROUP BY  
             backups.hostID, backups.num  
       };  
       my $st = $dbh -> prepare( $sql );  
       my @ret = ();  
       $st -> execute();  
   
       while ( my $tmp = $st -> fetchrow_hashref() )  
         {            
             push(@ret, {  
                          'host'     => $tmp->{'host'},  
                          'hostid'   => $tmp->{'hostID'},  
                          'backupno' => $tmp->{'backupno'},  
                          'type'     => $tmp->{'type'},  
                          'date'     => $tmp->{'date'}  
                        }  
             );  
224          }          }
225              
226        return @ret;                $sth->finish();
227    }          return ($results, \@ret);
228    }
229    
230    sub getHyperEstraier_url($) {
231            my ($use_hest) = @_;
232    
233            return unless $use_hest;
234    
235            use HyperEstraier;
236            my ($index_path, $index_node_url);
237    
238            if ($use_hest =~ m#^http://#) {
239                    $index_node_url = $use_hest;
240            } else {
241                    $index_path = $TopDir . '/' . $index_path;
242                    $index_path =~ s#//#/#g;
243            }
244            return ($index_path, $index_node_url);
245    }
246    
247  sub displayBackupsGrid()  sub getFilesHyperEstraier($) {
248    {          my ($param) = @_;
249        my $retHTML = "";  
250        my $addForm = 1;          my $offset = $param->{'offset'} || 0;
251            $offset *= $on_page;
252    
253            die "no index_path?" unless ($hest_index_path);
254    
255            use HyperEstraier;
256    
257            my ($index_path, $index_node_url) = getHyperEstraier_url($hest_index_path);
258    
259            # open the database
260            my $db;
261            if ($index_path) {
262                    $db = HyperEstraier::Database->new();
263                    $db->open($index_path, $HyperEstraier::ESTDBREADER);
264            } elsif ($index_node_url) {
265                    $db ||= HyperEstraier::Node->new($index_node_url);
266                    $db->set_auth('admin', 'admin');
267            } else {
268                    die "BUG: unimplemented";
269            }
270    
271            # create a search condition object
272            my $cond = HyperEstraier::Condition->new();
273    
274            my $q = $param->{'search_filename'};
275            my $shareid = $param->{'search_share'};
276    
277            if (length($q) > 0) {
278                    # exact match
279                    $cond->add_attr("filepath ISTRINC $q");
280    
281                    $q =~ s/(.)/$1 /g;
282                    # set the search phrase to the search condition object
283                    $cond->set_phrase($q);
284            }
285    
286            my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
287    
288            $cond->add_attr("backup_date NUMGE $backup_from") if ($backup_from);
289            $cond->add_attr("backup_date NUMLE $backup_to") if ($backup_to);
290    
291            $cond->add_attr("date NUMGE $files_from") if ($files_from);
292            $cond->add_attr("date NUMLE $files_to") if ($files_to);
293    
294            $cond->add_attr("shareid NUMEQ $shareid") if ($shareid);
295    
296    #       $cond->set_max( $offset + $on_page );
297            $cond->set_options( $HyperEstraier::Condition::SURE );
298            $cond->set_order( getSort('est', $param->{'sort'} ) );
299    
300            # get the result of search
301            my @res;
302            my ($result, $hits);
303    
304            if ($index_path) {
305                    $result = $db->search($cond, 0);
306                    $hits = $result->size;
307            } elsif ($index_node_url) {
308                    $result = $db->search($cond, 0);
309                    $hits = $result->doc_num;
310            } else {
311                    die "BUG: unimplemented";
312            }
313    
314            # for each document in result
315            for my $i ($offset .. ($offset + $on_page - 1)) {
316                    last if ($i >= $hits);
317    
318                    my $doc;
319                    if ($index_path) {
320                            my $id = $result->get($i);
321                            $doc = $db->get_doc($id, 0);
322                    } elsif ($index_node_url) {
323                            $doc = $result->get_doc($i);
324                    } else {
325                            die "BUG: unimplemented";
326                    }
327    
328                    my $row;
329                    foreach my $c (qw/fid hname sname backupnum filepath date type size/) {
330                            $row->{$c} = $doc->attr($c);
331                    }
332                    push @res, $row;
333            }
334    
335            return ($hits, \@res);
336    }
337    
338    sub getGzipName($$$)
339    {
340            my ($host, $share, $backupnum) = @_;
341            my $ret = $Conf{GzipSchema};
342            
343            $share =~ s/\//_/g;
344            $ret =~ s/\\h/$host/ge;
345            $ret =~ s/\\s/$share/ge;
346            $ret =~ s/\\n/$backupnum/ge;
347    
348            $ret =~ s/__+/_/g;
349    
350            return $ret;
351            
352    }
353    
354    sub get_tgz_size_by_name($) {
355            my $name = shift;
356    
357            my $tgz = $Conf{InstallDir}.'/'.$Conf{GzipTempDir}.'/'.$name;
358    
359            my $size = -1;
360    
361            if (-f $tgz) {
362                    $size = (stat($tgz))[7];
363            } elsif (-d $tgz) {
364                    opendir(my $dir, $tgz) || die "can't opendir $tgz: $!";
365                    my @parts = grep { !/^\./ && -f "$tgz/$_" } readdir($dir);
366                    $size = 0;
367                    foreach my $part (@parts) {
368                            $size += (stat("$tgz/$part"))[7] || die "can't stat $tgz/$part: $!";
369                    }
370                    closedir $dir;
371            }
372    
373            return $size;
374    }
375    
376    sub getGzipSize($$)
377    {
378            my ($hostID, $backupNum) = @_;
379            my $sql;
380            my $dbh = get_dbh();
381            
382            $sql = q{
383                                    SELECT hosts.name  as host,
384                                               shares.name as share,
385                                               backups.num as backupnum
386                                    FROM hosts, backups, shares
387                                    WHERE shares.id=backups.shareid AND
388                                              hosts.id =backups.hostid AND
389                                              hosts.id=? AND
390                                              backups.num=?
391                            };
392            my $sth = $dbh->prepare($sql);
393            $sth->execute($hostID, $backupNum);
394    
395            my $row = $sth->fetchrow_hashref();
396    
397            return get_tgz_size_by_name(
398                    getGzipName($row->{'host'}, $row->{share}, $row->{'backupnum'})
399            );
400    }
401    
402    sub getBackupsNotBurned() {
403    
404            my $dbh = get_dbh();
405    
406            my $sql = q{
407                    SELECT
408                            backups.hostID AS hostID,
409                            hosts.name AS host,
410                            shares.name AS share,
411                            backups.num AS backupnum,
412                            backups.type AS type,
413                            backups.date AS date,
414                            backups.size AS size,
415                            backups.id AS id,
416                            backups.inc_size AS inc_size,
417                            backups.parts AS parts
418                    FROM backups
419                    INNER JOIN shares       ON backups.shareID=shares.ID
420                    INNER JOIN hosts        ON backups.hostID = hosts.ID
421                    LEFT OUTER JOIN archive_backup ON archive_backup.backup_id = backups.id
422                    WHERE backups.inc_size > 0 AND backups.inc_deleted is false AND archive_backup.backup_id IS NULL
423                    GROUP BY
424                            backups.hostID,
425                            hosts.name,
426                            shares.name,
427                            backups.num,
428                            backups.shareid,
429                            backups.id,
430                            backups.type,
431                            backups.date,
432                            backups.size,
433                            backups.inc_size,
434                            backups.parts
435                    ORDER BY backups.date
436            };
437            my $sth = $dbh->prepare( $sql );
438            my @ret;
439            $sth->execute();
440    
441            while ( my $row = $sth->fetchrow_hashref() ) {
442                    $row->{'age'} = sprintf("%0.1f", ( (time() - $row->{'date'}) / 86400 ) );
443                    $row->{'size'} = sprintf("%0.2f", $row->{'size'} / 1024 / 1024);
444    
445                    # do some cluster calculation (approximate) and convert to kB
446                    $row->{'inc_size'} = int(($row->{'inc_size'} + 1023 ) / ( 2 * 1024 ) * 2);
447                    push @ret, $row;
448            }
449                
450        if ($addForm)          return @ret;      
451          {  }
452    
453              $retHTML .= <<EOF3;  sub displayBackupsGrid() {
454  <script language="javascript" type="text/javascript">  
455            my $retHTML .= q{
456                    <form id="forma" method="POST" action="}.$MyURL.q{?action=burn">
457            };
458    
459            $retHTML .= <<'EOF3';
460    <style type="text/css">
461  <!--  <!--
462    DIV#fixedBox {
463            position: absolute;
464            top: 50em;
465            left: -24%;
466            padding: 0.5em;
467            width: 20%;
468            background-color: #E0F0E0;
469            border: 1px solid #00C000;
470    }
471    
472    DIV#fixedBox, DIV#fixedBox INPUT, DIV#fixedBox TEXTAREA {
473            font-size: 10pt;
474    }
475    
476    FORM>DIV#fixedBox {
477            position: fixed !important;
478            left: 0.5em !important;
479            top: auto !important;
480            bottom: 1em !important;
481            width: 15% !important;
482    }
483    
484    DIV#fixedBox INPUT[type=text], DIV#fixedBox TEXTAREA {
485            border: 1px solid #00C000;
486    }
487    
488    DIV#fixedBox #note {
489            display: block;
490            width: 100%;
491    }
492    
493    DIV#fixedBox #submitBurner {
494            display: block;
495            width: 100%;
496            margin-top: 0.5em;
497            cursor: pointer;
498    }
499    
500    * HTML {
501            overflow-y: hidden;
502    }
503    
504    * HTML BODY {
505            overflow-y: auto;
506            height: 100%;
507            font-size: 100%;
508    }
509    
510    * HTML DIV#fixedBox {
511            position: absolute;
512    }
513    
514    #mContainer, #gradient, #mask, #progressIndicator {
515            display: block;
516            width: 100%;
517            font-size: 10pt;
518            font-weight: bold;
519            text-align: center;
520            vertical-align: middle;
521            padding: 1px;
522    }
523    
524    #gradient, #mask, #progressIndicator {
525            left: 0;
526            border-width: 1px;
527            border-style: solid;
528            border-color: #000000;
529            color: #404040;
530            margin: 0.4em;
531            position: absolute;
532            margin-left: -1px;
533            margin-top: -1px;
534            margin-bottom: -1px;
535            overflow: hidden;
536    }
537    
538      function checkAll(location)  #mContainer {
539      {          display: block;
540        for (var i=0;i<document.forma.elements.length;i++)          position: relative;
541        {          padding: 0px;
542          var e = document.forma.elements[i];          margin-top: 0.4em;
543          if ((e.checked || !e.checked) && e.name != \'all\') {          margin-bottom: 0.5em;
544              if (eval("document.forma."+location+".checked")) {  }
545                  e.checked = true;  
546              } else {  #gradient {
547                  e.checked = false;          z-index: 1;
548              }          background-color: #FFFF00;
549          }  }
550        }  
551      }  #mask {
552  //-->          z-index: 2;
553  </script>                background-color: #FFFFFF;
554    }
555    
556    #progressIndicator {
557            z-index: 3;
558            background-color: transparent;
559    }
560    
561    #parts {
562            padding: 0.4em;
563            display: none;
564            width: 100%;
565            font-size: 80%;
566            color: #ff0000;
567            text-align: center;
568    }
569    -->
570    </style>
571    <script type="text/javascript">
572    <!--
573    
574    var debug_div = null;
575  EOF3  EOF3
576                $retHTML .= q{<form name="forma" method="POST" action="}."$MyURL"."?action=burn\"";  
577                $retHTML.= q{<input type="hidden" value="burn" name="action">};          # take maximum archive size from configuration
578                $retHTML .= q{<input type="hidden" value="results" name="search_results">};          $retHTML .= 'var media_size = '. $Conf{MaxArchiveSize} .';';
579          }  
580        $retHTML .= "<table style=\"fview\">";          $retHTML .= <<'EOF3';
581        $retHTML .= "<tr> ";  
582        if ($addForm)  function debug(msg) {
583          {          return; // Disable debugging
584              $retHTML .= "<td class=\"tableheader\"><input type=\"checkbox\" name=\"allFiles\" onClick=\"checkAll('allFiles');\"></td>";  
585          }          if (! debug_div) debug_div = document.getElementById('debug');
586        $retHTML .=  "<td class=\"tableheader\">Host</td> <td class=\"tableheader\">Backup no</td> <td class=\"tableheader\">Type</td> <td class=\"tableheader\">date</td></tr>";  
587        my @backups = getBackupsNotBurned();          // this will create debug div if it doesn't exist.
588        my $backup;          if (! debug_div) {
589                    debug_div = document.createElement('div');
590        if ($addForm)                  if (document.body) document.body.appendChild(debug_div);
591          {                  else debug_div = null;
592              $retHTML .= "<tr>";          }
593              $retHTML .= "<td colspan=7 style=\"tableheader\">";          if (debug_div) {
594              $retHTML .= "<input type=\"submit\" value=\"Burn selected backups on medium\" name=\"submitBurner\">";                  debug_div.appendChild(document.createTextNode(msg));
595              $retHTML .= "</td>";                  debug_div.appendChild(document.createElement("br"));
596              $retHTML .= "</tr>";          }
597                }
598          }  
599        foreach $backup(@backups)  
600          {  var element_id_cache = Array();
601              my $ftype = "";  
602                function element_id(name,element) {
603              $retHTML .= "<tr>";          if (! element_id_cache[name]) {
604              if ($addForm)                  element_id_cache[name] = self.document.getElementById(name);
605                {          }
606                    $retHTML .= "<td class=\"fview\"> <input type=\"checkbox\" name=\"fcb"          return element_id_cache[name];
607                      .$backup->{'hostid'}."_".$backup->{'backupno'}  }
608                    ."\" value=\"".$backup->{'hostid'}."_".$backup->{'backupno'}."\"> </td>";  
609                }      function checkAll(location) {
610                        var f = element_id('forma') || null;
611              $retHTML .= "<td class=\"fviewborder\">" . $backup->{'host'} . "</td>";          if (!f) return false;
612              $retHTML .= "<td class=\"fviewborder\">" . $backup->{'backupno'} . "</td>";  
613              $retHTML .= "<td class=\"fviewborder\">" . $backup->{'type'} . "</td>";          var len = f.elements.length;
614              $retHTML .= "<td class=\"fviewborder\">" . $backup->{'date'} . "<td>";          var check_all = element_id('allFiles');
615              $retHTML .= "</tr>";          var suma = check_all.checked ? (parseInt(f.elements['totalsize'].value) || 0) : 0;
616          }  
617        $retHTML .= "</table>";          for (var i = 0; i < len; i++) {
618        if ($addForm)                  var e = f.elements[i];
619         {                  if (e.name != 'all' && e.name.substr(0, 3) == 'fcb') {
620             $retHTML .= "</form>";                          if (check_all.checked) {
621         }                                  if (e.checked) continue;
622                                          var el = element_id("fss" + e.name.substr(3));
623        return $retHTML;                                  var size = parseInt(el.value) || 0;
624                                      debug('suma: '+suma+' size: '+size);
625                                      if ((suma + size) < media_size) {
626    }                                                suma += size;
627                                            e.checked = true;
628  sub displayGrid($$$)                                  } else {
629    {                                          break;
630        my ($where, $addForm, $offset) = @_;                                  }
631        my $retHTML = "";                          } else {
632                                          e.checked = false;
633        if ($addForm)                          }
634          {                  }
635                $retHTML .= q{<form name="forma" method="POST" action="}."$MyURL"."?action=search\"";          }
636                $retHTML.= q{<input type="hidden" value="search" name="action">};          update_sum(suma);
637                $retHTML .= q{<input type="hidden" value="results" name="search_results">};  }
638          }  
639        $retHTML .= "<table style=\"fview\">";  function update_sum(suma, suma_disp) {
640        $retHTML .= "<tr> ";          if (! suma_disp) suma_disp = suma;
641        $retHTML .=  "<td class=\"tableheader\">Host</td> <td class=\"tableheader\">Name</td> <td class=\"tableheader\">Type</td> <td class=\"tableheader\">backup no.</td> <td class=\"tableheader\">size</td> <td class=\"tableheader\">date</td>  <td class=\"tableheader\">Media</td></tr>";          element_id('forma').elements['totalsize'].value = suma_disp;
642        my @files = getFiles($where, $offset);          pbar_set(suma, media_size);
643        my $file;          debug('total size: ' + suma);
644    }
645        foreach $file(@files)  
646          {  function sumiraj(e) {
647              my $ftype = "";          var suma = parseInt(element_id('forma').elements['totalsize'].value) || 0;
648                        var len = element_id('forma').elements.length;
649              if ($file->{'type'} == BPC_FTYPE_DIR)          if (e) {
650                {                  var size = parseInt( element_id("fss" + e.name.substr(3)).value);
651                    $ftype = "dir";                  if (e.checked) {
652                }                          suma += size;
653              else                  } else {
654                {                          suma -= size;
655                    $ftype = "file";                  }
656                }  
657              $retHTML .= "<tr>";                  var parts = parseInt( element_id("prt" + e.name.substr(3)).value);
658              $retHTML .= "<td class=\"fviewborder\">" . $file->{'hname'} ."</td>";                  if (suma > media_size && suma == size && parts > 1) {
659              $retHTML .= "<td class=\"fviewborder\">" . $file->{'fname'} . "</td>";                          element_id("parts").innerHTML = "This will take "+parts+" mediums!";
660              $retHTML .= "<td class=\"fviewborder\">" . $ftype . "</td>";                          element_id("parts").style.display = 'block';
661              $retHTML .= "<td class=\"fviewborder\">" . $file->{'backupno'} . "</td>";                          update_sum(media_size, suma);
662              $retHTML .= "<td class=\"fviewborder\">" . $file->{'size'} . "</td>";                          suma = media_size;
663              $retHTML .= "<td class=\"fviewborder\">" . $file->{'date'} . "</td>";                          return suma;
664              $retHTML .= "<td class=\"fviewborder\">" . $file->{'dvd'} . "</td>";                  } else {
665              $retHTML .= "</tr>";                          element_id("parts").style.display = 'none';
666          }                  }
667        $retHTML .= "</table>";  
668                    if (suma < 0) suma = 0;
669                  } else {
670                    suma = 0;
671        $retHTML .= "<INPUT TYPE=\"hidden\" VALUE=\"\" NAME=\"offset\">";                  for (var i = 0; i < len; i++) {
672        for (my $ii = 1; $ii <= $#files; $ii++)                          var e = element_id('forma').elements[i];
673        {                          if (e.name != 'all' && e.checked && e.name.substr(0,3) == 'fcb') {
674            $retHTML .= "<a href = \"#\" onclick=\"document.forma.offset.value=$ii;document.forma.submit();\">$ii</a>";                                  var el = element_id("fss" + e.name.substr(3));
675            if ($ii < $#files)                                  if (el && el.value) suma += parseInt(el.value) || 0;
676              {                          }
677                  $retHTML .= " | ";                  }
678              }          }
679        }          update_sum(suma);
680            return suma;
681    }
682         if ($addForm)  
683         {  /* progress bar */
684             $retHTML .= "</form>";  
685         }  var _pbar_width = null;
686    var _pbar_warn = 10;    // change color in last 10%
687    
688    function pbar_reset() {
689            element_id("mask").style.left = "0px";
690            _pbar_width = element_id("mContainer").offsetWidth - 2;
691            element_id("mask").style.width = _pbar_width + "px";
692            element_id("mask").style.display = "block";
693            element_id("progressIndicator").style.zIndex  = 10;
694            element_id("progressIndicator").innerHTML = "0";
695    }
696    
697    function dec2hex(d) {
698            var hch = '0123456789ABCDEF';
699            var a = d % 16;
700            var q = (d - a) / 16;
701            return hch.charAt(q) + hch.charAt(a);
702    }
703    
704    function pbar_set(amount, max) {
705            debug('pbar_set('+amount+', '+max+')');
706    
707            if (_pbar_width == null) {
708                    var _mc = element_id("mContainer");
709                    if (_pbar_width == null) _pbar_width = parseInt(_mc.offsetWidth ? (_mc.offsetWidth - 2) : 0) || null;
710                    if (_pbar_width == null) _pbar_width = parseInt(_mc.clientWidth ? (_mc.clientWidth + 2) : 0) || null;
711                    if (_pbar_width == null) _pbar_width = 0;
712            }
713    
714            var pcnt = Math.floor(amount * 100 / max);
715            var p90 = 100 - _pbar_warn;
716            var pcol = pcnt - p90;
717            if (Math.round(pcnt) <= 100) {
718                    if (pcol < 0) pcol = 0;
719                    var e = element_id("submitBurner");
720                    debug('enable_button');
721                    e.disabled = false;
722                    var a = e.getAttributeNode('disabled') || null;
723                    if (a) e.removeAttributeNode(a);
724            } else {
725                    debug('disable button');
726                    pcol = _pbar_warn;
727                    var e = element_id("submitBurner");
728                    if (!e.disabled) e.disabled = true;
729            }
730            var col_g = Math.floor((_pbar_warn - pcol) * 255 / _pbar_warn);
731            var col = '#FF' + dec2hex(col_g) + '00';
732    
733            //debug('pcol: '+pcol+' g:'+col_g+' _pbar_warn:'+ _pbar_warn + ' color: '+col);
734            element_id("gradient").style.backgroundColor = col;
735    
736            element_id("progressIndicator").innerHTML = pcnt + '%';
737            //element_id("progressIndicator").innerHTML = amount;
738    
739            element_id("mask").style.clip = 'rect(' + Array(
740                    '0px',
741                    element_id("mask").offsetWidth + 'px',
742                    element_id("mask").offsetHeight + 'px',
743                    Math.round(_pbar_width * amount / max) + 'px'
744            ).join(' ') + ')';
745    }
746    
747    if (!self.body) self.body = new Object();
748    self.onload = self.document.onload = self.body.onload = function() {
749            //pbar_reset();
750            sumiraj();
751    };
752    
753    // -->
754    </script>
755    <div id="fixedBox">
756    
757    Size: <input type="text" name="totalsize" size="7" readonly="readonly" style="text-align:right;" value="0" /> kB
758    
759    <div id="mContainer">
760            <div id="gradient">&nbsp;</div>
761            <div id="mask">&nbsp;</div>
762            <div id="progressIndicator">0%</div>
763    </div>
764    <br/>
765    
766    <div id="parts">&nbsp;</div>
767    
768    Note:
769    <textarea name="note" cols="10" rows="5" id="note"></textarea>
770    
771    <input type="submit" id="submitBurner" value="Burn selected" name="submitBurner" />
772    
773    </div>
774    <!--
775    <div id="debug" style="float: right; width: 10em; border: 1px #ff0000 solid; background-color: #ffe0e0; -moz-opacity: 0.7;">
776    no debug output yet
777    </div>
778    -->
779    EOF3
780            $retHTML .= q{
781                            <input type="hidden" value="burn" name="action">
782                            <input type="hidden" value="results" name="search_results">
783                            <table style="fview" border="0" cellspacing="0" cellpadding="2">
784                            <tr class="tableheader">
785                            <td class="tableheader">
786                                    <input type="checkbox" name="allFiles" id="allFiles" onClick="checkAll('allFiles');">
787                            </td>
788                            <td align="center">Share</td>
789                            <td align="center">Backup no</td>
790                            <td align="center">Type</td>
791                            <td align="center">date</td>
792                            <td align="center">age/days</td>
793                            <td align="center">size/MB</td>
794                            <td align="center">gzip size/kB</td>
795                            </tr>
796    
797            };
798    
799            my @color = (' bgcolor="#e0e0e0"', '');
800    
801            my $i = 0;
802            my $host = '';
803    
804            foreach my $backup ( getBackupsNotBurned() ) {
805    
806                    if ($host ne $backup->{'host'}) {
807                            $i++;
808                            $host = $backup->{'host'};
809                    }
810                    my $ftype = "";
811    
812                    my $checkbox_key = $backup->{'hostid'}. '_' .$backup->{'backupnum'} . '_' . $backup->{'id'};
813    
814                    $retHTML .=
815                            '<tr' . $color[$i %2 ] . '>
816                            <td class="fview">';
817    
818                    if (($backup->{'inc_size'} || 0) > 0) {
819                            $retHTML .= '
820                            <input type="checkbox" name="fcb' . $checkbox_key . '" value="' . $checkbox_key . '" onClick="sumiraj(this);">';
821                    }
822    
823                    $retHTML .=
824                            '</td>' .
825                            '<td align="right">' . $backup->{'host'} . ':' . $backup->{'share'} . '</td>' .
826                            '<td align="center">' . $backup->{'backupnum'} . '</td>' .
827                            '<td align="center">' . $backup->{'type'} . '</td>' .
828                            '<td align="center">' . epoch_to_iso( $backup->{'date'} ) . '</td>' .
829                            '<td align="center">' . $backup->{'age'} . '</td>' .
830                            '<td align="right">' . $backup->{'size'} . '</td>' .
831                            '<td align="right">' . $backup->{'inc_size'} .
832                            '<input type="hidden" id="fss'.$checkbox_key .'" value="'. $backup->{'inc_size'} .'"></td>' .
833                            '<input type="hidden" id="prt'.$checkbox_key .'" value="'. $backup->{'parts'} .'"></td>' .
834    
835                            "</tr>\n";
836            }
837    
838            $retHTML .= "</table>";
839            $retHTML .= "</form>";
840                
841        return $retHTML;          return $retHTML;
842    }  }      
843    
844    sub displayGrid($) {
845            my ($param) = @_;
846    
847            my $offset = $param->{'offset'};
848            my $hilite = $param->{'search_filename'};
849    
850            my $retHTML = "";
851    
852            my $start_t = time();
853    
854            my ($results, $files);
855            if ($param->{'use_hest'} && length($hilite) > 0) {
856                    ($results, $files) = getFilesHyperEstraier($param);
857            } else {
858                    ($results, $files) = getFiles($param);
859            }
860    
861            my $dur_t = time() - $start_t;
862            my $dur = sprintf("%0.4fs", $dur_t);
863    
864            my ($from, $to) = (($offset * $on_page) + 1, ($offset * $on_page) + $on_page);
865    
866            if ($results <= 0) {
867                    $retHTML .= qq{
868                            <p style="color: red;">No results found...</p>
869                    };
870                    return $retHTML;
871            } else {
872                    # DEBUG
873                    #use Data::Dumper;
874                    #$retHTML .= '<pre>' . Dumper($files) . '</pre>';
875            }
876    
877    
878            $retHTML .= qq{
879            <div>
880            Found <b>$results files</b> showing <b>$from - $to</b> (took $dur)
881            </div>
882            <table style="fview" width="100%" border="0" cellpadding="2" cellspacing="0">
883                    <tr class="fviewheader">
884                    <td></td>
885            };
886    
887            my ($sort_what, $sort_dir) = split(/_/,$param->{'sort'},2);
888    
889            sub sort_header($$$) {
890                    my ($param, $display, $name) = @_;
891    
892                    my $html = '<td align="center"';
893                    if (lc($sort_what) eq lc($name)) {
894                            my $dir = lc($sort_dir);
895                            $dir =~ tr/ad/da/;
896                            $param->{'sort'} = $name . '_' . $dir;
897                            $html .= ' style="border: 1px solid #808080;"';
898                    } else {
899                            $param->{'sort'} = $name . '_a';
900                    }
901                    $html .= '<a href="' . page_uri($param) . '">' . $display . '</a></td>';
902                    return $html;
903            }
904    
905            $retHTML .=
906                    sort_header($param, 'Share', 'share') .
907                    sort_header($param, 'Type and Name', 'path') .
908                    sort_header($param, '#', 'num') .
909                    sort_header($param, 'Size', 'size') .
910                    sort_header($param, 'Date', 'date');
911    
912            $retHTML .= qq{
913                    <td align="center">Media</td>
914                    </tr>
915            };
916    
917            my $file;
918    
919            sub hilite_html($$) {
920                    my ($html, $search) = @_;
921                    $html =~ s#($search)#<b>$1</b>#gis;
922                    return $html;
923            }
924    
925            sub restore_link($$$$$$) {
926                    my $type = shift;
927                    my $action = 'RestoreFile';
928                    $action = 'browse' if (lc($type) eq 'dir');
929                    return sprintf(qq{<a href="?action=%s&host=%s&num=%d&share=%s&dir=%s">%s</a>}, $action, @_);
930            }
931    
932            my $sth_archived;
933            my %archived_cache;
934    
935            sub check_archived($$$) {
936                    my ($host, $share, $num) = @_;
937    
938                    if (my $html = $archived_cache{"$host $share $num"}) {
939                            return $html;
940                    }
941    
942                    $sth_archived ||= $dbh->prepare(qq{
943                            select
944                                    dvd_nr, note,
945                                    count(archive_burned.copy) as copies
946                            from archive
947                            inner join archive_burned on archive_burned.archive_id = archive.id
948                            inner join archive_backup on archive.id = archive_backup.archive_id
949                            inner join backups on backups.id = archive_backup.backup_id
950                            inner join hosts on hosts.id = backups.hostid
951                            inner join shares on shares.id = backups.shareid
952                            where hosts.name = ? and shares.name = ? and backups.num = ?
953                            group by dvd_nr, note
954                    });
955    
956                    my @mediums;
957    
958                    $sth_archived->execute($host, $share, $num);
959                    while (my $row = $sth_archived->fetchrow_hashref()) {
960                            push @mediums, '<abbr title="' .
961                                    $row->{'note'} .
962                                    ' [' . $row->{'copies'} . ']' .
963                                    '">' .$row->{'dvd_nr'} .
964                                    '</abbr>';
965                    }
966    
967                    my $html = join(", ",@mediums);
968                    $archived_cache{"$host $share $num"} = $html;
969                    return $html;
970            }
971    
972            my $i = $offset * $on_page;
973    
974            foreach $file (@{ $files }) {
975                    $i++;
976    
977                    my $typeStr  = BackupPC::Attrib::fileType2Text(undef, $file->{'type'});
978                    $retHTML .= qq{<tr class="fviewborder">};
979    
980                    $retHTML .= qq{<td class="fviewborder">$i</td>};
981    
982                    $retHTML .=
983                            qq{<td class="fviewborder" align="right">} . $file->{'hname'} . ':' . $file->{'sname'} . qq{</td>} .
984                            qq{<td class="fviewborder"><img src="$Conf{CgiImageDirURL}/icon-$typeStr.gif" alt="$typeStr" align="middle">&nbsp;} . hilite_html( $file->{'filepath'}, $hilite ) . qq{</td>} .
985                            qq{<td class="fviewborder" align="center">} . restore_link( $typeStr, ${EscURI( $file->{'hname'} )}, $file->{'backupnum'}, ${EscURI( $file->{'sname'})}, ${EscURI( $file->{'filepath'} )}, $file->{'backupnum'} ) . qq{</td>} .
986                            qq{<td class="fviewborder" align="right">} . $file->{'size'} . qq{</td>} .
987                            qq{<td class="fviewborder">} . epoch_to_iso( $file->{'date'} ) . qq{</td>} .
988                            qq{<td class="fviewborder">} . check_archived( $file->{'hname'}, $file->{'sname'}, $file->{'backupnum'} ) . qq{</td>};
989    
990                    $retHTML .= "</tr>";
991            }
992            $retHTML .= "</table>";
993    
994            # all variables which has to be transfered
995            foreach my $n (qw/search_day_from search_month_from search_year_from search_day_to search_month_to search_year_to search_backup_day_from search_backup_month_from search_backup_year_from search_backup_day_to search_backup_month_to search_backup_year_to search_filename offset/) {
996                    $retHTML .= qq{<INPUT TYPE="hidden" NAME="$n" VALUE="$In{$n}">\n};
997            }
998    
999            my $del = '';
1000            my $max_page = int( $results / $on_page );
1001            my $page = 0;
1002    
1003            sub page_uri($) {
1004                    my $param = shift || die "no param?";
1005    
1006                    my $uri = $MyURL;
1007                    my $del = '?';
1008                    foreach my $k (keys %{ $param }) {
1009                            if ($param->{$k}) {
1010                                    $uri .= $del . $k . '=' . ${EscURI( $param->{$k} )};
1011                                    $del = '&';
1012                            }
1013                    }
1014                    return $uri;
1015            }
1016    
1017            sub page_link($$$) {
1018                    my ($param,$page,$display) = @_;
1019    
1020                    $param->{'offset'} = $page if (defined($page));
1021    
1022                    my $html = '<a href = "' . page_uri($param) . '">' . $display . '</a>';
1023            }
1024    
1025            $retHTML .= '<div style="text-align: center;">';
1026    
1027            if ($offset > 0) {
1028                    $retHTML .= page_link($param, $offset - 1, '&lt;&lt;') . ' ';
1029            }
1030    
1031            while ($page <= $max_page) {
1032                    if ($page == $offset) {
1033                            $retHTML .= $del . '<b>' . ($page + 1) . '</b>';
1034                    } else {
1035                            $retHTML .= $del . page_link($param, $page, $page + 1);
1036                    }
1037    
1038                    if ($page < $offset - $pager_pages && $page != 0) {
1039                            $retHTML .= " ... ";
1040                            $page = $offset - $pager_pages;
1041                            $del = '';
1042                    } elsif ($page > $offset + $pager_pages && $page != $max_page) {
1043                            $retHTML .= " ... ";
1044                            $page = $max_page;
1045                            $del = '';
1046                    } else {
1047                            $del = ' | ';
1048                            $page++;
1049                    }
1050            }
1051    
1052            if ($offset < $max_page) {
1053                    $retHTML .= ' ' . page_link($param, $offset + 1, '&gt;&gt;');
1054            }
1055    
1056            $retHTML .= "</div>";
1057    
1058            return $retHTML;
1059    }
1060    
1061  1;  1;

Legend:
Removed from v.9  
changed lines
  Added in v.211

  ViewVC Help
Powered by ViewVC 1.1.26