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