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