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