added EST_SYNC_EVERY => 10000 to sync HyperEstraier database every 10000
[BackupPC.git] / bin / BackupPC_updatedb
1 #!/usr/local/bin/perl -w
2
3 use strict;
4 use lib "__INSTALLDIR__/lib";
5
6 use DBI;
7 use BackupPC::Lib;
8 use BackupPC::View;
9 use Data::Dumper;
10 use Getopt::Std;
11 use Time::HiRes qw/time/;
12 use File::Pid;
13 use POSIX qw/strftime/;
14
15 use constant BPC_FTYPE_DIR => 5;
16 use constant EST_SYNC_EVERY => 10000;
17
18 my $debug = 0;
19 $|=1;
20
21 my $start_t = time();
22
23 my $pidfile = new File::Pid;
24
25 if (my $pid = $pidfile->running ) {
26         die "$0 already running: $pid\n";
27 } elsif ($pidfile->pid ne $$) {
28         $pidfile->remove;
29         $pidfile = new File::Pid;
30 }
31 $pidfile->write;
32 print STDERR "$0 using pid ",$pidfile->pid," file ",$pidfile->file,"\n";
33
34 my $t_fmt = '%Y-%m-%d %H:%M:%S';
35
36 my $hosts;
37 my $bpc = BackupPC::Lib->new || die;
38 my %Conf = $bpc->Conf();
39 my $TopDir = $bpc->TopDir();
40 my $beenThere = {};
41
42 my $dsn = $Conf{SearchDSN} || die "Need SearchDSN in config.pl\n";
43 my $user = $Conf{SearchUser} || '';
44 my $index_path = $Conf{HyperEstraierIndex};
45 $index_path = $TopDir . '/' . $index_path;
46 $index_path =~ s#//#/#g;
47
48
49 my $dbh = DBI->connect($dsn, $user, "", { RaiseError => 1, AutoCommit => 0 });
50
51 my %opt;
52
53 if ( !getopts("cdm:v:i", \%opt ) ) {
54         print STDERR <<EOF;
55 usage: $0 [-c|-d] [-m num] [-v|-v level] [-i]
56
57 Options:
58         -c      create database on first use
59         -d      delete database before import
60         -m num  import just num increments for one host
61         -v num  set verbosity (debug) level (default $debug)
62         -i      update HyperEstraier full text index
63 EOF
64         exit 1;
65 }
66
67 if ($opt{v}) {
68         print "Debug level at $opt{v}\n";
69         $debug = $opt{v};
70 }
71
72 #---- subs ----
73
74 sub fmt_time {
75         my $t = shift || return;
76         my $out = "";
77         my ($ss,$mm,$hh) = gmtime($t);
78         $out .= "${hh}h" if ($hh);
79         $out .= sprintf("%02d:%02d", $mm,$ss);
80         return $out;
81 }
82
83 sub curr_time {
84         return strftime($t_fmt,localtime());
85 }
86
87 my $hest_db;
88
89 sub signal {
90         my($sig) = @_;
91         if ($hest_db) {
92                 print "\nCaught a SIG$sig--syncing database and shutting down\n";
93                 $hest_db->sync();
94                 $hest_db->close();
95         }
96         exit(0);
97 }
98
99 $SIG{'INT'}  = \&signal;
100 $SIG{'QUIT'} = \&signal;
101
102 sub hest_update {
103
104         my ($host_id, $share_id, $num) = @_;
105
106         print curr_time," updating HyperEstraier: select files";
107
108         my $t = time();
109
110         my $where = '';
111         if ($host_id && $share_id && $num) {
112                 $where = qq{
113                 WHERE
114                         hosts.id = ? AND
115                         shares.id = ? AND
116                         files.backupnum = ?
117                 };
118         }
119
120         my $sth = $dbh->prepare(qq{
121                 SELECT
122                         files.id                        AS fid,
123                         hosts.name                      AS hname,
124                         shares.name                     AS sname,
125                         -- shares.share                 AS sharename,
126                         files.backupnum                 AS backupnum,
127                         -- files.name                   AS filename,
128                         files.path                      AS filepath,
129                         files.date                      AS date,
130                         files.type                      AS type,
131                         files.size                      AS size,
132                         files.shareid                   AS shareid,
133                         backups.date                    AS backup_date
134                 FROM files 
135                         INNER JOIN shares       ON files.shareID=shares.ID
136                         INNER JOIN hosts        ON hosts.ID = shares.hostID
137                         INNER JOIN backups      ON backups.num = files.backupNum and backups.hostID = hosts.ID AND backups.shareID = shares.ID
138                 $where
139         });
140
141         $sth->execute(@_);
142         my $results = $sth->rows;
143
144         if ($results == 0) {
145                 print " - no files, skipping\n";
146                 return;
147         }
148
149         my $dot = int($results / 15) || 1;
150
151         print " $results ($dot/#)";
152
153         sub fmt_date {
154                 my $t = shift || return;
155                 my $iso = BackupPC::Lib::timeStamp($t);
156                 $iso =~ s/\s/T/;
157                 return $iso;
158         }
159
160         my $max = int($results / $dot);
161
162         print ", opening index $index_path...";
163         use HyperEstraier;
164         my $db = HyperEstraier::Database->new();
165
166 #       unless ($hest_db) {
167 #               print " open reader";
168 #               $hest_db = HyperEstraier::Database->new();
169 #
170 #       }
171
172
173         $db->open($index_path, $HyperEstraier::Database::DBWRITER | $HyperEstraier::Database::DBCREAT);
174
175         my $added = 0;
176
177         while (my $row = $sth->fetchrow_hashref()) {
178
179                 my $fid = $row->{'fid'} || die "no fid?";
180                 my $uri = 'file:///' . $fid;
181
182                 my $id = $db->uri_to_id($uri);
183                 next unless ($id == -1);
184
185                 # create a document object 
186                 my $doc = HyperEstraier::Document->new;
187
188                 # add attributes to the document object 
189                 $doc->add_attr('@uri', $uri);
190
191                 foreach my $c (@{ $sth->{NAME} }) {
192                         $doc->add_attr($c, $row->{$c}) if ($row->{$c});
193                 }
194
195                 #$doc->add_attr('@cdate', fmt_date($row->{'date'}));
196
197                 # add the body text to the document object 
198                 my $path = $row->{'filepath'};
199                 $doc->add_text($path);
200                 $path =~ s/(.)/$1 /g;
201                 $doc->add_hidden_text($path);
202
203                 print STDERR $doc->dump_draft,"\n" if ($debug > 1);
204
205                 # register the document object to the database
206                 $db->put_doc($doc, $HyperEstraier::Database::PDCLEAN);
207
208                 $added++;
209                 if ($added % $dot == 0) {
210                         print "$max ";
211                         $max--;
212                 }
213
214                 if ($added % EST_SYNC_EVERY == 0) {
215                         print "sync ";
216                         $db->sync();
217                 }
218
219         }
220
221         print "sync $added new files";
222         $db->sync();
223         print ", close";
224         $db->close();
225
226         my $dur = (time() - $t) || 1;
227         printf(" [%.2f/s new %.2f/s dur: %s]\n",
228                 ( $results / $dur ),
229                 ( $added / $dur ),
230                 fmt_time($dur)
231         );
232 }
233
234 #---- /subs ----
235
236
237 ## update index ##
238 if (($opt{i} || ($index_path && ! -e $index_path)) && !$opt{c}) {
239         # update all
240         print "force update of HyperEstraier index ";
241         print "importing existing data" unless (-e $index_path);
242         print "by -i flag" if ($opt{i});
243         print "\n";
244         hest_update();
245 }
246
247 ## create tables ##
248 if ($opt{c}) {
249         sub do_index {
250                 my $index = shift || return;
251                 my ($table,$col,$unique) = split(/_/, $index);
252                 $unique ||= '';
253                 $index =~ s/,/_/g;
254                 $dbh->do(qq{ create $unique index $index on $table($col) });
255         }
256
257         print "creating tables...\n";
258       
259         $dbh->do(qq{
260                 create table hosts (
261                         ID      SERIAL          PRIMARY KEY,
262                         name    VARCHAR(30)     NOT NULL,
263                         IP      VARCHAR(15)
264                 );            
265         });
266               
267         $dbh->do(qq{
268                 create table shares (
269                         ID      SERIAL          PRIMARY KEY,
270                         hostID  INTEGER         NOT NULL references hosts(id),
271                         name    VARCHAR(30)     NOT NULL,
272                         share   VARCHAR(200)    NOT NULL,
273                         localpath VARCHAR(200)      
274                 );            
275         });
276         
277         $dbh->do(qq{
278                 create table backups (
279                         hostID  INTEGER         NOT NULL references hosts(id),
280                         num     INTEGER         NOT NULL,
281                         date    integer         NOT NULL, 
282                         type    CHAR(4)         not null,
283                         shareID integer         not null references shares(id),
284                         size    integer         not null,
285                         PRIMARY KEY(hostID, num, shareID) 
286                 );            
287         });
288
289         #do_index('backups_hostid,num_unique');
290
291         $dbh->do(qq{
292                 create table dvds (
293                         ID      SERIAL          PRIMARY KEY, 
294                         num     INTEGER         NOT NULL,
295                         name    VARCHAR(255)    NOT NULL,
296                         mjesto  VARCHAR(255)
297                 );
298         });
299
300         $dbh->do(qq{     
301                 create table files (
302                         ID      SERIAL          PRIMARY KEY,  
303                         shareID INTEGER         NOT NULL references shares(id),
304                         backupNum  INTEGER      NOT NULL,
305                         name       VARCHAR(255) NOT NULL,
306                         path       VARCHAR(255) NOT NULL,
307                         date       integer      NOT NULL,
308                         type       INTEGER      NOT NULL,
309                         size       INTEGER      NOT NULL,
310                         dvdid      INTEGER      references dvds(id)     
311                 );
312         });
313
314         print "creating indexes:";
315
316         foreach my $index (qw(
317                 hosts_name
318                 backups_hostID
319                 backups_num
320                 shares_hostID
321                 shares_name
322                 files_shareID
323                 files_path
324                 files_name
325                 files_date
326                 files_size
327         )) {
328                 print " $index";
329                 do_index($index);
330         }
331         print "...\n";
332
333         $dbh->commit;
334
335 }
336
337 ## delete data before inseting ##
338 if ($opt{d}) {
339         print "deleting ";
340         foreach my $table (qw(files dvds backups shares hosts)) {
341                 print "$table ";
342                 $dbh->do(qq{ DELETE FROM $table });
343         }
344         print " done...\n";
345
346         $dbh->commit;
347 }
348
349 ## insert new values ##
350
351 # get hosts
352 $hosts = $bpc->HostInfoRead();
353 my $hostID;
354 my $shareID;
355
356 my $sth;
357
358 $sth->{insert_hosts} = $dbh->prepare(qq{
359 INSERT INTO hosts (name, IP) VALUES (?,?)
360 });
361
362 $sth->{hosts_by_name} = $dbh->prepare(qq{
363 SELECT ID FROM hosts WHERE name=?
364 });
365
366 $sth->{backups_count} = $dbh->prepare(qq{
367 SELECT COUNT(*)
368 FROM backups
369 WHERE hostID=? AND num=? AND shareid=?
370 });
371
372 $sth->{insert_backups} = $dbh->prepare(qq{
373 INSERT INTO backups (hostID, num, date, type, shareid, size)
374 VALUES (?,?,?,?,?,?)
375 });
376
377 $sth->{insert_files} = $dbh->prepare(qq{
378 INSERT INTO files
379         (shareID, backupNum, name, path, date, type, size)
380         VALUES (?,?,?,?,?,?,?)
381 });
382
383 foreach my $host_key (keys %{$hosts}) {
384
385         my $hostname = $hosts->{$host_key}->{'host'} || die "can't find host for $host_key";
386
387         $sth->{hosts_by_name}->execute($hosts->{$host_key}->{'host'});
388
389         unless (($hostID) = $sth->{hosts_by_name}->fetchrow_array()) {
390                 $sth->{insert_hosts}->execute(
391                         $hosts->{$host_key}->{'host'},
392                         $hosts->{$host_key}->{'ip'}
393                 );
394
395                 $hostID = $dbh->last_insert_id(undef,undef,'hosts',undef);
396         }
397
398         print "host ".$hosts->{$host_key}->{'host'}.": ";
399  
400         # get backups for a host
401         my @backups = $bpc->BackupInfoRead($hostname);
402         my $incs = scalar @backups;
403         print  "$incs increments\n";
404
405         my $inc_nr = 0;
406         $beenThere = {};
407
408         foreach my $backup (@backups) {
409
410                 $inc_nr++;
411                 last if ($opt{m} && $inc_nr > $opt{m});
412
413                 my $backupNum = $backup->{'num'};
414                 my @backupShares = ();
415
416                 printf("%-10s %2d/%-2d #%-2d %s %5s/%5s files (date: %s dur: %s)\n", 
417                         $hosts->{$host_key}->{'host'},
418                         $inc_nr, $incs, $backupNum, 
419                         $backup->{type} || '?',
420                         $backup->{nFilesNew} || '?', $backup->{nFiles} || '?',
421                         strftime($t_fmt,localtime($backup->{startTime})),
422                         fmt_time($backup->{endTime} - $backup->{startTime})
423                 );
424
425                 my $files = BackupPC::View->new($bpc, $hostname, \@backups, 1);
426                 foreach my $share ($files->shareList($backupNum)) {
427
428                         my $t = time();
429
430                         $shareID = getShareID($share, $hostID, $hostname);
431                 
432                         $sth->{backups_count}->execute($hostID, $backupNum, $shareID);
433                         my ($count) = $sth->{backups_count}->fetchrow_array();
434                         # skip if allready in database!
435                         next if ($count > 0);
436
437                         # dump some log
438                         print curr_time," ", $share;
439
440                         my ($f, $nf, $d, $nd, $size) = recurseDir($bpc, $hostname, $files, $backupNum, $share, "", $shareID);
441
442                         $sth->{insert_backups}->execute(
443                                 $hostID,
444                                 $backupNum,
445                                 $backup->{'endTime'},
446                                 $backup->{'type'},
447                                 $shareID,
448                                 $size,
449                         );
450
451                         print " commit";
452                         $dbh->commit();
453
454                         my $dur = (time() - $t) || 1;
455                         printf(" %d/%d files %d/%d dirs %0.2f MB [%.2f/s dur: %s]\n",
456                                 $nf, $f, $nd, $d,
457                                 ($size / 1024 / 1024),
458                                 ( ($f+$d) / $dur ),
459                                 fmt_time($dur)
460                         );
461
462                         hest_update($hostID, $shareID, $backupNum);
463                 }
464
465         }
466 }
467 undef $sth;
468 $dbh->commit();
469 $dbh->disconnect();
470
471 print "total duration: ",fmt_time(time() - $start_t),"\n";
472
473 $pidfile->remove;
474
475 sub getShareID() {
476
477         my ($share, $hostID, $hostname) = @_;
478
479         $sth->{share_id} ||= $dbh->prepare(qq{
480                 SELECT ID FROM shares WHERE hostID=? AND name=?
481         });
482
483         $sth->{share_id}->execute($hostID,$share);
484
485         my ($id) = $sth->{share_id}->fetchrow_array();
486
487         return $id if (defined($id));
488
489         $sth->{insert_share} ||= $dbh->prepare(qq{
490                 INSERT INTO shares 
491                         (hostID,name,share,localpath) 
492                 VALUES (?,?,?,?)
493         });
494
495         my $drop_down = $hostname . '/' . $share;
496         $drop_down =~ s#//+#/#g;
497
498         $sth->{insert_share}->execute($hostID,$share, $drop_down ,undef);
499         return $dbh->last_insert_id(undef,undef,'shares',undef);
500 }
501
502 sub found_in_db {
503
504         my @data = @_;
505         shift @data;
506
507         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
508
509         return $beenThere->{$key} if (defined($beenThere->{$key}));
510
511         $sth->{file_in_db} ||= $dbh->prepare(qq{
512                 SELECT 1 FROM files
513                 WHERE shareID = ? and
514                         path = ? and 
515                         date = ? and
516                         size = ?
517                 LIMIT 1
518         });
519
520         my @param = ($shareID,$path,$date,$size);
521         $sth->{file_in_db}->execute(@param);
522         my $rows = $sth->{file_in_db}->rows;
523         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
524
525         $beenThere->{$key}++;
526
527         $sth->{'insert_files'}->execute(@data) unless ($rows);
528         return $rows;
529 }
530
531 ####################################################
532 # recursing through filesystem structure and       #
533 # and returning flattened files list               #
534 ####################################################
535 sub recurseDir($$$$$$$$) {
536
537         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
538
539         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
540
541         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
542
543         { # scope
544                 my @stack;
545
546                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
547                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
548
549                 # first, add all the entries in current directory
550                 foreach my $path_key (keys %{$filesInBackup}) {
551                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
552                         my @data = (
553                                 $shareID,
554                                 $backupNum,
555                                 $path_key,
556                                 $filesInBackup->{$path_key}->{'relPath'},
557                                 $filesInBackup->{$path_key}->{'mtime'},
558                                 $filesInBackup->{$path_key}->{'type'},
559                                 $filesInBackup->{$path_key}->{'size'}
560                         );
561
562                         my $key = join(" ", (
563                                 $shareID,
564                                 $dir,
565                                 $path_key,
566                                 $filesInBackup->{$path_key}->{'mtime'},
567                                 $filesInBackup->{$path_key}->{'size'}
568                         ));
569
570                         my $found;
571                         if (! defined($beenThere->{$key}) && ! ($found = found_in_db($key, @data)) ) {
572                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
573
574                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
575                                         $new_dirs++ unless ($found);
576                                         print STDERR " dir\n" if ($debug >= 2);
577                                 } else {
578                                         $new_files++ unless ($found);
579                                         print STDERR " file\n" if ($debug >= 2);
580                                 }
581                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
582                         }
583
584                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
585                                 $nr_dirs++;
586
587                                 my $full_path = $dir . '/' . $path_key;
588                                 push @stack, $full_path;
589                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
590
591 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
592 #
593 #                               $nr_files += $f;
594 #                               $new_files += $nf;
595 #                               $nr_dirs += $d;
596 #                               $new_dirs += $nd;
597
598                         } else {
599                                 $nr_files++;
600                         }
601                 }
602
603                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
604
605                 while ( my $dir = shift @stack ) {
606                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
607                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
608                         $nr_files += $f;
609                         $new_files += $nf;
610                         $nr_dirs += $d;
611                         $new_dirs += $nd;
612                         $size += $s;
613                 }
614         }
615
616         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
617 }
618