a0d00690e6c3c7e9ded45a3527a0ad455d0b80ec
[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::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/$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 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 ($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                         hest_update($hostID, $shareID, $backupNum) if ($nf + $nd > 0);
592                 }
593
594         }
595 }
596 undef $sth;
597 $dbh->commit();
598 $dbh->disconnect();
599
600 print "total duration: ",fmt_time(time() - $start_t),"\n";
601
602 $pidfile->remove;
603
604 sub getShareID() {
605
606         my ($share, $hostID, $hostname) = @_;
607
608         $sth->{share_id} ||= $dbh->prepare(qq{
609                 SELECT ID FROM shares WHERE hostID=? AND name=?
610         });
611
612         $sth->{share_id}->execute($hostID,$share);
613
614         my ($id) = $sth->{share_id}->fetchrow_array();
615
616         return $id if (defined($id));
617
618         $sth->{insert_share} ||= $dbh->prepare(qq{
619                 INSERT INTO shares 
620                         (hostID,name,share) 
621                 VALUES (?,?,?)
622         });
623
624         my $drop_down = $hostname . '/' . $share;
625         $drop_down =~ s#//+#/#g;
626
627         $sth->{insert_share}->execute($hostID,$share, $drop_down);
628         return $dbh->last_insert_id(undef,undef,'shares',undef);
629 }
630
631 sub found_in_db {
632
633         my @data = @_;
634         shift @data;
635
636         my ($key, $shareID,undef,$name,$path,$date,undef,$size) = @_;
637
638         return $beenThere->{$key} if (defined($beenThere->{$key}));
639
640         $sth->{file_in_db} ||= $dbh->prepare(qq{
641                 SELECT 1 FROM files
642                 WHERE shareID = ? and
643                         path = ? and 
644                         size = ? and
645                         ( date = ? or date = ? or date = ? )
646                 LIMIT 1
647         });
648
649         my @param = ($shareID,$path,$size,$date, $date-$dst_offset, $date+$dst_offset);
650         $sth->{file_in_db}->execute(@param);
651         my $rows = $sth->{file_in_db}->rows;
652         print STDERR "## found_in_db($shareID,$path,$date,$size) ",( $rows ? '+' : '-' ), join(" ",@param), "\n" if ($debug >= 3);
653
654         $beenThere->{$key}++;
655
656         $sth->{'insert_files'}->execute(@data) unless ($rows);
657         return $rows;
658 }
659
660 ####################################################
661 # recursing through filesystem structure and       #
662 # and returning flattened files list               #
663 ####################################################
664 sub recurseDir($$$$$$$$) {
665
666         my ($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID) = @_;
667
668         print STDERR "\nrecurse($hostname,$backupNum,$share,$dir,$shareID)\n" if ($debug >= 1);
669
670         my ($nr_files, $new_files, $nr_dirs, $new_dirs, $size) = (0,0,0,0,0);
671
672         { # scope
673                 my @stack;
674
675                 print STDERR "# dirAttrib($backupNum, $share, $dir)\n" if ($debug >= 2);
676                 my $filesInBackup = $files->dirAttrib($backupNum, $share, $dir);
677
678                 # first, add all the entries in current directory
679                 foreach my $path_key (keys %{$filesInBackup}) {
680                         print STDERR "# file ",Dumper($filesInBackup->{$path_key}),"\n" if ($debug >= 3);
681                         my @data = (
682                                 $shareID,
683                                 $backupNum,
684                                 $path_key,
685                                 $filesInBackup->{$path_key}->{'relPath'},
686                                 $filesInBackup->{$path_key}->{'mtime'},
687                                 $filesInBackup->{$path_key}->{'type'},
688                                 $filesInBackup->{$path_key}->{'size'}
689                         );
690
691                         my $key = join(" ", (
692                                 $shareID,
693                                 $dir,
694                                 $path_key,
695                                 $filesInBackup->{$path_key}->{'mtime'},
696                                 $filesInBackup->{$path_key}->{'size'}
697                         ));
698
699                         my $key_dst_prev = join(" ", (
700                                 $shareID,
701                                 $dir,
702                                 $path_key,
703                                 $filesInBackup->{$path_key}->{'mtime'} - $dst_offset,
704                                 $filesInBackup->{$path_key}->{'size'}
705                         ));
706
707                         my $key_dst_next = join(" ", (
708                                 $shareID,
709                                 $dir,
710                                 $path_key,
711                                 $filesInBackup->{$path_key}->{'mtime'} + $dst_offset,
712                                 $filesInBackup->{$path_key}->{'size'}
713                         ));
714
715                         my $found;
716                         if (
717                                 ! defined($beenThere->{$key}) &&
718                                 ! defined($beenThere->{$key_dst_prev}) &&
719                                 ! defined($beenThere->{$key_dst_next}) &&
720                                 ! ($found = found_in_db($key, @data))
721                         ) {
722                                 print STDERR "# key: $key [", $beenThere->{$key},"]" if ($debug >= 2);
723
724                                 if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
725                                         $new_dirs++ unless ($found);
726                                         print STDERR " dir\n" if ($debug >= 2);
727                                 } else {
728                                         $new_files++ unless ($found);
729                                         print STDERR " file\n" if ($debug >= 2);
730                                 }
731                                 $size += $filesInBackup->{$path_key}->{'size'} || 0;
732                         }
733
734                         if ($filesInBackup->{$path_key}->{'type'} == BPC_FTYPE_DIR) {
735                                 $nr_dirs++;
736
737                                 my $full_path = $dir . '/' . $path_key;
738                                 push @stack, $full_path;
739                                 print STDERR "### store to stack: $full_path\n" if ($debug >= 3);
740
741 #                               my ($f,$nf,$d,$nd) = recurseDir($bpc, $hostname, $backups, $backupNum, $share, $path_key, $shareID) unless ($beenThere->{$key});
742 #
743 #                               $nr_files += $f;
744 #                               $new_files += $nf;
745 #                               $nr_dirs += $d;
746 #                               $new_dirs += $nd;
747
748                         } else {
749                                 $nr_files++;
750                         }
751                 }
752
753                 print STDERR "## STACK ",join(", ", @stack),"\n" if ($debug >= 2);
754
755                 while ( my $dir = shift @stack ) {
756                         my ($f,$nf,$d,$nd, $s) = recurseDir($bpc, $hostname, $files, $backupNum, $share, $dir, $shareID);
757                         print STDERR "# $dir f: $f nf: $nf d: $d nd: $nd\n" if ($debug >= 1);
758                         $nr_files += $f;
759                         $new_files += $nf;
760                         $nr_dirs += $d;
761                         $new_dirs += $nd;
762                         $size += $s;
763                 }
764         }
765
766         return ($nr_files, $new_files, $nr_dirs, $new_dirs, $size);
767 }
768