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