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