first cut at implemeting HyperEstraier search,
[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 $index_path = $Conf{HyperEstraierIndex};
19 if ($index_path) {
20         $index_path = $TopDir . '/' . $index_path;
21         $index_path =~ s#//#/#g;
22 }
23
24 my $dbh;
25
26 sub get_dbh {
27         $dbh ||= DBI->connect($dsn, $db_user, "", { RaiseError => 1, AutoCommit => 1 } );
28         return $dbh;
29 }
30
31 sub getUnits() {
32         my @ret;
33
34         my $dbh = get_dbh();
35         my $sth = $dbh->prepare(qq{
36                 SELECT
37                         shares.id       as id,
38                         hosts.name || ':' || shares.name as share
39                 FROM shares
40                 JOIN hosts on hostid = hosts.id
41                 ORDER BY share
42         } );
43         $sth->execute();
44         push @ret, { 'id' => '', 'share' => '-'};       # dummy any
45
46         while ( my $row = $sth->fetchrow_hashref() ) {
47                 push @ret, $row;
48         }
49         return @ret;
50 }
51
52 sub epoch_to_iso {
53         my $t = shift || return;
54         my $iso = BackupPC::Lib::timeStamp(undef, $t);
55         $iso =~ s/\s/ /g;
56         return $iso;
57 }
58
59 sub dates_from_form($) {
60         my $param = shift || return;
61
62         sub mk_epoch_date($$) {
63                 my ($name,$suffix) = @_;
64
65                 my $yyyy = $param->{ $name . '_year_' . $suffix} || return;
66                 my $mm .= $param->{ $name . '_month_' . $suffix} ||
67                         ( $suffix eq 'from' ? 1 : 12);
68                 my $dd .= $param->{ $name . '_day_' . $suffix} ||
69                         ( $suffix eq 'from' ? 1 : 31);
70                 my $dt = new DateTime(
71                         year => $yyyy,
72                         month => $mm,
73                         day => $dd
74                 );
75                 return $dt->epoch || 'NULL';
76         }
77
78         return (
79                 mk_epoch_date('search_backup', 'from'),
80                 mk_epoch_date('search_backup', 'to'),
81                 mk_epoch_date('search', 'from'),
82                 mk_epoch_date('search', 'to'),
83         );
84 }
85
86
87 sub getWhere($) {
88         my $param = shift || return;
89
90         my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
91
92         my @conditions;
93         push @conditions, qq{ backups.date >= $backup_from } if ($backup_from);
94         push @conditions, qq{ backups.date <= $backup_to } if ($backup_to);
95         push @conditions, qq{ files.date >= $files_from } if ($files_from);
96         push @conditions, qq{ files.date <= $files_to } if ($files_to);
97
98         print STDERR "backup: $backup_from - $backup_to files: $files_from - $files_to cond:" . join(" | ",@conditions);
99
100         push( @conditions, ' files.shareid = ' . $param->{'search_share'} ) if ($param->{'search_share'});
101         push (@conditions, " upper(files.path) LIKE upper('%".$param->{'search_filename'}."%')") if ($param->{'search_filename'});
102
103         return join(" and ", @conditions);
104 }
105
106
107 sub getFiles($$) {
108         my ($param, $offset) = @_;
109
110         my $dbh = get_dbh();
111
112         my $sql_cols = qq{
113                 files.id                        AS fid,
114                 hosts.name                      AS hname,
115                 shares.name                     AS sname,
116                 -- shares.share                 AS sharename,
117                 files.backupnum                 AS backupnum,
118                 -- files.name                   AS filename,
119                 files.path                      AS filepath,
120                 files.date                      AS date,
121                 files.type                      AS type,
122                 files.size                      AS size,
123                 -- dvds.name                    AS dvd
124                 null                            AS dvd
125         };
126
127         my $sql_from = qq{
128                 FROM files 
129                         INNER JOIN shares       ON files.shareID=shares.ID
130                         INNER JOIN hosts        ON hosts.ID = shares.hostID
131                         INNER JOIN backups      ON backups.num = files.backupnum and backups.hostID = hosts.ID AND backups.shareID = shares.ID
132         };
133
134         my $sql_dvd_from = qq{
135                         -- LEFT  JOIN dvds              ON dvds.ID = files.dvdid
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_dvd_from $sql_where $sql_order };
150
151         $offset ||= 0;
152         $offset = ($offset * $on_page);
153
154         my $sth = $dbh->prepare($sql_count);
155         $sth->execute();
156         my ($results) = $sth->fetchrow_array();
157
158         $sth = $dbh->prepare($sql_results);
159         $sth->execute( $offset );
160
161         if ($sth->rows != $results) {
162                 my $bug = "$0 BUG: [[ $sql_count ]] = $results while [[ $sql_results ]] = " . $sth->rows;
163                 $bug =~ s/\s+/ /gs;
164                 print STDERR "$bug\n";
165         }
166
167         my @ret;
168       
169         while (my $row = $sth->fetchrow_hashref()) {
170                 push @ret, $row;
171         }
172      
173         $sth->finish();
174         return ($results, \@ret);
175 }
176
177 sub getFilesHyperEstraier($$) {
178         my ($param, $offset) = @_;
179
180         die "no index_path?" unless ($index_path);
181
182         use HyperEstraier;
183
184         # open the database
185         my $db = HyperEstraier::Database->new();
186         $db->open($index_path, $HyperEstraier::ESTDBREADER);
187
188         # create a search condition object
189         my $cond = HyperEstraier::Condition->new();
190
191         my $q = $param->{'search_filename'};
192         my $shareid = $param->{'search_share'};
193
194         if ($q) {
195                 $q =~ s/(.)/$1 /g;
196
197                 # set the search phrase to the search condition object
198                 $cond->set_phrase($q);
199
200                 my ($backup_from, $backup_to, $files_from, $files_to) = dates_from_form($param);
201
202                 $cond->add_attr("backup_date NUMGE $backup_from") if ($backup_from);
203                 $cond->add_attr("backup_date NUMLE $backup_to") if ($backup_to);
204
205                 $cond->add_attr("date NUMGE $files_from") if ($files_from);
206                 $cond->add_attr("date NUMLE $files_to") if ($files_to);
207
208                 $cond->add_attr("shareid NUMEQ $shareid") if ($shareid);
209         }
210
211         $offset ||= 0;
212         $offset = ($offset * $on_page);
213
214 #       $cond->set_max( $offset + $on_page );
215         $cond->set_options( $HyperEstraier::Condition::SURE );
216         $cond->set_order( 'date NUMA' );
217
218         # get the result of search
219         my $result = $db->search($cond, 0);
220         $result->get(0);
221
222         my @res;
223         my $hits = $result->size;
224
225         # for each document in result
226         for my $i ($offset .. $result->size-1) {
227                 my $id = $result->get($i);
228                 my $doc = $db->get_doc($id, 0);
229
230                 my $row;
231                 foreach my $c (qw/fid hname sname backupnum fiilename filepath date type size/) {
232                         $row->{$c} = $doc->attr($c);
233                 }
234                 push @res, $row;
235         }
236
237         return ($hits, \@res);
238 }
239
240 sub getBackupsNotBurned() {
241
242         my $dbh = get_dbh();
243         my $sql = q{ 
244         SELECT
245                 backups.hostID          AS hostid,
246                 min(hosts.name)         AS host,
247                 backups.num             AS backupnum,
248                 min(backups.type)       AS type,
249                 min(backups.date)       AS date,
250                 min(backups.size)       AS size
251         FROM files 
252                 INNER JOIN shares       ON files.shareID=shares.ID
253                 INNER JOIN hosts        ON hosts.ID = shares.hostID
254                 INNER JOIN backups      ON backups.num = files.backupnum and backups.hostID = hosts.ID AND backups.shareID = shares.ID
255         WHERE 
256                 files.dvdid     IS NULL
257         GROUP BY 
258                 backups.hostID, backups.num
259         ORDER BY min(backups.date)
260         };
261         my $sth = $dbh->prepare( $sql );
262         my @ret;
263         $sth->execute();
264
265         while ( my $row = $sth->fetchrow_hashref() ) {
266                 $row->{'age'} = sprintf("%0.1f", ( (time() - $row->{'date'}) / 86400 ) );
267                 $row->{'size'} = sprintf("%0.2f", $row->{'size'} / 1024 / 1024);
268                 push @ret, $row;
269         }
270       
271         return @ret;      
272 }
273
274 sub displayBackupsGrid()
275   {
276       my $retHTML = "";
277       my $addForm = 1;
278       
279       if ($addForm) {
280
281             $retHTML .= <<EOF3;
282 <script language="javascript" type="text/javascript">
283 <!--
284
285     function checkAll(location)
286     {
287       for (var i=0;i<document.forma.elements.length;i++)
288       {
289         var e = document.forma.elements[i];
290         if ((e.checked || !e.checked) && e.name != \'all\') {
291             if (eval("document.forma."+location+".checked")) {
292                 e.checked = true;
293             } else {
294                 e.checked = false;
295             }
296         }
297       }
298     }
299 //-->
300 </script>      
301 EOF3
302               $retHTML .= q{<form name="forma" method="GET" action="}."$MyURL"."?action=burn\"";
303               $retHTML.= q{<input type="hidden" value="burn" name="action">};
304               $retHTML .= q{<input type="hidden" value="results" name="search_results">};
305         }
306         $retHTML .= qq{
307                 <table style="fview" border="1" cellspacing="0" cellpadding="3">
308                 <tr class="tableheader">
309         };
310
311         if ($addForm) {
312             $retHTML .= "<td class=\"tableheader\"><input type=\"checkbox\" name=\"allFiles\" onClick=\"checkAll('allFiles');\"></td>";
313         }
314         $retHTML .=  qq{
315                 <td align="center">Host</td>
316                 <td align="center">Backup no</td>
317                 <td align="center">Type</td>
318                 <td align="center">date</td>
319                 <td align="center">age/days</td>
320                 <td align="center">size/MB</td>
321                 </tr>
322         };
323
324         my @backups = getBackupsNotBurned();
325         my $backup;
326
327         if ($addForm) {
328                 $retHTML .= qq{
329                         <tr><td colspan=7 style="tableheader">
330                         <input type="submit" value="Burn selected backups on medium" name="submitBurner">
331                         </td></tr>
332                 };
333         }
334
335         foreach $backup(@backups) {
336
337                 my $ftype = "";
338             
339                 $retHTML .= "<tr>";
340                 if ($addForm) {
341                         $retHTML .= '<td class="fview"><input type="checkbox" name="fcb' .
342                                 $backup->{'hostid'}.'_'.$backup->{'backupnum'} . 
343                                 '" value="' . $backup->{'hostid'}.'_'.$backup->{'backupnum'} .
344                                 '"></td>';
345                 }           
346             
347                 $retHTML .= '<td class="fviewborder">' . $backup->{'host'} . '</td>' .
348                         '<td class="fviewborder">' . $backup->{'backupnum'} . '</td>' .
349                         '<td class="fviewborder">' . $backup->{'type'} . '</td>' .
350                         '<td class="fviewborder">' . epoch_to_iso( $backup->{'date'} ) . '</td>' .
351                         '<td class="fviewborder">' . $backup->{'age'} . '</td>' .
352                         '<td class="fviewborder">' . $backup->{'size'} . '</td>' .
353                         '</tr>';
354         }
355
356         $retHTML .= "</table>";
357
358         if ($addForm) {
359                 $retHTML .= "</form>";
360         }
361       
362         return $retHTML;
363 }      
364
365 sub displayGrid($) {
366         my ($param) = @_;
367
368         my $offset = $param->{'offset'};
369         my $hilite = $param->{'search_filename'};
370
371         my $retHTML = "";
372  
373         my $start_t = time();
374
375         my ($results, $files);
376         if ($param->{'use_hest'}) {
377                 ($results, $files) = getFilesHyperEstraier($param, $offset);
378         } else {
379                 ($results, $files) = getFiles($param, $offset);
380         }
381
382         my $dur_t = time() - $start_t;
383         my $dur = sprintf("%0.4fs", $dur_t);
384
385         my ($from, $to) = (($offset * $on_page) + 1, ($offset * $on_page) + $on_page);
386
387         if ($results <= 0) {
388                 $retHTML .= qq{
389                         <p style="color: red;">No results found...</p>
390                 };
391                 return $retHTML;
392         } else {
393                 # DEBUG
394                 #use Data::Dumper;
395                 #$retHTML .= '<pre>' . Dumper($files) . '</pre>';
396         }
397
398
399         $retHTML .= qq{
400         <div>
401         Found <b>$results files</b> showing <b>$from - $to</b> (took $dur)
402         </div>
403         <table style="fview" width="100%" border="0" cellpadding="2" cellspacing="0">
404                 <tr class="fviewheader"> 
405                 <td align="center">Share</td>
406                 <td align="center">Type and Name</td>
407                 <td align="center">#</td>
408                 <td align="center">Size</td>
409                 <td align="center">Date</td>
410                 <td align="center">Media</td>
411                 </tr>
412         };
413
414         my $file;
415
416         sub hilite_html($$) {
417                 my ($html, $search) = @_;
418                 $html =~ s#($search)#<b>$1</b>#gis;
419                 return $html;
420         }
421
422         sub restore_link($$$$$$) {
423                 my $type = shift;
424                 my $action = 'RestoreFile';
425                 $action = 'browse' if (lc($type) eq 'dir');
426                 return sprintf(qq{<a href="?action=%s&host=%s&num=%d&share=%s&dir=%s">%s</a>}, $action, @_);
427         }
428
429         foreach $file (@{ $files }) {
430                 my $typeStr  = BackupPC::Attrib::fileType2Text(undef, $file->{'type'});
431                 $retHTML .= qq{<tr class="fviewborder">};
432
433                 $retHTML .=
434                         qq{<td class="fviewborder" align="right">} . $file->{'hname'} . ':' . $file->{'sname'} . qq{</td>} .
435                         qq{<td class="fviewborder"><img src="$Conf{CgiImageDirURL}/icon-$typeStr.gif" alt="$typeStr" align="middle">&nbsp;} . hilite_html( $file->{'filepath'}, $hilite ) . qq{</td>} .
436                         qq{<td class="fviewborder" align="center">} . restore_link( $typeStr, ${EscURI( $file->{'hname'} )}, $file->{'backupnum'}, ${EscURI( $file->{'sname'})}, ${EscURI( $file->{'filepath'} )}, $file->{'backupnum'} ) . qq{</td>} .
437                         qq{<td class="fviewborder" align="right">} . $file->{'size'} . qq{</td>} .
438                         qq{<td class="fviewborder">} . epoch_to_iso( $file->{'date'} ) . qq{</td>} .
439                         qq{<td class="fviewborder">} . $file->{'dvd'} . qq{</td>};
440
441                 $retHTML .= "</tr>";
442         }
443         $retHTML .= "</table>";
444
445         # all variables which has to be transfered
446         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/) {
447                 $retHTML .= qq{<INPUT TYPE="hidden" NAME="$n" VALUE="$In{$n}">\n};
448         }
449
450         my $del = '';
451         my $max_page = int( $results / $on_page );
452         my $page = 0;
453
454         sub page_link($$$) {
455                 my ($param,$page,$display) = @_;
456
457                 $param->{'offset'} = $page;
458
459                 my $html = '<a href = "' . $MyURL;
460                 my $del = '?';
461                 foreach my $k (keys %{ $param }) {
462                         if ($param->{$k}) {
463                                 $html .= $del . $k . '=' . ${EscURI( $param->{$k} )};
464                                 $del = '&';
465                         }
466                 }
467                 $html .= '">' . $display . '</a>';
468         }
469
470         $retHTML .= '<div style="text-align: center;">';
471
472         if ($offset > 0) {
473                 $retHTML .= page_link($param, $offset - 1, '&lt;&lt;') . ' ';
474         }
475
476         while ($page <= $max_page) {
477                 if ($page == $offset) {
478                         $retHTML .= $del . '<b>' . ($page + 1) . '</b>';
479                 } else {
480                         $retHTML .= $del . page_link($param, $page, $page + 1);
481                 }
482
483                 if ($page < $offset - $pager_pages && $page != 0) {
484                         $retHTML .= " ... ";
485                         $page = $offset - $pager_pages;
486                         $del = '';
487                 } elsif ($page > $offset + $pager_pages && $page != $max_page) {
488                         $retHTML .= " ... ";
489                         $page = $max_page;
490                         $del = '';
491                 } else {
492                         $del = ' | ';
493                         $page++;
494                 }
495         }
496
497         if ($offset < $max_page) {
498                 $retHTML .= ' ' . page_link($param, $offset + 1, '&gt;&gt;');
499         }
500
501         $retHTML .= "</div>";
502
503         return $retHTML;
504 }
505
506 1;