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