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