r8481@llin: dpavlin | 2005-10-12 13:15:12 +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/\W+/_/g;
276                 print "$index on $table($col)" . ( $unique ? "u" : "" ) . " ";
277                 $dbh->do(qq{ create $unique index $index on $table($col) });
278         }
279
280         print "creating tables...\n";
281
282         $dbh->do( qq{
283                 create table hosts (
284                         ID      SERIAL          PRIMARY KEY,
285                         name    VARCHAR(30)     NOT NULL,
286                         IP      VARCHAR(15)
287                 );            
288
289                 create table shares (
290                         ID      SERIAL          PRIMARY KEY,
291                         hostID  INTEGER         NOT NULL references hosts(id),
292                         name    VARCHAR(30)     NOT NULL,
293                         share   VARCHAR(200)    NOT NULL
294                 );            
295
296                 create table dvds (
297                         ID      SERIAL          PRIMARY KEY, 
298                         num     INTEGER         NOT NULL,
299                         name    VARCHAR(255)    NOT NULL,
300                         mjesto  VARCHAR(255)
301                 );
302
303                 create table backups (
304                         id      serial,
305                         hostID  INTEGER         NOT NULL references hosts(id),
306                         num     INTEGER         NOT NULL,
307                         date    integer         NOT NULL, 
308                         type    CHAR(4)         not null,
309                         shareID integer         not null references shares(id),
310                         size    bigint          not null,
311                         inc_size bigint         not null default -1,
312                         inc_deleted boolean     default false,
313                         PRIMARY KEY(id)
314                 );            
315
316                 create table files (
317                         ID              SERIAL,
318                         shareID         INTEGER NOT NULL references shares(id),
319                         backupNum       INTEGER NOT NULL,
320                         name            VARCHAR(255) NOT NULL,
321                         path            VARCHAR(255) NOT NULL,
322                         date            integer NOT NULL,
323                         type            INTEGER NOT NULL,
324                         size            bigint  NOT NULL,
325                         primary key(id)
326                 );
327
328                 create table archive (
329                         id              serial,
330                         dvd_nr          int not null,
331                         total_size      bigint default -1,
332                         note            text,
333                         username        varchar(20) not null,
334                         date            timestamp default now(),
335                         primary key(id)
336                 );      
337
338                 create table archive_backup (
339                         archive_id      int not null references archive(id) on delete cascade,
340                         backup_id       int not null references backups(id),
341                         primary key(archive_id, backup_id)
342                 );
343
344                 create table archive_burned (
345                         archive_id int references archive(id),
346                         date date default now(),
347                         iso_size int default -1
348                 );
349
350         });
351
352         print "creating indexes: ";
353
354         foreach my $index (qw(
355                 hosts:name
356                 backups:hostID
357                 backups:num
358                 backups:shareID
359                 shares:hostID
360                 shares:name
361                 files:shareID
362                 files:path
363                 files:name
364                 files:date
365                 files:size
366                 archive:dvd_nr
367                 archive_burned:archive_id
368         )) {
369                 do_index($index);
370         }
371
372         print " creating sequence: ";
373         foreach my $seq (qw/dvd_nr/) {
374                 print "$seq ";
375                 $dbh->do( qq{ CREATE SEQUENCE $seq } );
376         }
377
378
379         print "...\n";
380
381         $dbh->commit;
382
383 }
384
385 ## delete data before inseting ##
386 if ($opt{d}) {
387         print "deleting ";
388         foreach my $table (qw(files dvds backups shares hosts)) {
389                 print "$table ";
390                 $dbh->do(qq{ DELETE FROM $table });
391         }
392         print " done...\n";
393
394         $dbh->commit;
395 }
396
397 ## insert new values ##
398
399 # get hosts
400 $hosts = $bpc->HostInfoRead();
401 my $hostID;
402 my $shareID;
403
404 my $sth;
405
406 $sth->{insert_hosts} = $dbh->prepare(qq{
407 INSERT INTO hosts (name, IP) VALUES (?,?)
408 });
409
410 $sth->{hosts_by_name} = $dbh->prepare(qq{
411 SELECT ID FROM hosts WHERE name=?
412 });
413
414 $sth->{backups_count} = $dbh->prepare(qq{
415 SELECT COUNT(*)
416 FROM backups
417 WHERE hostID=? AND num=? AND shareid=?
418 });
419
420 $sth->{insert_backups} = $dbh->prepare(qq{
421 INSERT INTO backups (hostID, num, date, type, shareid, size)
422 VALUES (?,?,?,?,?,?)
423 });
424
425 $sth->{insert_files} = $dbh->prepare(qq{
426 INSERT INTO files
427         (shareID, backupNum, name, path, date, type, size)
428         VALUES (?,?,?,?,?,?,?)
429 });
430
431 foreach my $host_key (keys %{$hosts}) {
432
433         my $hostname = $hosts->{$host_key}->{'host'} || die "can't find host for $host_key";
434
435         $sth->{hosts_by_name}->execute($hosts->{$host_key}->{'host'});
436
437         unless (($hostID) = $sth->{hosts_by_name}->fetchrow_array()) {
438                 $sth->{insert_hosts}->execute(
439                         $hosts->{$host_key}->{'host'},
440                         $hosts->{$host_key}->{'ip'}
441                 );
442
443                 $hostID = $dbh->last_insert_id(undef,undef,'hosts',undef);
444         }
445
446         print "host ".$hosts->{$host_key}->{'host'}.": ";
447  
448         # get backups for a host
449         my @backups = $bpc->BackupInfoRead($hostname);
450         my $incs = scalar @backups;
451         print  "$incs increments\n";
452
453         my $inc_nr = 0;
454         $beenThere = {};
455
456         foreach my $backup (@backups) {
457
458                 $inc_nr++;
459                 last if ($opt{m} && $inc_nr > $opt{m});
460
461                 my $backupNum = $backup->{'num'};
462                 my @backupShares = ();
463
464                 printf("%-10s %2d/%-2d #%-2d %s %5s/%5s files (date: %s dur: %s)\n", 
465                         $hosts->{$host_key}->{'host'},
466                         $inc_nr, $incs, $backupNum, 
467                         $backup->{type} || '?',
468                         $backup->{nFilesNew} || '?', $backup->{nFiles} || '?',
469                         strftime($t_fmt,localtime($backup->{startTime})),
470                         fmt_time($backup->{endTime} - $backup->{startTime})
471                 );
472
473                 my $files = BackupPC::View->new($bpc, $hostname, \@backups, 1);
474                 foreach my $share ($files->shareList($backupNum)) {
475
476                         my $t = time();
477
478                         $shareID = getShareID($share, $hostID, $hostname);
479                 
480                         $sth->{backups_count}->execute($hostID, $backupNum, $shareID);
481                         my ($count) = $sth->{backups_count}->fetchrow_array();
482                         # skip if allready in database!
483                         next if ($count > 0);
484
485                         # dump some log
486                         print curr_time," ", $share;
487
488                         my ($f, $nf, $d, $nd, $size) = recurseDir($bpc, $hostname, $files, $backupNum, $share, "", $shareID);
489
490                         $sth->{insert_backups}->execute(
491                                 $hostID,
492                                 $backupNum,
493                                 $backup->{'endTime'},
494                                 substr($backup->{'type'},0,4),
495                                 $shareID,
496                                 $size,
497                         );
498
499                         print " commit";
500                         $dbh->commit();
501
502                         my $dur = (time() - $t) || 1;
503                         printf(" %d/%d files %d/%d dirs %0.2f MB [%.2f/s dur: %s]\n",
504                                 $nf, $f, $nd, $d,
505                                 ($size / 1024 / 1024),
506                                 ( ($f+$d) / $dur ),
507                                 fmt_time($dur)
508                         );
509
510                         hest_update($hostID, $shareID, $backupNum) if ($nf + $nd > 0);
511                 }
512
513         }
514 }
515 undef $sth;
516 $dbh->commit();
517 $dbh->disconnect();
518
519 print "total duration: ",fmt_time(time() - $start_t),"\n";
520
521 $pidfile->remove;
522
523 sub getShareID() {
524
525         my ($share, $hostID, $hostname) = @_;
526
527         $sth->{share_id} ||= $dbh->prepare(qq{
528                 SELECT ID FROM shares WHERE hostID=? AND name=?
529         });
530
531         $sth->{share_id}->execute($hostID,$share);
532
533         my ($id) = $sth->{share_id}->fetchrow_array();
534
535         return $id if (defined($id));
536
537         $sth->{insert_share} ||= $dbh->prepare(qq{
538                 INSERT INTO shares 
539                         (hostID,name,share) 
540                 VALUES (?,?,?)
541         });
542
543         my $drop_down = $hostname . '/' . $share;
544         $drop_down =~ s#//+#/#g;
545
546         $sth->{insert_share}->execute($hostID,$share, $drop_down);
547         return $dbh->last_insert_id(undef,undef,'shares',undef);
548 }
549
550 sub found_in_db {
551
552         my @data = @_;
553         shift @data;
554
555         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
556
557         return $beenThere->{$key} if (defined($beenThere->{$key}));
558
559         $sth->{file_in_db} ||= $dbh->prepare(qq{
560                 SELECT 1 FROM files
561                 WHERE shareID = ? and
562                         path = ? and 
563                         date = ? and
564                         size = ?
565                 LIMIT 1
566         });
567
568         my @param = ($shareID,$path,$date,$size);
569         $sth->{file_in_db}->execute(@param);
570         my $rows = $sth->{file_in_db}->rows;
571         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
572
573         $beenThere->{$key}++;
574
575         $sth->{'insert_files'}->execute(@data) unless ($rows);
576         return $rows;
577 }
578
579 ####################################################
580 # recursing through filesystem structure and       #
581 # and returning flattened files list               #
582 ####################################################
583 sub recurseDir($$$$$$$$) {
584
585         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
586
587         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
588
589         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
590
591         { # scope
592                 my @stack;
593
594                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
595                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
596
597                 # first, add all the entries in current directory
598                 foreach my $path_key (keys %{$filesInBackup}) {
599                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
600                         my @data = (
601                                 $shareID,
602                                 $backupNum,
603                                 $path_key,
604                                 $filesInBackup->{$path_key}->{'relPath'},
605                                 $filesInBackup->{$path_key}->{'mtime'},
606                                 $filesInBackup->{$path_key}->{'type'},
607                                 $filesInBackup->{$path_key}->{'size'}
608                         );
609
610                         my $key = join(" ", (
611                                 $shareID,
612                                 $dir,
613                                 $path_key,
614                                 $filesInBackup->{$path_key}->{'mtime'},
615                                 $filesInBackup->{$path_key}->{'size'}
616                         ));
617
618                         my $found;
619                         if (! defined($beenThere->{$key}) && ! ($found = found_in_db($key, @data)) ) {
620                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
621
622                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
623                                         $new_dirs++ unless ($found);
624                                         print STDERR " dir\n" if ($debug >= 2);
625                                 } else {
626                                         $new_files++ unless ($found);
627                                         print STDERR " file\n" if ($debug >= 2);
628                                 }
629                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
630                         }
631
632                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
633                                 $nr_dirs++;
634
635                                 my $full_path = $dir . '/' . $path_key;
636                                 push @stack, $full_path;
637                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
638
639 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
640 #
641 #                               $nr_files += $f;
642 #                               $new_files += $nf;
643 #                               $nr_dirs += $d;
644 #                               $new_dirs += $nd;
645
646                         } else {
647                                 $nr_files++;
648                         }
649                 }
650
651                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
652
653                 while ( my $dir = shift @stack ) {
654                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
655                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
656                         $nr_files += $f;
657                         $new_files += $nf;
658                         $nr_dirs += $d;
659                         $new_dirs += $nd;
660                         $size += $s;
661                 }
662         }
663
664         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
665 }
666