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