r8206@llin: dpavlin | 2005-09-23 10:53:58 +0200
[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 use BackupPC::SearchLib;
15
16 use constant BPC_FTYPE_DIR => 5;
17 use constant EST_CHUNK => 100000;
18
19 my $debug = 0;
20 $|=1;
21
22 my $start_t = time();
23
24 my $pidfile = new File::Pid;
25
26 if (my $pid = $pidfile->running ) {
27         die "$0 already running: $pid\n";
28 } elsif ($pidfile->pid ne $$) {
29         $pidfile->remove;
30         $pidfile = new File::Pid;
31 }
32 $pidfile->write;
33 print STDERR "$0 using pid ",$pidfile->pid," file ",$pidfile->file,"\n";
34
35 my $t_fmt = '%Y-%m-%d %H:%M:%S';
36
37 my $hosts;
38 my $bpc = BackupPC::Lib->new || die;
39 my %Conf = $bpc->Conf();
40 my $TopDir = $bpc->TopDir();
41 my $beenThere = {};
42
43 my $dsn = $Conf{SearchDSN} || die "Need SearchDSN in config.pl\n";
44 my $user = $Conf{SearchUser} || '';
45
46 my $use_hest = $Conf{HyperEstraierIndex};
47 my ($index_path, $index_node_url) = BackupPC::SearchLib::getHyperEstraier_url($use_hest);
48
49 my $dbh = DBI->connect($dsn, $user, "", { RaiseError => 1, AutoCommit => 0 });
50
51 my %opt;
52
53 if ( !getopts("cdm:v:ij", \%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 Hyper Estraier full text index
63         -j      update full text, don't check existing files
64
65 Option -j is variation on -i. It will allow faster initial creation
66 of full-text index from existing database.
67
68 EOF
69         exit 1;
70 }
71
72 if ($opt{v}) {
73         print "Debug level at $opt{v}\n";
74         $debug = $opt{v};
75 }
76
77 #---- subs ----
78
79 sub fmt_time {
80         my $t = shift || return;
81         my $out = "";
82         my ($ss,$mm,$hh) = gmtime($t);
83         $out .= "${hh}h" if ($hh);
84         $out .= sprintf("%02d:%02d", $mm,$ss);
85         return $out;
86 }
87
88 sub curr_time {
89         return strftime($t_fmt,localtime());
90 }
91
92 my $hest_db;
93 my $hest_node;
94
95 sub signal {
96         my($sig) = @_;
97         if ($hest_db) {
98                 print "\nCaught a SIG$sig--syncing database and shutting down\n";
99                 $hest_db->sync();
100                 $hest_db->close();
101         }
102         exit(0);
103 }
104
105 $SIG{'INT'}  = \&signal;
106 $SIG{'QUIT'} = \&signal;
107
108 sub hest_update {
109
110         my ($host_id, $share_id, $num) = @_;
111
112         my $skip_check = $opt{j} && print STDERR "Skipping check for existing files -- this should be used only with initital import\n";
113
114         unless ($use_hest) {
115                 print STDERR "HyperEstraier support not enabled in configuration\n";
116                 return;
117         }
118
119         print curr_time," updating HyperEstraier:";
120
121         my $t = time();
122
123         my $offset = 0;
124         my $added = 0;
125
126         print " opening index $use_hest";
127         if ($index_path) {
128                 $hest_db = HyperEstraier::Database->new();
129                 $hest_db->open($TopDir . $index_path, $HyperEstraier::Database::DBWRITER | $HyperEstraier::Database::DBCREAT);
130                 print " directly";
131         } elsif ($index_node_url) {
132                 $hest_node ||= HyperEstraier::Node->new($index_node_url);
133                 $hest_node->set_auth('admin', 'admin');
134                 print " via node URL";
135         } else {
136                 die "don't know how to use HyperEstraier Index $use_hest";
137         }
138         print " increment is " . EST_CHUNK . " files:";
139
140         my $results = 0;
141
142         do {
143
144                 my $where = '';
145                 my @data;
146                 if ($host_id && $share_id && $num) {
147                         $where = qq{
148                         WHERE
149                                 hosts.id = ? AND
150                                 shares.id = ? AND
151                                 files.backupnum = ?
152                         };
153                         @data = ( $host_id, $share_id, $num );
154                 }
155
156                 my $limit = sprintf('LIMIT '.EST_CHUNK.' OFFSET %d', $offset);
157
158                 my $sth = $dbh->prepare(qq{
159                         SELECT
160                                 files.id                        AS fid,
161                                 hosts.name                      AS hname,
162                                 shares.name                     AS sname,
163                                 -- shares.share                 AS sharename,
164                                 files.backupnum                 AS backupnum,
165                                 -- files.name                   AS filename,
166                                 files.path                      AS filepath,
167                                 files.date                      AS date,
168                                 files.type                      AS type,
169                                 files.size                      AS size,
170                                 files.shareid                   AS shareid,
171                                 backups.date                    AS backup_date
172                         FROM files 
173                                 INNER JOIN shares       ON files.shareID=shares.ID
174                                 INNER JOIN hosts        ON hosts.ID = shares.hostID
175                                 INNER JOIN backups      ON backups.num = files.backupNum and backups.hostID = hosts.ID AND backups.shareID = shares.ID
176                         $where
177                         $limit
178                 });
179
180                 $sth->execute(@data);
181                 $results = $sth->rows;
182
183                 if ($results == 0) {
184                         print " - no new files\n";
185                         last;
186                 }
187
188                 sub fmt_date {
189                         my $t = shift || return;
190                         my $iso = BackupPC::Lib::timeStamp($t);
191                         $iso =~ s/\s/T/;
192                         return $iso;
193                 }
194
195                 while (my $row = $sth->fetchrow_hashref()) {
196
197                         my $fid = $row->{'fid'} || die "no fid?";
198                         my $uri = 'file:///' . $fid;
199
200                         unless ($skip_check) {
201                                 my $id = ($hest_db || $hest_node)->uri_to_id($uri);
202                                 next unless ($id == -1);
203                         }
204
205                         # create a document object 
206                         my $doc = HyperEstraier::Document->new;
207
208                         # add attributes to the document object 
209                         $doc->add_attr('@uri', $uri);
210
211                         foreach my $c (@{ $sth->{NAME} }) {
212                                 $doc->add_attr($c, $row->{$c}) if ($row->{$c});
213                         }
214
215                         #$doc->add_attr('@cdate', fmt_date($row->{'date'}));
216
217                         # add the body text to the document object 
218                         my $path = $row->{'filepath'};
219                         $doc->add_text($path);
220                         $path =~ s/(.)/$1 /g;
221                         $doc->add_hidden_text($path);
222
223                         print STDERR $doc->dump_draft,"\n" if ($debug > 1);
224
225                         # register the document object to the database
226                         if ($hest_db) {
227                                 $hest_db->put_doc($doc, $HyperEstraier::Database::PDCLEAN);
228                         } elsif ($hest_node) {
229                                 $hest_node->put_doc($doc);
230                         } else {
231                                 die "not supported";
232                         }
233                         $added++;
234                 }
235
236                 print " $added";
237                 $hest_db->sync() if ($index_path);
238
239                 $offset += EST_CHUNK;
240
241         } while ($results == EST_CHUNK);
242
243         if ($index_path) {
244                 print ", close";
245                 $hest_db->close();
246         }
247
248         my $dur = (time() - $t) || 1;
249         printf(" [%.2f/s dur: %s]\n",
250                 ( $added / $dur ),
251                 fmt_time($dur)
252         );
253 }
254
255 #---- /subs ----
256
257
258 ## update index ##
259 if (($opt{i} || $opt{j} || ($index_path && ! -e $index_path)) && !$opt{c}) {
260         # update all
261         print "force update of HyperEstraier index ";
262         print "importing existing data" unless (-e $index_path);
263         print "by -i flag" if ($opt{i});
264         print "by -j flag" if ($opt{j});
265         print "\n";
266         hest_update();
267 }
268
269 ## create tables ##
270 if ($opt{c}) {
271         sub do_index {
272                 my $index = shift || return;
273                 my ($table,$col,$unique) = split(/_/, $index);
274                 $unique ||= '';
275                 $index =~ s/,/_/g;
276                 $dbh->do(qq{ create $unique index $index on $table($col) });
277         }
278
279         print "creating tables...\n";
280       
281         $dbh->do(qq{
282                 create table hosts (
283                         ID      SERIAL          PRIMARY KEY,
284                         name    VARCHAR(30)     NOT NULL,
285                         IP      VARCHAR(15)
286                 );            
287         });
288               
289         $dbh->do(qq{
290                 create table shares (
291                         ID      SERIAL          PRIMARY KEY,
292                         hostID  INTEGER         NOT NULL references hosts(id),
293                         name    VARCHAR(30)     NOT NULL,
294                         share   VARCHAR(200)    NOT NULL,
295                         localpath VARCHAR(200)      
296                 );            
297         });
298
299         $dbh->do(qq{
300                 create table dvds (
301                         ID      SERIAL          PRIMARY KEY, 
302                         num     INTEGER         NOT NULL,
303                         name    VARCHAR(255)    NOT NULL,
304                         mjesto  VARCHAR(255)
305                 );
306         });
307         
308         $dbh->do(qq{
309                 create table backups (
310                         hostID  INTEGER         NOT NULL references hosts(id),
311                         num     INTEGER         NOT NULL,
312                         date    integer         NOT NULL, 
313                         type    CHAR(4)         not null,
314                         shareID integer         not null references shares(id),
315                         size    integer         not null,
316                         PRIMARY KEY(hostID, num, shareID) 
317                 );            
318         });
319
320         #do_index('backups_hostid,num_unique');
321
322
323         $dbh->do(qq{     
324                 create table files (
325                         ID      SERIAL          PRIMARY KEY,  
326                         shareID INTEGER         NOT NULL references shares(id),
327                         backupNum  INTEGER      NOT NULL,
328                         name       VARCHAR(255) NOT NULL,
329                         path       VARCHAR(255) NOT NULL,
330                         date       integer      NOT NULL,
331                         type       INTEGER      NOT NULL,
332                         size       INTEGER      NOT NULL
333                 );
334         });
335
336
337         $dbh->do( qq{
338                 create table archive
339                 (
340                         id                      int not null,
341                         dvd_nr          int not null,
342                         note            text,
343                         username        varchar(20) not null,
344                         date            timestamp,
345                         primary key(id)
346                 );      
347         }
348         );
349
350         $dbh->do( qq{
351                 create table archive_backup
352                 (
353                         archive_id      int not null,
354                         backup_id       int not null,
355                         status          text,
356                         primary key(archive_id, backup_id)
357                 );
358         });
359
360         $dbh->do( qq{
361                 create table workflows(
362                         id                      int not null,
363                         step_id         int not null,
364                         start           timestamp,
365                         stop            timestamp,
366                         username        varchar(20),
367                         archive_id      int not null,
368                         running         boolean default true,
369                         primary key(id)
370                 );
371         });
372
373         $dbh->do( qq{
374                 create table workflow_step
375                 (
376                         step_id         int not null,
377                         code            text,
378                         next_step       int,
379                         stop            boolean default false,
380                         primary key(step_id)
381                 );
382         });
383
384         $dbh->do( qq{
385                         alter table workflow_step
386                                 add constraint fk_workflow_next_step
387                                 foreign key(next_step)
388                                 references workflow_step(step_id);
389         });
390
391         $dbh->do( qq{
392                 alter table workflows
393                         add constraint fk_workflows_step_id
394                         foreign key(step_id)
395                         references workflow_step(step_id);
396         });
397         
398         $dbh->do( qq{
399                 alter table workflows
400                         add constraint fk_workflows_archive_id
401                         foreign key(archive_id)
402                         references archive(id); 
403         });
404
405         $dbh->do( qq{
406                 create table workflow_log
407                 (
408                         workflow_id             int not null,
409                         step_id                 int not null,
410                         date                    timestamp not null,
411                         status                  text,
412                         primary key(workflow_id, step_id)
413                 );
414         });
415
416         $dbh->do( qq{
417                 alter table workflow_log
418                         add constraint fk_workflow_log_workflow_id
419                         foreign key (workflow_id)
420                         references workflows(id);
421                 });
422         
423         $dbh->do( qq{
424                 alter table workflow_log
425                         add constraint fk_workflow_log_step_id
426                         foreign key (step_id)
427                         references      workflow_step(step_id);
428                 });
429
430         print "creating indexes:";
431
432         foreach my $index (qw(
433                 hosts_name
434                 backups_hostID
435                 backups_num
436                 shares_hostID
437                 shares_name
438                 files_shareID
439                 files_path
440                 files_name
441                 files_date
442                 files_size
443         )) {
444                 print " $index";
445                 do_index($index);
446         }
447         print "...\n";
448
449         $dbh->commit;
450
451 }
452
453 ## delete data before inseting ##
454 if ($opt{d}) {
455         print "deleting ";
456         foreach my $table (qw(files dvds backups shares hosts)) {
457                 print "$table ";
458                 $dbh->do(qq{ DELETE FROM $table });
459         }
460         print " done...\n";
461
462         $dbh->commit;
463 }
464
465 ## insert new values ##
466
467 # get hosts
468 $hosts = $bpc->HostInfoRead();
469 my $hostID;
470 my $shareID;
471
472 my $sth;
473
474 $sth->{insert_hosts} = $dbh->prepare(qq{
475 INSERT INTO hosts (name, IP) VALUES (?,?)
476 });
477
478 $sth->{hosts_by_name} = $dbh->prepare(qq{
479 SELECT ID FROM hosts WHERE name=?
480 });
481
482 $sth->{backups_count} = $dbh->prepare(qq{
483 SELECT COUNT(*)
484 FROM backups
485 WHERE hostID=? AND num=? AND shareid=?
486 });
487
488 $sth->{insert_backups} = $dbh->prepare(qq{
489 INSERT INTO backups (hostID, num, date, type, shareid, size)
490 VALUES (?,?,?,?,?,?)
491 });
492
493 $sth->{insert_files} = $dbh->prepare(qq{
494 INSERT INTO files
495         (shareID, backupNum, name, path, date, type, size)
496         VALUES (?,?,?,?,?,?,?)
497 });
498
499 foreach my $host_key (keys %{$hosts}) {
500
501         my $hostname = $hosts->{$host_key}->{'host'} || die "can't find host for $host_key";
502
503         $sth->{hosts_by_name}->execute($hosts->{$host_key}->{'host'});
504
505         unless (($hostID) = $sth->{hosts_by_name}->fetchrow_array()) {
506                 $sth->{insert_hosts}->execute(
507                         $hosts->{$host_key}->{'host'},
508                         $hosts->{$host_key}->{'ip'}
509                 );
510
511                 $hostID = $dbh->last_insert_id(undef,undef,'hosts',undef);
512         }
513
514         print "host ".$hosts->{$host_key}->{'host'}.": ";
515  
516         # get backups for a host
517         my @backups = $bpc->BackupInfoRead($hostname);
518         my $incs = scalar @backups;
519         print  "$incs increments\n";
520
521         my $inc_nr = 0;
522         $beenThere = {};
523
524         foreach my $backup (@backups) {
525
526                 $inc_nr++;
527                 last if ($opt{m} && $inc_nr > $opt{m});
528
529                 my $backupNum = $backup->{'num'};
530                 my @backupShares = ();
531
532                 printf("%-10s %2d/%-2d #%-2d %s %5s/%5s files (date: %s dur: %s)\n", 
533                         $hosts->{$host_key}->{'host'},
534                         $inc_nr, $incs, $backupNum, 
535                         $backup->{type} || '?',
536                         $backup->{nFilesNew} || '?', $backup->{nFiles} || '?',
537                         strftime($t_fmt,localtime($backup->{startTime})),
538                         fmt_time($backup->{endTime} - $backup->{startTime})
539                 );
540
541                 my $files = BackupPC::View->new($bpc, $hostname, \@backups, 1);
542                 foreach my $share ($files->shareList($backupNum)) {
543
544                         my $t = time();
545
546                         $shareID = getShareID($share, $hostID, $hostname);
547                 
548                         $sth->{backups_count}->execute($hostID, $backupNum, $shareID);
549                         my ($count) = $sth->{backups_count}->fetchrow_array();
550                         # skip if allready in database!
551                         next if ($count > 0);
552
553                         # dump some log
554                         print curr_time," ", $share;
555
556                         my ($f, $nf, $d, $nd, $size) = recurseDir($bpc, $hostname, $files, $backupNum, $share, "", $shareID);
557
558                         $sth->{insert_backups}->execute(
559                                 $hostID,
560                                 $backupNum,
561                                 $backup->{'endTime'},
562                                 substr($backup->{'type'},0,4),
563                                 $shareID,
564                                 $size,
565                         );
566
567                         print " commit";
568                         $dbh->commit();
569
570                         my $dur = (time() - $t) || 1;
571                         printf(" %d/%d files %d/%d dirs %0.2f MB [%.2f/s dur: %s]\n",
572                                 $nf, $f, $nd, $d,
573                                 ($size / 1024 / 1024),
574                                 ( ($f+$d) / $dur ),
575                                 fmt_time($dur)
576                         );
577
578                         hest_update($hostID, $shareID, $backupNum) if ($nf + $nd > 0);
579                 }
580
581         }
582 }
583 undef $sth;
584 $dbh->commit();
585 $dbh->disconnect();
586
587 print "total duration: ",fmt_time(time() - $start_t),"\n";
588
589 $pidfile->remove;
590
591 sub getShareID() {
592
593         my ($share, $hostID, $hostname) = @_;
594
595         $sth->{share_id} ||= $dbh->prepare(qq{
596                 SELECT ID FROM shares WHERE hostID=? AND name=?
597         });
598
599         $sth->{share_id}->execute($hostID,$share);
600
601         my ($id) = $sth->{share_id}->fetchrow_array();
602
603         return $id if (defined($id));
604
605         $sth->{insert_share} ||= $dbh->prepare(qq{
606                 INSERT INTO shares 
607                         (hostID,name,share,localpath) 
608                 VALUES (?,?,?,?)
609         });
610
611         my $drop_down = $hostname . '/' . $share;
612         $drop_down =~ s#//+#/#g;
613
614         $sth->{insert_share}->execute($hostID,$share, $drop_down ,undef);
615         return $dbh->last_insert_id(undef,undef,'shares',undef);
616 }
617
618 sub found_in_db {
619
620         my @data = @_;
621         shift @data;
622
623         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
624
625         return $beenThere->{$key} if (defined($beenThere->{$key}));
626
627         $sth->{file_in_db} ||= $dbh->prepare(qq{
628                 SELECT 1 FROM files
629                 WHERE shareID = ? and
630                         path = ? and 
631                         date = ? and
632                         size = ?
633                 LIMIT 1
634         });
635
636         my @param = ($shareID,$path,$date,$size);
637         $sth->{file_in_db}->execute(@param);
638         my $rows = $sth->{file_in_db}->rows;
639         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
640
641         $beenThere->{$key}++;
642
643         $sth->{'insert_files'}->execute(@data) unless ($rows);
644         return $rows;
645 }
646
647 ####################################################
648 # recursing through filesystem structure and       #
649 # and returning flattened files list               #
650 ####################################################
651 sub recurseDir($$$$$$$$) {
652
653         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
654
655         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
656
657         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
658
659         { # scope
660                 my @stack;
661
662                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
663                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
664
665                 # first, add all the entries in current directory
666                 foreach my $path_key (keys %{$filesInBackup}) {
667                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
668                         my @data = (
669                                 $shareID,
670                                 $backupNum,
671                                 $path_key,
672                                 $filesInBackup->{$path_key}->{'relPath'},
673                                 $filesInBackup->{$path_key}->{'mtime'},
674                                 $filesInBackup->{$path_key}->{'type'},
675                                 $filesInBackup->{$path_key}->{'size'}
676                         );
677
678                         my $key = join(" ", (
679                                 $shareID,
680                                 $dir,
681                                 $path_key,
682                                 $filesInBackup->{$path_key}->{'mtime'},
683                                 $filesInBackup->{$path_key}->{'size'}
684                         ));
685
686                         my $found;
687                         if (! defined($beenThere->{$key}) && ! ($found = found_in_db($key, @data)) ) {
688                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
689
690                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
691                                         $new_dirs++ unless ($found);
692                                         print STDERR " dir\n" if ($debug >= 2);
693                                 } else {
694                                         $new_files++ unless ($found);
695                                         print STDERR " file\n" if ($debug >= 2);
696                                 }
697                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
698                         }
699
700                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
701                                 $nr_dirs++;
702
703                                 my $full_path = $dir . '/' . $path_key;
704                                 push @stack, $full_path;
705                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
706
707 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
708 #
709 #                               $nr_files += $f;
710 #                               $new_files += $nf;
711 #                               $nr_dirs += $d;
712 #                               $new_dirs += $nd;
713
714                         } else {
715                                 $nr_files++;
716                         }
717                 }
718
719                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
720
721                 while ( my $dir = shift @stack ) {
722                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
723                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
724                         $nr_files += $f;
725                         $new_files += $nf;
726                         $nr_dirs += $d;
727                         $new_dirs += $nd;
728                         $size += $s;
729                 }
730         }
731
732         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
733 }
734