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