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