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