add getElementById cache to speed up things, make floating div for total
[BackupPC.git] / lib / BackupPC / SearchLib.pm
1 #!/usr/bin/perl
2 package BackupPC::SearchLib;
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
12 my $on_page = 100;
13 my $pager_pages = 10;
14
15 my $dsn = $Conf{SearchDSN};
16 my $db_user = $Conf{SearchUser} || '';
17
18 my $hest_index_path = $Conf{HyperEstraierIndex};
19
20 my $dbh;
21
22 sub get_dbh {
23         $dbh ||= DBI->connect($dsn, $db_user, "", { RaiseError => 1, AutoCommit => 1 } );
24         return $dbh;
25 }
26
27 sub getUnits() {
28         my @ret;
29
30         my $dbh = get_dbh();
31         my $sth = $dbh->prepare(qq{
32                 SELECT
33                         shares.id       as id,
34                         hosts.name || ':' || shares.name as share
35                 FROM shares
36                 JOIN hosts on hostid = hosts.id
37                 ORDER BY share
38         } );
39         $sth->execute();
40         push @ret, { 'id' => '', 'share' => '-'};       # dummy any
41
42         while ( my $row = $sth->fetchrow_hashref() ) {
43                 push @ret, $row;
44         }
45         return @ret;
46 }
47
48 sub epoch_to_iso {
49         my $t = shift || return;
50         my $iso = BackupPC::Lib::timeStamp(undef, $t);
51         $iso =~ s/\s/ /g;
52         return $iso;
53 }
54
55 sub dates_from_form($) {
56         my $param = shift || return;
57
58         sub mk_epoch_date($$) {
59                 my ($name,$suffix) = @_;
60
61                 my $yyyy = $param->{ $name . '_year_' . $suffix} || return undef;
62                 my $mm .= $param->{ $name . '_month_' . $suffix} ||
63                         ( $suffix eq 'from' ? 1 : 12);
64                 my $dd .= $param->{ $name . '_day_' . $suffix} ||
65                         ( $suffix eq 'from' ? 1 : 31);
66
67                 $yyyy =~ s/\D//g;
68                 $mm =~ s/\D//g;
69                 $dd =~ s/\D//g;
70
71                 my $dt = new DateTime(
72                         year => $yyyy,
73                         month => $mm,
74                         day => $dd
75                 );
76                 print STDERR "mk_epoch_date($name,$suffix) [$yyyy-$mm-$dd] = " . $dt->ymd . " " . $dt->hms . "\n";
77                 return $dt->epoch || 'NULL';
78         }
79
80         my @ret = (
81                 mk_epoch_date('search_backup', 'from'),
82                 mk_epoch_date('search_backup', 'to'),
83                 mk_epoch_date('search', 'from'),
84                 mk_epoch_date('search', 'to'),
85         );
86
87         return @ret;
88
89 }
90
91
92 sub getWhere($) {
93         my $param = shift || return;
94
95         my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
96
97         my @conditions;
98         push @conditions, qq{ backups.date >= $backup_from } if ($backup_from);
99         push @conditions, qq{ backups.date <= $backup_to } if ($backup_to);
100         push @conditions, qq{ files.date >= $files_from } if ($files_from);
101         push @conditions, qq{ files.date <= $files_to } if ($files_to);
102
103         print STDERR "backup: $backup_from - $backup_to files: $files_from - $files_to cond:" . join(" | ",@conditions);
104
105         push( @conditions, ' files.shareid = ' . $param->{'search_share'} ) if ($param->{'search_share'});
106         push (@conditions, " upper(files.path) LIKE upper('%".$param->{'search_filename'}."%')") if ($param->{'search_filename'});
107
108         return join(" and ", @conditions);
109 }
110
111
112 sub getFiles($) {
113         my ($param) = @_;
114
115         my $offset = $param->{'offset'} || 0;
116         $offset *= $on_page;
117
118         my $dbh = get_dbh();
119
120         my $sql_cols = qq{
121                 files.id                        AS fid,
122                 hosts.name                      AS hname,
123                 shares.name                     AS sname,
124                 files.backupnum                 AS backupnum,
125                 files.path                      AS filepath,
126                 files.date                      AS date,
127                 files.type                      AS type,
128                 files.size                      AS size
129         };
130
131         my $sql_from = qq{
132                 FROM files 
133                         INNER JOIN shares       ON files.shareID=shares.ID
134                         INNER JOIN hosts        ON hosts.ID = shares.hostID
135                         INNER JOIN backups      ON backups.num = files.backupnum and backups.hostID = hosts.ID AND backups.shareID = files.shareID
136         };
137
138         my $sql_where;
139         my $where = getWhere($param);
140         $sql_where = " WHERE ". $where if ($where);
141
142         my $sql_order = qq{
143                 ORDER BY files.date
144                 LIMIT $on_page
145                 OFFSET ?
146         };
147
148         my $sql_count = qq{ select count(files.id) $sql_from $sql_where };
149         my $sql_results = qq{ select $sql_cols $sql_from $sql_where $sql_order };
150
151         my $sth = $dbh->prepare($sql_count);
152         $sth->execute();
153         my ($results) = $sth->fetchrow_array();
154
155         $sth = $dbh->prepare($sql_results);
156         $sth->execute( $offset );
157
158         if ($sth->rows != $results) {
159                 my $bug = "$0 BUG: [[ $sql_count ]] = $results while [[ $sql_results ]] = " . $sth->rows;
160                 $bug =~ s/\s+/ /gs;
161                 print STDERR "$bug\n";
162         }
163
164         my @ret;
165       
166         while (my $row = $sth->fetchrow_hashref()) {
167                 push @ret, $row;
168         }
169      
170         $sth->finish();
171         return ($results, \@ret);
172 }
173
174 sub getHyperEstraier_url($) {
175         my ($use_hest) = @_;
176
177         return unless $use_hest;
178
179         use HyperEstraier;
180         my ($index_path, $index_node_url);
181
182         if ($use_hest =~ m#^http://#) {
183                 $index_node_url = $use_hest;
184         } else {
185                 $index_path = $TopDir . '/' . $index_path;
186                 $index_path =~ s#//#/#g;
187         }
188         return ($index_path, $index_node_url);
189 }
190
191 sub getFilesHyperEstraier($) {
192         my ($param) = @_;
193
194         my $offset = $param->{'offset'} || 0;
195         $offset *= $on_page;
196
197         die "no index_path?" unless ($hest_index_path);
198
199         use HyperEstraier;
200
201         my ($index_path, $index_node_url) = getHyperEstraier_url($hest_index_path);
202
203         # open the database
204         my $db;
205         if ($index_path) {
206                 $db = HyperEstraier::Database->new();
207                 $db->open($index_path, $HyperEstraier::ESTDBREADER);
208         } elsif ($index_node_url) {
209                 $db ||= HyperEstraier::Node->new($index_node_url);
210                 $db->set_auth('admin', 'admin');
211         } else {
212                 die "BUG: unimplemented";
213         }
214
215         # create a search condition object
216         my $cond = HyperEstraier::Condition->new();
217
218         my $q = $param->{'search_filename'};
219         my $shareid = $param->{'search_share'};
220
221         if (length($q) > 0) {
222                 # exact match
223                 $cond->add_attr("filepath ISTRINC $q");
224
225                 $q =~ s/(.)/$1 /g;
226                 # set the search phrase to the search condition object
227                 $cond->set_phrase($q);
228         }
229
230         my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
231
232         $cond->add_attr("backup_date NUMGE $backup_from") if ($backup_from);
233         $cond->add_attr("backup_date NUMLE $backup_to") if ($backup_to);
234
235         $cond->add_attr("date NUMGE $files_from") if ($files_from);
236         $cond->add_attr("date NUMLE $files_to") if ($files_to);
237
238         $cond->add_attr("shareid NUMEQ $shareid") if ($shareid);
239
240 #       $cond->set_max( $offset + $on_page );
241         $cond->set_options( $HyperEstraier::Condition::SURE );
242         $cond->set_order( 'date NUMA' );
243
244         # get the result of search
245         my @res;
246         my ($result, $hits);
247
248         if ($index_path) {
249                 $result = $db->search($cond, 0);
250                 $hits = $result->size;
251         } elsif ($index_node_url) {
252                 $result = $db->search($cond, 0);
253                 $hits = $result->doc_num;
254         } else {
255                 die "BUG: unimplemented";
256         }
257
258         # for each document in result
259         for my $i ($offset .. ($offset + $on_page - 1)) {
260                 last if ($i >= $hits);
261
262                 my $doc;
263                 if ($index_path) {
264                         my $id = $result->get($i);
265                         $doc = $db->get_doc($id, 0);
266                 } elsif ($index_node_url) {
267                         $doc = $result->get_doc($i);
268                 } else {
269                         die "BUG: unimplemented";
270                 }
271
272                 my $row;
273                 foreach my $c (qw/fid hname sname backupnum fiilename filepath date type size/) {
274                         $row->{$c} = $doc->attr($c);
275                 }
276                 push @res, $row;
277         }
278
279         return ($hits, \@res);
280 }
281
282 sub getGzipName($$$)
283 {
284         my ($host, $share, $backupnum) = @_;
285         my $ret = $Conf{GzipSchema};
286         
287         $share =~ s/\//_/g;
288         $ret =~ s/\\h/$host/ge;
289         $ret =~ s/\\s/$share/ge;
290         $ret =~ s/\\n/$backupnum/ge;
291         
292         return $ret;
293         
294 }
295
296 sub getBackupsNotBurned() {
297
298         my $dbh = get_dbh();
299
300         my $sql = q{
301                 SELECT 
302                         backups.hostID AS hostID,
303                         hosts.name AS host,
304                         shares.name AS share,
305                         backups.id AS backupnum,
306                         backups.type AS type,
307                         backups.date AS date,
308                         backups.size AS size
309                 FROM backups 
310                 INNER JOIN shares       ON backups.shareID=shares.ID
311                 INNER JOIN hosts        ON backups.hostID = hosts.ID
312                 LEFT OUTER JOIN archive_backup ON archive_backup.backup_id = backups.id AND archive_backup.backup_id IS NULL
313                 WHERE backups.size > 0
314                 GROUP BY
315                         backups.hostID,
316                         hosts.name,
317                         shares.name,
318                         backups.num,
319                         backups.shareid,
320                         backups.id,
321                         backups.type,
322                         backups.date,
323                         backups.size
324                 ORDER BY backups.date
325         };
326         my $sth = $dbh->prepare( $sql );
327         my @ret;
328         $sth->execute();
329
330         while ( my $row = $sth->fetchrow_hashref() ) {
331                 $row->{'age'} = sprintf("%0.1f", ( (time() - $row->{'date'}) / 86400 ) );
332                 $row->{'size'} = sprintf("%0.2f", $row->{'size'} / 1024 / 1024);
333                 my (undef,undef,undef,undef,undef,undef,undef,$fs_size,undef,undef,undef,undef,undef) = 
334                         stat( $Conf{InstallDir}.'/'.$Conf{GzipTempDir}.'/'.
335                                 getGzipName($row->{'host'}, $row->{share}, $row->{'backupnum'}));
336                 $row->{'fs_size'} = $fs_size;
337                 push @ret, $row;
338         }
339       
340         return @ret;      
341 }
342
343 sub displayBackupsGrid() {
344
345         my $retHTML .= q{
346                 <form id="forma" method="POST" action=};
347                 $retHTML .= "\"".$MyURL."\"";
348                 $retHTML .= q{?action=burn>
349         };
350
351         $retHTML .= <<EOF3;
352 <script language="javascript" type="text/javascript">
353 <!--
354
355 var debug_div = null;
356
357 function debug(msg) {
358 //      return; // Disable debugging
359
360         if (! debug_div) debug_div = document.getElementById('debug');
361
362         // this will create debug div if it doesn't exist.
363         if (! debug_div) {
364                 debug_div = document.createElement('div');
365                 if (document.body) document.body.appendChild(debug_div);
366                 else debug_div = null;
367         }
368         if (debug_div) {
369                 debug_div.appendChild(document.createTextNode(msg));
370                 debug_div.appendChild(document.createElement("br"));
371         }
372 }
373
374
375 var element_id_cache = Array();
376
377 function element_id(name,element) {
378         if (! element_id_cache[name]) {
379                 element_id_cache[name] = self.document.getElementById(name);
380         }
381         return element_id_cache[name];
382 }
383
384 function checkAll(location) {
385         var len = element_id('forma').elements.length;
386         var check_all = element_id('allFiles');
387
388         for (var i = 0; i < len; i++) {
389
390                 var e = element_id('forma').elements[i];
391                 if ((e.checked || !e.checked) && e.name != \'all\') {
392                         if (check_all.checked) {
393                                 e.checked = true;
394                         } else {
395                                 e.checked = false;
396                         }
397                 }
398         }
399
400         sumiraj();
401 }
402     
403 function sumiraj(e) {
404         var suma = parseInt(element_id('forma').totalsize.value) || 0;
405         var len = element_id('forma').elements.length;
406         if (e) {
407                 var size = parseInt( element_id("fss" + e.name.substr(3)).value );
408                 if (e.checked) {
409                         suma += size;
410                 } else {
411                         suma -= size;
412                 }
413         } else {
414                 suma = 0;
415                 for (var i = 0; i < len; i++) {
416                         var e = element_id('forma').elements[i];
417                         if (e.name != \'all\' && e.checked && e.name.substr(0,3) == 'fcb') {
418                                 var el = element_id("fss" + e.name.substr(3));
419                                 if (el && el.value) suma += parseInt(el.value) || 0;
420                         }
421                 }
422         }
423         element_id('forma').totalsize.value = suma;
424         debug('total size: '+suma);
425         return suma;
426 }
427
428 if (!self.body) self.body = new Object();
429 self.onload = self.document.onload = self.body.onload = function() {
430         sumiraj();
431 }
432
433 //-->
434 </script>
435 <div id="debug" style="position: absolute; top: 0; right: 0; width: 10em; border: 1px #ff0000 solid; background-color: #ffe0e0; -moz-opacity: 0.7;">
436 Total size:
437 <input type="text" name="totalsize" size="8">
438 <br/>
439 Note:
440 <textarea name="note" cols="10" rows="5">
441 </textarea>
442 <br/>
443 <input type="submit" value="Burn selected backups" name="submitBurner">
444 </div>
445 EOF3
446         $retHTML .= q{
447                         <input type="hidden" value="burn" name="action">
448                         <input type="hidden" value="results" name="search_results">
449                         <table style="fview" border="0" cellspacing="0" cellpadding="2">
450                         <tr class="tableheader">
451                         <td class="tableheader">
452                                 <input type="checkbox" name="allFiles" id="allFiles" onClick="checkAll('allFiles');">
453                         </td>
454                         <td align="center">Share</td>
455                         <td align="center">Backup no</td>
456                         <td align="center">Type</td>
457                         <td align="center">date</td>
458                         <td align="center">age/days</td>
459                         <td align="center">size/MB</td>
460                         <td align="center">gzip size</td>
461                         </tr>
462
463         };
464
465         my @color = (' bgcolor="#e0e0e0"', '');
466
467         my $i = 0;
468         my $host = '';
469
470         foreach my $backup ( getBackupsNotBurned() ) {
471
472                 if ($host ne $backup->{'host'}) {
473                         $i++;
474                         $host = $backup->{'host'};
475                 }
476                 my $ftype = "";
477
478                 $retHTML .=
479                         '<tr' . $color[$i %2 ] . '>
480                         <td class="fview">';
481                 # FIXME
482                 #$backup->{'fs_size'} = int($backup->{'size'} * 1024);
483                 if (($backup->{'fs_size'} || 0) > 0) {
484                         $retHTML .= '
485                         <input type="checkbox" name="fcb' .
486                         $backup->{'hostid'}.'_'.$backup->{'backupnum'} . 
487                         '" value="' . $backup->{'hostid'}.'_'.$backup->{'backupnum'} .
488                         '" onClick="sumiraj(this);">';
489                 }
490                 $retHTML .=
491                         '</td>' .
492                         '<td align="right">' . $backup->{'host'} . ':' . $backup->{'share'} . '</td>' .
493                         '<td align="center">' . $backup->{'backupnum'} . '</td>' .
494                         '<td align="center">' . $backup->{'type'} . '</td>' .
495                         '<td align="center">' . epoch_to_iso( $backup->{'date'} ) . '</td>' .
496                         '<td align="center">' . $backup->{'age'} . '</td>' .
497                         '<td align="right">' . $backup->{'size'} . '</td>' .
498                         '<td align="right">' . $backup->{'fs_size'} .
499                         '<input type="hidden" iD="fss'.$backup->{'hostid'}.'_'.$backup->{'backupnum'} . '" value="'. $backup->{'fs_size'} .'"></td>' .
500
501                         "</tr>\n";
502         }
503
504         $retHTML .= "</table>";
505         $retHTML .= "</form>";
506       
507         return $retHTML;
508 }      
509
510 sub displayGrid($) {
511         my ($param) = @_;
512
513         my $offset = $param->{'offset'};
514         my $hilite = $param->{'search_filename'};
515
516         my $retHTML = "";
517  
518         my $start_t = time();
519
520         my ($results, $files);
521         if ($param->{'use_hest'} && length($hilite) > 0) {
522                 ($results, $files) = getFilesHyperEstraier($param);
523         } else {
524                 ($results, $files) = getFiles($param);
525         }
526
527         my $dur_t = time() - $start_t;
528         my $dur = sprintf("%0.4fs", $dur_t);
529
530         my ($from, $to) = (($offset * $on_page) + 1, ($offset * $on_page) + $on_page);
531
532         if ($results <= 0) {
533                 $retHTML .= qq{
534                         <p style="color: red;">No results found...</p>
535                 };
536                 return $retHTML;
537         } else {
538                 # DEBUG
539                 #use Data::Dumper;
540                 #$retHTML .= '<pre>' . Dumper($files) . '</pre>';
541         }
542
543
544         $retHTML .= qq{
545         <div>
546         Found <b>$results files</b> showing <b>$from - $to</b> (took $dur)
547         </div>
548         <table style="fview" width="100%" border="0" cellpadding="2" cellspacing="0">
549                 <tr class="fviewheader"> 
550                 <td></td>
551                 <td align="center">Share</td>
552                 <td align="center">Type and Name</td>
553                 <td align="center">#</td>
554                 <td align="center">Size</td>
555                 <td align="center">Date</td>
556                 <td align="center">Media</td>
557                 </tr>
558         };
559
560         my $file;
561
562         sub hilite_html($$) {
563                 my ($html, $search) = @_;
564                 $html =~ s#($search)#<b>$1</b>#gis;
565                 return $html;
566         }
567
568         sub restore_link($$$$$$) {
569                 my $type = shift;
570                 my $action = 'RestoreFile';
571                 $action = 'browse' if (lc($type) eq 'dir');
572                 return sprintf(qq{<a href="?action=%s&host=%s&num=%d&share=%s&dir=%s">%s</a>}, $action, @_);
573         }
574
575         my $i = $offset * $on_page;
576
577         foreach $file (@{ $files }) {
578                 $i++;
579
580                 my $typeStr  = BackupPC::Attrib::fileType2Text(undef, $file->{'type'});
581                 $retHTML .= qq{<tr class="fviewborder">};
582
583                 $retHTML .= qq{<td class="fviewborder">$i</td>};
584
585                 $retHTML .=
586                         qq{<td class="fviewborder" align="right">} . $file->{'hname'} . ':' . $file->{'sname'} . qq{</td>} .
587                         qq{<td class="fviewborder"><img src="$Conf{CgiImageDirURL}/icon-$typeStr.gif" alt="$typeStr" align="middle">&nbsp;} . hilite_html( $file->{'filepath'}, $hilite ) . qq{</td>} .
588                         qq{<td class="fviewborder" align="center">} . restore_link( $typeStr, ${EscURI( $file->{'hname'} )}, $file->{'backupnum'}, ${EscURI( $file->{'sname'})}, ${EscURI( $file->{'filepath'} )}, $file->{'backupnum'} ) . qq{</td>} .
589                         qq{<td class="fviewborder" align="right">} . $file->{'size'} . qq{</td>} .
590                         qq{<td class="fviewborder">} . epoch_to_iso( $file->{'date'} ) . qq{</td>} .
591                         qq{<td class="fviewborder">} . '?' . qq{</td>};
592
593                 $retHTML .= "</tr>";
594         }
595         $retHTML .= "</table>";
596
597         # all variables which has to be transfered
598         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/) {
599                 $retHTML .= qq{<INPUT TYPE="hidden" NAME="$n" VALUE="$In{$n}">\n};
600         }
601
602         my $del = '';
603         my $max_page = int( $results / $on_page );
604         my $page = 0;
605
606         sub page_link($$$) {
607                 my ($param,$page,$display) = @_;
608
609                 $param->{'offset'} = $page;
610
611                 my $html = '<a href = "' . $MyURL;
612                 my $del = '?';
613                 foreach my $k (keys %{ $param }) {
614                         if ($param->{$k}) {
615                                 $html .= $del . $k . '=' . ${EscURI( $param->{$k} )};
616                                 $del = '&';
617                         }
618                 }
619                 $html .= '">' . $display . '</a>';
620         }
621
622         $retHTML .= '<div style="text-align: center;">';
623
624         if ($offset > 0) {
625                 $retHTML .= page_link($param, $offset - 1, '&lt;&lt;') . ' ';
626         }
627
628         while ($page <= $max_page) {
629                 if ($page == $offset) {
630                         $retHTML .= $del . '<b>' . ($page + 1) . '</b>';
631                 } else {
632                         $retHTML .= $del . page_link($param, $page, $page + 1);
633                 }
634
635                 if ($page < $offset - $pager_pages && $page != 0) {
636                         $retHTML .= " ... ";
637                         $page = $offset - $pager_pages;
638                         $del = '';
639                 } elsif ($page > $offset + $pager_pages && $page != $max_page) {
640                         $retHTML .= " ... ";
641                         $page = $max_page;
642                         $del = '';
643                 } else {
644                         $del = ' | ';
645                         $page++;
646                 }
647         }
648
649         if ($offset < $max_page) {
650                 $retHTML .= ' ' . page_link($param, $offset + 1, '&gt;&gt;');
651         }
652
653         $retHTML .= "</div>";
654
655         return $retHTML;
656 }
657
658 1;