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