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