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