use DateTime::Duration to correctly convert partial dates in to fields
[BackupPC.git] / lib / BackupPC / Search.pm
1 #!/usr/bin/perl
2 package BackupPC::Search;
3
4 use strict;
5 use BackupPC::CGI::Lib qw(:all);
6 use BackupPC::Attrib qw(:all);
7 use DBI;
8 use DateTime;
9 use vars qw(%In $MyURL);
10 use Time::HiRes qw/time/;
11 use XML::Writer;
12 use IO::File;
13 use Data::Dump qw(dump);
14
15 require Exporter;
16 our @ISA=qw(Exporter);
17 our @EXPORT=qw(unit);
18
19 my $on_page = 100;
20 my $pager_pages = 10;
21
22 my $dbh;
23
24 my $bpc = BackupPC::Lib->new || die;
25 $bpc->ConfigRead('_search_archive');
26 my %Conf = $bpc->Conf();
27
28 sub search_module {
29
30         my $search_module = $Conf{SearchModule} || die "search is disabled";
31         eval "use $search_module";
32         if ( $@ ) {
33                 warn "ERROR: $search_module: $!";
34         } else {
35                 warn "# using $search_module for full-text search";
36         }
37
38         return $search_module->new( %Conf );
39 }
40
41 my $dbh;
42
43 sub get_dbh {
44         $dbh ||= DBI->connect($Conf{SearchDSN}, $Conf{SearchUser}, "", { RaiseError => 1, AutoCommit => 1 } );
45         return $dbh;
46 }
47
48 sub getUnits() {
49         my @ret;
50
51         my $dbh = get_dbh();
52         my $sth = $dbh->prepare(qq{
53                 SELECT
54                         shares.id       as id,
55                         hosts.name || ':' || shares.name as share
56                 FROM shares
57                 JOIN hosts on hostid = hosts.id
58                 ORDER BY share
59         } );
60         $sth->execute();
61         push @ret, { 'id' => '', 'share' => '-'};       # dummy any
62
63         while ( my $row = $sth->fetchrow_hashref() ) {
64                 push @ret, $row;
65         }
66         return @ret;
67 }
68
69 sub epoch_to_iso {
70         my $t = shift || return;
71         my $iso = BackupPC::Lib::timeStamp(undef, $t);
72         $iso =~ s/\s/ /g;
73         return $iso;
74 }
75
76 sub dates_from_form($) {
77         my $param = shift || return;
78
79         sub mk_epoch_date($$) {
80                 my ($name,$suffix) = @_;
81
82                 my $yyyy = $param->{ $name . '_year_'  . $suffix}  || return undef;
83                 my $mm   = $param->{ $name . '_month_' . $suffix};
84                 my $dd   = $param->{ $name . '_day_'   . $suffix};
85
86                 $yyyy =~ s/\D//g;
87                 $mm   =~ s/\D//g;
88                 $dd   =~ s/\D//g;
89
90                 my $dt = new DateTime(
91                         year   => $yyyy,
92                         month  => $mm || 1,
93                         day    => $dd || 1,
94                         hour   => 0,
95                         minute => 0,
96                         second => 0,
97                 );
98                 if ( $suffix eq 'to' && ( ! $mm || ! $dd ) ) {
99                         $dt += DateTime::Duration->new( years  => 1 ) if ! $mm;
100                         $dt += DateTime::Duration->new( months => 1 ) if ! $dd;
101                         $dt -= DateTime::Duration->new( days   => 1 );
102                 }
103
104                 print STDERR "mk_epoch_date($name,$suffix) [$yyyy-$mm-$dd] = " . $dt->ymd . " " . $dt->hms . "\n";
105                 return $dt->epoch || 'NULL';
106         }
107
108         my @ret = (
109                 mk_epoch_date('search_backup', 'from'),
110                 mk_epoch_date('search_backup', 'to'),
111                 mk_epoch_date('search', 'from'),
112                 mk_epoch_date('search', 'to'),
113         );
114
115         return @ret;
116
117 }
118
119
120 sub getWhere($) {
121         my $param = shift || return;
122
123         my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
124
125         my @conditions;
126         push @conditions, qq{ backups.date >= $backup_from } if ($backup_from);
127         push @conditions, qq{ backups.date <= $backup_to } if ($backup_to);
128         push @conditions, qq{ files.date >= $files_from } if ($files_from);
129         push @conditions, qq{ files.date <= $files_to } if ($files_to);
130
131         print STDERR "backup: $backup_from - $backup_to files: $files_from - $files_to cond:" . join(" and ",@conditions);
132
133         push( @conditions, ' files.shareid = ' . $param->{'search_share'} ) if ($param->{'search_share'});
134         push (@conditions, " upper(files.path) LIKE upper('%".$param->{'search_filename'}."%')") if ($param->{'search_filename'});
135
136         if ( $param->{burned} ) {
137                 my $is_what = 'is null';
138                 $is_what = '= 1' if ($param->{burned} eq 'burned');
139                 push @conditions, "archive_burned.part $is_what";
140                 push @conditions, "archive_burned.copy $is_what";
141         }
142
143         return join(" and ", @conditions);
144 }
145
146 my $sort_def = {
147         search => {
148                 default => 'date_a',
149                 sql => {
150                         sname_d => 'shares.name DESC',
151                         sname_a => 'shares.name ASC',
152                         filepath_d => 'files.path DESC',
153                         filepath_a => 'files.path ASC',
154                         backupnum_d => 'files.backupnum DESC',
155                         backupnum_a => 'files.backupnum ASC',
156                         size_d => 'files.size DESC',
157                         size_a => 'files.size ASC',
158                         date_d => 'files.date DESC',
159                         date_a => 'files.date ASC',
160                 },
161         }, burn => {
162                 default => 'date_a',
163                 sql => {
164                         sname_d => 'host DESC, share DESC',
165                         sname_a => 'host ASC, share ASC',
166                         num_d => 'backupnum DESC',
167                         num_a => 'backupnum ASC',
168                         date_d => 'date DESC',
169                         date_a => 'date ASC',
170                         age_d => 'age DESC',
171                         age_a => 'age ASC',
172                         size_d => 'size DESC',
173                         size_a => 'size ASC',
174                         incsize_d => 'inc_size DESC',
175                         incsize_a => 'inc_size ASC',
176                 }
177         }
178 };
179
180 sub getSort($$$) {
181         my ($part,$type, $sort_order) = @_;
182
183         die "unknown part: $part" unless ($sort_def->{$part});
184         die "unknown type: $type" unless ($sort_def->{$part}->{$type});
185
186         $sort_order ||= $sort_def->{$part}->{'default'};
187
188         if (my $ret = $sort_def->{$part}->{$type}->{$sort_order}) {
189                 return $ret;
190         } else {
191                 # fallback to default sort order
192                 return $sort_def->{$part}->{$type}->{ $sort_def->{$part}->{'default'} };
193         }
194 }
195
196 sub getFiles($) {
197         my ($param) = @_;
198
199         my $offset = $param->{'offset'} || 0;
200         $offset *= $on_page;
201
202         my $dbh = get_dbh();
203
204         my $sql_cols = qq{
205                 files.id                        AS fid,
206                 hosts.name                      AS hname,
207                 shares.name                     AS sname,
208                 files.backupnum                 AS backupnum,
209                 files.path                      AS filepath,
210                 files.date                      AS date,
211                 files.type                      AS type,
212                 files.size                      AS size
213         };
214
215         my $sql_from = qq{
216                 FROM files 
217                         INNER JOIN shares       ON files.shareID=shares.ID
218                         INNER JOIN hosts        ON hosts.ID = shares.hostID
219                         INNER JOIN backups      ON backups.num = files.backupnum and backups.hostID = hosts.ID AND backups.shareID = files.shareID
220         };
221
222         my $sql_where;
223         my $where = getWhere($param);
224         $sql_where = " WHERE ". $where if ($where);
225
226         # do we have to add tables for burned media?
227         if ( $param->{burned} ) {
228                 $sql_from .= qq{
229                         LEFT OUTER JOIN archive_backup_parts on backup_id = backups.id
230                         LEFT OUTER JOIN archive_burned on archive_burned.archive_id = archive_id
231                 };
232         }
233
234         my $order = getSort('search', 'sql', $param->{'sort'});
235
236         my $sql_order = qq{
237                 ORDER BY $order
238                 LIMIT $on_page
239                 OFFSET ?
240         };
241
242         my $sql_count = qq{ select count(files.id) $sql_from $sql_where };
243         my $sql_results = qq{ select $sql_cols $sql_from $sql_where $sql_order };
244
245         my $sth = $dbh->prepare($sql_count);
246         $sth->execute();
247         my ($results) = $sth->fetchrow_array();
248
249         $sth = $dbh->prepare($sql_results);
250         $sth->execute( $offset );
251
252         if ($sth->rows != $results) {
253                 my $bug = "$0 BUG: [[ $sql_count ]] = $results while [[ $sql_results ]] = " . $sth->rows;
254                 $bug =~ s/\s+/ /gs;
255                 print STDERR "$bug\n";
256         }
257
258         my @ret;
259       
260         while (my $row = $sth->fetchrow_hashref()) {
261                 push @ret, $row;
262         }
263      
264         $sth->finish();
265         return ($results, \@ret);
266 }
267
268 sub getFilesHyperEstraier($) {
269         my ($param) = @_;
270
271         my $offset = $param->{'offset'} || 0;
272         $offset *= $on_page;
273
274         my $q = $param->{'search_filename'};
275         my $shareid = $param->{'search_share'};
276         my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
277
278         return search_module->search(
279                 $offset, $on_page, $param->{sort},
280                 $q, $shareid, $backup_from, $backup_to, $files_from, $files_to
281         );
282                 
283 }
284
285 sub getGzipName($$$)
286 {
287         my ($host, $share, $backupnum) = @_;
288         my $ret = $Conf{GzipSchema};
289         
290         $share =~ s/\//_/g;
291         $ret =~ s/\\h/$host/ge;
292         $ret =~ s/\\s/$share/ge;
293         $ret =~ s/\\n/$backupnum/ge;
294
295         $ret =~ s/__+/_/g;
296
297         return $ret;
298         
299 }
300
301 sub get_tgz_size_by_name($) {
302         my $name = shift;
303
304         my $tgz = $Conf{GzipTempDir}.'/'.$name;
305         my $size = -1;
306
307         $|=1;
308         if (-f "${tgz}.tar.gz") {
309                 $size = (stat("${tgz}.tar.gz"))[7];
310         } elsif (-d $tgz) {
311                 opendir(my $dir, $tgz) || die "can't opendir $tgz: $!";
312                 my @parts = grep { !/^\./ && !/md5/ && -f "$tgz/$_" } readdir($dir);
313                 $size = 0;
314                 foreach my $part (@parts) {
315                         my $currSize =  (stat("$tgz/$part"))[7]; 
316                         $size += (stat("$tgz/$part"))[7] || die "can't stat $tgz/$part: $!";
317                 }
318
319                 closedir $dir;
320         } else {
321                 return -1;
322         }
323
324         return $size;
325 }
326
327 sub getGzipSizeFromBackupID($) {
328         my ($backupID) = @_;
329         my $dbh = get_dbh();
330         my $sql = q{
331                                 SELECT hosts.name  as host,
332                                            shares.name as share,
333                                            backups.num as backupnum
334                                 FROM hosts, backups, shares
335                                 WHERE shares.id=backups.shareid AND
336                                           hosts.id =backups.hostid AND
337                                           backups.id = ?
338         };
339         my $sth = $dbh->prepare($sql);
340         $sth->execute($backupID);
341         my $row = $sth->fetchrow_hashref();
342
343         return get_tgz_size_by_name(
344                 getGzipName($row->{'host'}, $row->{share}, $row->{backupnum})
345         );
346 }
347
348 sub getGzipSize($$)
349 {
350         my ($hostID, $backupNum) = @_;
351         my $sql;
352         my $dbh = get_dbh();
353         
354         $sql = q{ 
355                                 SELECT hosts.name  as host,
356                                            shares.name as share,
357                                            backups.num as backupnum
358                                 FROM hosts, backups, shares
359                                 WHERE shares.id=backups.shareid AND
360                                           hosts.id =backups.hostid AND
361                                           hosts.id=? AND
362                                           backups.num=?
363                         };
364         my $sth = $dbh->prepare($sql);
365         $sth->execute($hostID, $backupNum);
366
367         my $row = $sth->fetchrow_hashref();
368
369         return get_tgz_size_by_name(
370                 getGzipName($row->{'host'}, $row->{share}, $row->{'backupnum'})
371         );
372 }
373
374
375 sub getBackupsNotBurned($) {
376
377         my $param = shift;
378         my $dbh = get_dbh();
379
380         my $order = getSort('burn', 'sql', $param->{'sort'});
381
382 print STDERR "## sort=". ($param->{'sort'} || 'no sort param') . " burn sql order: $order\n";
383
384         my $sql = qq{
385                 SELECT
386                         p.id,
387                         p.filename,
388                         b.date,
389                         date_part('epoch',now()) - b.date as age,
390                         p.size,
391                         count(ap.*) as scheduled,
392                         count(ab.*) as burned
393                 FROM backup_parts p
394                 JOIN backups b          ON b.id = p.backup_id
395                 LEFT OUTER JOIN archive_parts  ap ON ap.backup_part_id = p.id
396                 LEFT OUTER JOIN archive_burned ab ON ab.archive_id = ap.archive_id
397                 GROUP BY p.id,filename,b.date,age,p.size,p.part_nr
398         };
399
400         $sql .= qq{
401                 HAVING count(ap.*) = 0
402         } unless $param->{scheduled};
403
404         $sql .= qq{
405                 ORDER BY b.date,p.part_nr
406         };
407         my $sth = $dbh->prepare( $sql );
408         my @ret;
409         $sth->execute();
410
411         while ( my $row = $sth->fetchrow_hashref() ) {
412                 $row->{'age'} = sprintf("%0.1f", ( $row->{'age'} / 86400 ) );
413                 #$row->{'age'} = sprintf("%0.1f", ( (time() - $row->{'date'}) / 86400 ) );
414                 push @ret, $row;
415         }
416       
417         return @ret;
418 }
419
420 sub displayBackupsGrid($) {
421
422         my $param = shift;
423
424         my $max_archive_size = $Conf{ArchiveMediaSize} || die "no ArchiveMediaSize";
425
426         my $retHTML .= qq|
427                 <form id="forma" method="POST" action="$MyURL?action=burn">
428
429 <script type="text/javascript">
430 var media_size = $max_archive_size ;
431 </script>
432
433         |;
434
435         { local $/ = undef; $retHTML .= <DATA> }
436
437         $retHTML .= q{
438                         <input type="hidden" value="burn" name="action">
439                         <input type="hidden" value="results" name="search_results">
440                         <table style="fview" border="0" cellspacing="0" cellpadding="2">
441                         <tr class="tableheader">
442                         <td class="tableheader">
443                                 <input type="checkbox" name="allFiles" id="allFiles" onClick="checkAll('allFiles');">
444                         </td>
445         } .
446                 sort_header($param, 'Filename', 'filename', 'left') .
447                 sort_header($param, 'Date', 'date', 'center') .
448                 sort_header($param, 'Age/days', 'age', 'center') .
449                 sort_header($param, 'Size', 'size', 'center') .
450         qq{
451                         <td align="center" title="scheduled">sc</td>
452                         <td align="center" title="burned">bu</td>
453                 </tr>
454         };
455
456         my @color = (' bgcolor="#e0e0e0"', '');
457
458         my $i = 1;
459 #       my $img_url = $Conf{CgiImageDirURL};
460
461         foreach my $backup ( getBackupsNotBurned($param) ) {
462
463                 $retHTML .= join(''
464                         ,'<tr',$color[$i++%2],'>'
465                         ,'<td class="fview">'
466                                 ,'<input type="checkbox" name="fcb',$backup->{id},'" value="',$backup->{id},'" onClick="sumiraj(this);">'
467                                 ,'<input type="hidden" id="fss',$backup->{id},'" value="',$backup->{size},'">'
468                         ,'</td>'
469                         ,'<td align="left">', $backup->{'filename'}, '</td>'
470                         ,'<td align="center">', epoch_to_iso( $backup->{'date'} ), '</td>'
471                         ,'<td align="center">', $backup->{'age'}, '</td>'
472                         ,'<td align="right">', unit($backup->{'size'}), '</td>'
473                         ,'<td align="center">', $backup->{scheduled}, '</td>'
474                         ,'<td align="center">', $backup->{burned}, '</td>'
475                         ,"</tr>\n"
476                 );
477         }
478
479         $retHTML .= "</table>";
480
481         $retHTML .= q{
482 <input type=submit name="scheduled" value="Show scheduled parts">
483         } unless $param->{scheduled};
484
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                                 archive.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_parts on archive.id = archive_backup_parts.archive_id
614                         inner join backups on backups.id = archive_backup_parts.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 archive.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