ee982fffb4201d5f71a5c6660e54f95ee509322a
[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
358 sub getBackupsNotBurned($) {
359
360         my $param = shift;
361         my $dbh = get_dbh();
362
363         my $order = getSort('burn', 'sql', $param->{'sort'});
364
365 print STDERR "## sort=". ($param->{'sort'} || 'no sort param') . " burn sql order: $order\n";
366
367         my $sql = qq{
368                 SELECT
369                         p.id,
370                         p.filename,
371                         b.date,
372                         date_part('epoch',now()) - b.date as age,
373                         p.size,
374                         count(ap.*) as scheduled,
375                         count(ab.*) as burned
376                 FROM backup_parts p
377                 JOIN backups b          ON b.id = p.backup_id
378                 LEFT OUTER JOIN archive_parts  ap ON ap.backup_part_id = p.id
379                 LEFT OUTER JOIN archive_burned ab ON ab.archive_id = ap.archive_id
380                 GROUP BY p.id,filename,b.date,age,p.size,p.part_nr
381         };
382
383         $sql .= qq{
384                 HAVING count(ap.*) = 0
385         } unless $param->{scheduled};
386
387         $sql .= qq{
388                 ORDER BY b.date,p.part_nr
389         };
390         my $sth = $dbh->prepare( $sql );
391         my @ret;
392         $sth->execute();
393
394         while ( my $row = $sth->fetchrow_hashref() ) {
395                 $row->{'age'} = sprintf("%0.1f", ( $row->{'age'} / 86400 ) );
396                 #$row->{'age'} = sprintf("%0.1f", ( (time() - $row->{'date'}) / 86400 ) );
397                 push @ret, $row;
398         }
399       
400         return @ret;
401 }
402
403 sub displayBackupsGrid($) {
404
405         my $param = shift;
406
407         my $max_archive_size = $Conf{ArchiveMediaSize} || die "no ArchiveMediaSize";
408
409         my $retHTML .= qq|
410                 <form id="forma" method="POST" action="$MyURL?action=burn">
411
412 <script type="text/javascript">
413 var media_size = $max_archive_size ;
414 </script>
415
416         |;
417
418         { local $/ = undef; $retHTML .= <DATA> }
419
420         $retHTML .= q{
421                         <input type="hidden" value="burn" name="action">
422                         <input type="hidden" value="results" name="search_results">
423                         <table style="fview" border="0" cellspacing="0" cellpadding="2">
424                         <tr class="tableheader">
425                         <td class="tableheader">
426                                 <input type="checkbox" name="allFiles" id="allFiles" onClick="checkAll('allFiles');">
427                         </td>
428         } .
429                 sort_header($param, 'Filename', 'filename', 'left') .
430                 sort_header($param, 'Date', 'date', 'center') .
431                 sort_header($param, 'Age/days', 'age', 'center') .
432                 sort_header($param, 'Size', 'size', 'center') .
433         qq{
434                         <td align="center" title="scheduled">sc</td>
435                         <td align="center" title="burned">bu</td>
436                 </tr>
437         };
438
439         my @color = (' bgcolor="#e0e0e0"', '');
440
441         my $i = 1;
442 #       my $img_url = $Conf{CgiImageDirURL};
443
444         foreach my $backup ( getBackupsNotBurned($param) ) {
445
446                 $retHTML .= join(''
447                         ,'<tr',$color[$i++%2],'>'
448                         ,'<td class="fview">'
449                                 ,'<input type="checkbox" name="fcb',$backup->{id},'" value="',$backup->{id},'" onClick="sumiraj(this);">'
450                                 ,'<input type="hidden" id="fss',$backup->{id},'" value="',$backup->{size},'">'
451                         ,'</td>'
452                         ,'<td align="left">', $backup->{'filename'}, '</td>'
453                         ,'<td align="center">', epoch_to_iso( $backup->{'date'} ), '</td>'
454                         ,'<td align="center">', $backup->{'age'}, '</td>'
455                         ,'<td align="right">', unit($backup->{'size'}), '</td>'
456                         ,'<td align="center">', $backup->{scheduled}, '</td>'
457                         ,'<td align="center">', $backup->{burned}, '</td>'
458                         ,"</tr>\n"
459                 );
460         }
461
462         $retHTML .= "</table>";
463
464         $retHTML .= q{
465 <input type=submit name="scheduled" value="Show scheduled parts">
466         } unless $param->{scheduled};
467
468         $retHTML .= "</form>";
469       
470         return $retHTML;
471 }      
472
473 sub displayGrid($) {
474         my ($param) = @_;
475
476         my $offset = $param->{'offset'};
477         my $hilite = $param->{'search_filename'};
478
479         my $retHTML = "";
480  
481         my $start_t = time();
482
483         my ($results, $files);
484         if ($param->{'use_hest'} && length($hilite) > 0) {
485                 ($results, $files) = getFilesHyperEstraier($param);
486         } else {
487                 ($results, $files) = getFiles($param);
488         }
489
490         my $dur_t = time() - $start_t;
491         my $dur = sprintf("%0.4fs", $dur_t);
492
493         my ($from, $to) = (($offset * $on_page) + 1, ($offset * $on_page) + $on_page);
494
495         if ($results <= 0) {
496                 $retHTML .= qq{
497                         <p style="color: red;">No results found...</p>
498                 };
499                 return $retHTML;
500         } else {
501                 # DEBUG
502                 #use Data::Dumper;
503                 #$retHTML .= '<pre>' . Dumper($files) . '</pre>';
504         }
505
506
507         $retHTML .= qq{
508         <div>
509         Found <b>$results files</b> showing <b>$from - $to</b> (took $dur)
510         </div>
511         <table style="fview" width="100%" border="0" cellpadding="2" cellspacing="0">
512                 <tr class="fviewheader"> 
513                 <td></td>
514         };
515
516         sub sort_header($$$$) {
517                 my ($param, $display, $name, $align) = @_;
518
519                 my ($sort_what, $sort_direction) = split(/_/,$param->{'sort'},2);
520
521                 my $old_sort = $param->{'sort'};
522
523                 my $html = qq{<td align="$align"};
524                 my $arrow = '';
525
526                 if (lc($sort_what) eq lc($name)) {
527                         my $direction = lc($sort_direction);
528
529                         # swap direction or fallback to default
530                         $direction =~ tr/ad/da/;
531                         $direction = 'a' unless ($direction =~ /[ad]/);
532
533                         $param->{'sort'} = $name . '_' . $direction;
534                         $html .= ' style="border: 1px solid #808080;"';
535                 
536                         # add unicode arrow for direction
537                         $arrow .= '&nbsp;';
538                         $arrow .= $direction eq 'a'  ?  '&#9650;'
539                                 : $direction eq 'd'  ?  '&#9660;'
540                                 :                       ''
541                                 ;
542
543                 } else {
544                         $param->{'sort'} = $name . '_a';
545                 }
546
547                 $html .= '><a href="' . page_uri($param) . '">' . $display . '</a>' . $arrow . '</td>';
548                 $param->{'sort'} = $old_sort;
549
550                 return $html;
551         }
552
553         $retHTML .=
554                 sort_header($param, 'Share', 'sname', 'center') .
555                 sort_header($param, 'Type and Name', 'filepath', 'center') .
556                 sort_header($param, '#', 'backupnum', 'center') .
557                 sort_header($param, 'Size', 'size', 'center') .
558                 sort_header($param, 'Date', 'date', 'center');
559
560         $retHTML .= qq{
561                 <td align="center">Media</td>
562                 </tr>
563         };
564
565         my $file;
566
567         sub hilite_html($$) {
568                 my ($html, $search) = @_;
569                 $html =~ s#($search)#<b>$1</b>#gis;
570                 return $html;
571         }
572
573         sub restore_link($$$$$$) {
574                 my $type = shift;
575                 my $action = 'RestoreFile';
576                 $action = 'browse' if (lc($type) eq 'dir');
577                 return sprintf(qq{<a href="?action=%s&host=%s&num=%d&share=%s&dir=%s">%s</a>}, $action, @_);
578         }
579
580         my $sth_archived;
581         my %archived_cache;
582
583         sub check_archived($$$) {
584                 my ($host, $share, $num) = @_;
585
586                 if (my $html = $archived_cache{"$host $share $num"}) {
587                         return $html;
588                 }
589
590                 $sth_archived ||= $dbh->prepare(qq{
591                         select
592                                 archive.dvd_nr, note,
593                                 count(archive_burned.copy) as copies
594                         from archive
595                         inner join archive_burned on archive_burned.archive_id = archive.id
596                         inner join archive_backup_parts on archive.id = archive_backup_parts.archive_id
597                         inner join backups on backups.id = archive_backup_parts.backup_id
598                         inner join hosts on hosts.id = backups.hostid
599                         inner join shares on shares.id = backups.shareid
600                         where hosts.name = ? and shares.name = ? and backups.num = ?
601                         group by archive.dvd_nr, note
602                 });
603
604                 my @mediums;
605
606                 $sth_archived->execute($host, $share, $num);
607                 while (my $row = $sth_archived->fetchrow_hashref()) {
608                         push @mediums, '<abbr title="' .
609                                 $row->{'note'} .
610                                 ' [' . $row->{'copies'} . ']' .
611                                 '">' .$row->{'dvd_nr'} .
612                                 '</abbr>';
613                 }
614
615                 my $html = join(", ",@mediums);
616                 $archived_cache{"$host $share $num"} = $html;
617                 return $html;
618         }
619
620         my $i = $offset * $on_page;
621
622         foreach $file (@{ $files }) {
623                 $i++;
624
625                 my $typeStr  = BackupPC::Attrib::fileType2Text(undef, $file->{'type'});
626                 $retHTML .= qq{<tr class="fviewborder">};
627
628                 $retHTML .= qq{<td class="fviewborder">$i</td>};
629
630                 $retHTML .=
631                         qq{<td class="fviewborder" align="right">} . $file->{'hname'} . ':' . $file->{'sname'} . qq{</td>} .
632                         qq{<td class="fviewborder"><img src="$Conf{CgiImageDirURL}/icon-$typeStr.png" alt="$typeStr" align="middle">&nbsp;} . hilite_html( $file->{'filepath'}, $hilite ) . qq{</td>} .
633                         qq{<td class="fviewborder" align="center">} . restore_link( $typeStr, ${EscURI( $file->{'hname'} )}, $file->{'backupnum'}, ${EscURI( $file->{'sname'})}, ${EscURI( $file->{'filepath'} )}, $file->{'backupnum'} ) . qq{</td>} .
634                         qq{<td class="fviewborder" align="right">} . $file->{'size'} . qq{</td>} .
635                         qq{<td class="fviewborder">} . epoch_to_iso( $file->{'date'} ) . qq{</td>} .
636                         qq{<td class="fviewborder">} . check_archived( $file->{'hname'}, $file->{'sname'}, $file->{'backupnum'} ) . qq{</td>};
637
638                 $retHTML .= "</tr>";
639         }
640         $retHTML .= "</table>";
641
642         # all variables which has to be transfered
643         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/) {
644                 $retHTML .= qq{<INPUT TYPE="hidden" NAME="$n" VALUE="$In{$n}">\n};
645         }
646
647         my $del = '';
648         my $max_page = int( $results / $on_page );
649         my $page = 0;
650
651         sub page_uri($) {
652                 my $param = shift || die "no param?";
653
654                 my $uri = $MyURL;
655                 my $del = '?';
656                 foreach my $k (keys %{ $param }) {
657                         if ($param->{$k}) {
658                                 $uri .= $del . $k . '=' . ${EscURI( $param->{$k} )};
659                                 $del = '&';
660                         }
661                 }
662                 return $uri;
663         }
664
665         sub page_link($$$) {
666                 my ($param,$page,$display) = @_;
667
668                 $param->{'offset'} = $page if (defined($page));
669
670                 my $html = '<a href = "' . page_uri($param) . '">' . $display . '</a>';
671         }
672
673         $retHTML .= '<div style="text-align: center;">';
674
675         if ($offset > 0) {
676                 $retHTML .= page_link($param, $offset - 1, '&lt;&lt;') . ' ';
677         }
678
679         while ($page <= $max_page) {
680                 if ($page == $offset) {
681                         $retHTML .= $del . '<b>' . ($page + 1) . '</b>';
682                 } else {
683                         $retHTML .= $del . page_link($param, $page, $page + 1);
684                 }
685
686                 if ($page < $offset - $pager_pages && $page != 0) {
687                         $retHTML .= " ... ";
688                         $page = $offset - $pager_pages;
689                         $del = '';
690                 } elsif ($page > $offset + $pager_pages && $page != $max_page) {
691                         $retHTML .= " ... ";
692                         $page = $max_page;
693                         $del = '';
694                 } else {
695                         $del = ' | ';
696                         $page++;
697                 }
698         }
699
700         if ($offset < $max_page) {
701                 $retHTML .= ' ' . page_link($param, $offset + 1, '&gt;&gt;');
702         }
703
704         $retHTML .= "</div>";
705
706         return $retHTML;
707 }
708
709 my @units = qw/b k M G/;
710 sub unit {
711         my $v = shift;
712
713         my $o = 0;
714
715         while ( ( $v / 10000 ) >= 1 ) {
716                 $o++;
717                 $v /= 1024;
718         }
719
720         if ( $v >= 1 ) {
721                 return sprintf("%d%s", $v, $units[$o]);
722         } elsif ( $v == 0 ) {
723                 return 0;
724         } else {
725                 return sprintf("%.1f%s", $v, $units[$o]);
726         }
727 }
728
729 1;
730
731 __DATA__
732
733
734 <style type="text/css">
735 <!--
736 DIV#fixedBox {
737         position: absolute;
738         top: 50em;
739         left: -24%;
740         padding: 0.5em;
741         width: 20%;
742         background-color: #E0F0E0;
743         border: 1px solid #00C000;
744 }
745
746 DIV#fixedBox, DIV#fixedBox INPUT, DIV#fixedBox TEXTAREA {
747         font-size: 10pt;
748 }
749
750 FORM>DIV#fixedBox {
751         position: fixed !important;
752         left: 0.5em !important;
753         top: auto !important;
754         bottom: 1em !important;
755         width: 15% !important;
756 }
757
758 DIV#fixedBox INPUT[type=text], DIV#fixedBox TEXTAREA {
759         border: 1px solid #00C000;
760 }
761
762 DIV#fixedBox #note {
763         display: block;
764         width: 100%;
765 }
766
767 DIV#fixedBox #submitBurner {
768         display: block;
769         width: 100%;
770         margin-top: 0.5em;
771         cursor: pointer;
772 }
773
774 * HTML {
775         overflow-y: hidden;
776 }
777
778 * HTML BODY {
779         overflow-y: auto;
780         height: 100%;
781         font-size: 100%;
782 }
783
784 * HTML DIV#fixedBox {
785         position: absolute;
786 }
787
788 #mContainer, #gradient, #mask, #progressIndicator {
789         display: block;
790         width: 100%;
791         font-size: 10pt;
792         font-weight: bold;
793         text-align: center;
794         vertical-align: middle;
795         padding: 1px;
796 }
797
798 #gradient, #mask, #progressIndicator {
799         left: 0;
800         border-width: 1px;
801         border-style: solid;
802         border-color: #000000;
803         color: #404040;
804         margin: 0.4em;
805         position: absolute;
806         margin-left: -1px;
807         margin-top: -1px;
808         margin-bottom: -1px;
809         overflow: hidden;
810 }
811
812 #mContainer {
813         display: block;
814         position: relative;
815         padding: 0px;
816         margin-top: 0.4em;
817         margin-bottom: 0.5em;
818 }
819
820 #gradient {
821         z-index: 1;
822         background-color: #FFFF00;
823 }
824
825 #mask {
826         z-index: 2;
827         background-color: #FFFFFF;
828 }
829
830 #progressIndicator {
831         z-index: 3;
832         background-color: transparent;
833 }
834
835 #volumes {
836         padding: 0.4em;
837         display: none;
838         width: 100%;
839         font-size: 80%;
840         color: #ff0000;
841         text-align: center;
842 }
843 -->
844 </style>
845 <script type="text/javascript">
846 <!--
847
848 var debug_div;
849
850 function debug(msg) {
851         return; // Disable debugging
852
853         if (! debug_div) debug_div = document.getElementById('debug');
854
855         // this will create debug div if it doesn't exist.
856         if (! debug_div) {
857                 debug_div = document.createElement('div');
858                 if (document.body) document.body.appendChild(debug_div);
859                 else debug_div = null;
860         }
861         if (debug_div) {
862                 debug_div.appendChild(document.createTextNode(msg));
863                 debug_div.appendChild(document.createElement("br"));
864         }
865 }
866
867
868 var element_id_cache = Array();
869
870 function element_id(name,element) {
871         if (! element_id_cache[name]) {
872                 element_id_cache[name] = self.document.getElementById(name);
873         }
874         return element_id_cache[name];
875 }
876
877 function checkAll(location) {
878         var f = element_id('forma') || null;
879         if (!f) return false;
880
881         var len = f.elements.length;
882         var check_all = element_id('allFiles');
883         var suma = check_all.checked ? (parseInt(f.elements['totalsize'].value) || 0) : 0;
884
885         for (var i = 0; i < len; i++) {
886                 var e = f.elements[i];
887                 if (e.name != 'all' && e.name.substr(0, 3) == 'fcb') {
888                         if (check_all.checked) {
889                                 if (e.checked) continue;
890                                 var el = element_id("fss" + e.name.substr(3));
891                                 var size = parseInt(el.value) || 0;
892                                 debug('suma: '+suma+' size: '+size);
893                                 if ((suma + size) < media_size) {
894                                         suma += size;
895                                         e.checked = true;
896                                 } else {
897                                         break;
898                                 }
899                         } else {
900                                 e.checked = false;
901                         }
902                 }
903         }
904         update_sum(suma);
905 }
906
907 function update_sum(suma, suma_disp) {
908         if (! suma_disp) suma_disp = suma;
909         suma_disp = Math.floor(suma_disp / 1024);
910         element_id('forma').elements['totalsize_kb'].value = suma_disp;
911         element_id('forma').elements['totalsize'].value = suma;
912         pbar_set(suma, media_size);
913         debug('total size: ' + suma);
914 }
915
916 function update_size(name, checked, suma) {
917         var size = parseInt( element_id("fss" + name).value);
918
919         if (checked) {
920                 suma += size;
921         } else {
922                 suma -= size;
923         }
924
925         debug('update_size('+name+','+checked+') suma: '+suma);
926 /* FIXME
927         if (volumes > 1) {
928                 if (checked) {
929                         element_id("volumes").innerHTML = "This will take "+volumes+" mediums!";
930                         element_id("volumes").style.display = 'block';
931                         suma = size;
932                         update_sum(suma);
933                 } else {
934                         suma -= size;
935                         element_id("volumes").style.display = 'none';
936                 }
937         }
938 */
939         return suma;
940 }
941
942 function sumiraj(e) {
943         var suma = parseInt(element_id('forma').elements['totalsize'].value) || 0;
944         var len = element_id('forma').elements.length;
945         if (e) {
946                 suma = update_size(e.name.substr(3), e.checked, suma);
947                 if (suma < 0) suma = 0;
948         } else {
949                 suma = 0;
950                 for (var i = 0; i < len; i++) {
951                         var fel = element_id('forma').elements[i];
952                         if (fel.name != 'all' && fel.checked && fel.name.substr(0,3) == 'fcb') {
953                                 suma = update_size(fel.name.substr(3), fel.checked, suma);
954                         } 
955                 }
956         }
957         update_sum(suma);
958         return suma;
959 }
960
961 /* progress bar */
962
963 var _pbar_width = null;
964 var _pbar_warn = 10;    // change color in last 10%
965
966 function pbar_reset() {
967         element_id("mask").style.left = "0px";
968         _pbar_width = element_id("mContainer").offsetWidth - 2;
969         element_id("mask").style.width = _pbar_width + "px";
970         element_id("mask").style.display = "block";
971         element_id("progressIndicator").style.zIndex  = 10;
972         element_id("progressIndicator").innerHTML = "0";
973 }
974
975 function dec2hex(d) {
976         var hch = '0123456789ABCDEF';
977         var a = d % 16;
978         var q = (d - a) / 16;
979         return hch.charAt(q) + hch.charAt(a);
980 }
981
982 function pbar_set(amount, max) {
983         debug('pbar_set('+amount+', '+max+')');
984
985         if (_pbar_width == null) {
986                 var _mc = element_id("mContainer");
987                 if (_pbar_width == null) _pbar_width = parseInt(_mc.offsetWidth ? (_mc.offsetWidth - 2) : 0) || null;
988                 if (_pbar_width == null) _pbar_width = parseInt(_mc.clientWidth ? (_mc.clientWidth + 2) : 0) || null;
989                 if (_pbar_width == null) _pbar_width = 0;
990         }
991
992         var pcnt = Math.floor(amount * 100 / max);
993         var p90 = 100 - _pbar_warn;
994         var pcol = pcnt - p90;
995         if (Math.round(pcnt) <= 100) {
996                 if (pcol < 0) pcol = 0;
997                 var e = element_id("submitBurner");
998                 debug('enable_button');
999                 e.disabled = false;
1000                 var a = e.getAttributeNode('disabled') || null;
1001                 if (a) e.removeAttributeNode(a);
1002         } else {
1003                 debug('disable button');
1004                 pcol = _pbar_warn;
1005                 var e = element_id("submitBurner");
1006                 if (!e.disabled) e.disabled = true;
1007         }
1008         var col_g = Math.floor((_pbar_warn - pcol) * 255 / _pbar_warn);
1009         var col = '#FF' + dec2hex(col_g) + '00';
1010
1011         //debug('pcol: '+pcol+' g:'+col_g+' _pbar_warn:'+ _pbar_warn + ' color: '+col);
1012         element_id("gradient").style.backgroundColor = col;
1013
1014         element_id("progressIndicator").innerHTML = pcnt + '%';
1015         //element_id("progressIndicator").innerHTML = amount;
1016
1017         element_id("mask").style.clip = 'rect(' + Array(
1018                 '0px',
1019                 element_id("mask").offsetWidth + 'px',
1020                 element_id("mask").offsetHeight + 'px',
1021                 Math.round(_pbar_width * amount / max) + 'px'
1022         ).join(' ') + ')';
1023 }
1024
1025 if (!self.body) self.body = new Object();
1026 self.onload = self.document.onload = self.body.onload = function() {
1027         //pbar_reset();
1028         sumiraj();
1029 };
1030
1031 // -->
1032 </script>
1033 <div id="fixedBox">
1034
1035 <input type="hidden" name="totalsize"/>
1036 Size: <input type="text" name="totalsize_kb" size="7" readonly="readonly" style="text-align:right;" value="0" /> kB
1037
1038 <div id="mContainer">
1039         <div id="gradient">&nbsp;</div>
1040         <div id="mask">&nbsp;</div>
1041         <div id="progressIndicator">0%</div>
1042 </div>
1043 <br/>
1044
1045 <div id="volumes">&nbsp;</div>
1046
1047 Note:
1048 <textarea name="note" cols="10" rows="5" id="note"></textarea>
1049
1050 <input type="submit" id="submitBurner" value="Burn selected" name="submitBurner" />
1051
1052 </div>
1053 <!--
1054 <div id="debug" style="float: right; width: 10em; border: 1px #ff0000 solid; background-color: #ffe0e0; -moz-opacity: 0.7;">
1055 no debug output yet
1056 </div>
1057 -->
1058