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