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