#! /usr/bin/env perl

#
#   Copyright (C) Dr. Heinz-Josef Claes (2009)
#                 hjclaes@web.de
#   
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#


my $VERSION = '$Id$ ';
push @VERSION, $VERSION;
my ($VERSIONpName, $VERSIONsvnID) = $VERSION =~ /Id:\s+(\S+)\s+(\d+)/;
$main::STOREBACKUPVERSION = undef;


use strict;
use warnings;


use Digest::MD5 qw(md5_hex);
use Fcntl qw(O_RDWR O_CREAT);
use File::Copy;
use POSIX;


sub libPath
{
    my $file = shift;

    my $dir;

    # Falls Datei selbst ein symlink ist, solange folgen, bis aufgelöst
    if (-f $file)
    {
	while (-l $file)
	{
	    my $link = readlink($file);

	    if (substr($link, 0, 1) ne "/")
	    {
		$file =~ s/[^\/]+$/$link/;
	    }
	    else
	    {
		$file = $link;
	    }
	}

	($dir, $file) = &splitFileDir($file);
	$file = "/$file";
    }
    else
    {
	print STDERR "<$file> does not exist!\n";
	exit 1;
    }

    $dir .= "/../lib";           # Pfad zu den Bibliotheken
    my $oldDir = `/bin/pwd`;
    chomp $oldDir;
    if (chdir $dir)
    {
	my $absDir = `/bin/pwd`;
	chop $absDir;
	chdir $oldDir;

	return (&splitFileDir("$absDir$file"));
    }
    else
    {
	print STDERR "<$dir> does not exist, exiting\n";
    }
}
sub splitFileDir
{
    my $name = shift;

    return ('.', $name) unless ($name =~/\//);    # nur einfacher Dateiname

    my ($dir, $file) = $name =~ /^(.*)\/(.*)$/s;
    $dir = '/' if ($dir eq '');                   # gilt, falls z.B. /filename
    return ($dir, $file);
}
my ($req, $prog) = &libPath($0);
push @INC, "$req";

require 'checkParam2.pl';
require 'checkObjPar.pl';
require 'prLog.pl';
require 'version.pl';
require 'fileDir.pl';
require 'forkProc.pl';
require 'humanRead.pl';
require 'dateTools.pl';
require 'evalTools.pl';
require 'storeBackupLib.pl';

my $checkSumFile = '.md5CheckSums';

=head1 NAME

storeBackupCheckBackup.pl - checks if a file in the backup is missing or corrupted

=head1 SYNOPSIS

	storeBackupCheckBackup.pl -b backupDir [-v level] [-p number]
              [backupRoot . . .]

=head1 DESCRIPTION

This program calculates md5 sums from the files in the backup and compares
them with md5 sums stored by storeBackup.pl.
It so will recognize, if a file in the backup is missing or currupted.
It only checks plain files, not special files or symbolic links.

=head1 OPTIONS

=over 8

=item B<--print>

    print configuration parameters and stop

=item B<--backupDir> F<backupDir>, B<-b> F<backupDir>

    top level directory of all backups

=item B<--verbose>, B<-v>

    generate some extra messages

=item B<--parJobs>, B<-p>

    number of parallel jobs, default = choosen automaticly

=item [backupRoot]

    Root directories of backups where to search relative
    to backupDir. If no directories are specified, all
    backups below backupDir are chosen.

=back

=head1 COPYRIGHT

Copyright (c) 2008 by Heinz-Josef Claes (see README)
Published under the GNU General Public License v3 or any later version

=cut

my $Help = join('', grep(!/^\s*$/, `pod2text $0`));
$Help = "cannot find pod2text, see documentation for details\n"
    unless $Help;

&printVersions(\@ARGV, '-V');

my $CheckPar =
    CheckParam->new('-allowLists' => 'yes',
		    '-list' => [Option->new('-name' => 'backupDir',
					    '-cl_option' => '-b',
					    '-cl_alias' => '--backupDir',
					    '-cf_key' => 'backupDir',
					    '-param' => 'yes'),
				Option->new('-name' => 'verbose',
					    '-cl_option' => '-v',
					    '-cl_alias' => '--verbose'),
				Option->new('-name' => 'parJobs',
					    '-cl_option' => '-p',
					    '-cl_alias' => '--parJobs',
					    '-param' => 'yes',
					    '-pattern' => '\A[1-9]\d*\Z'),
				Option->new('-name' => 'print',
					    '-cl_option' => '--print')
		    ]
    );


$CheckPar->check('-argv' => \@ARGV,
                 '-help' => $Help
                 );

# Auswertung der Parameter
my $print = $CheckPar->getOptWithoutPar('print');
my $backupDir = $CheckPar->getOptWithPar('backupDir');
my $parJobs = $CheckPar->getOptWithPar('parJobs');
my $verbose = $CheckPar->getOptWithoutPar('verbose');
my (@backupRoot) = $CheckPar->getListPar();


unless ($parJobs)
{
    local *FILE;
    if (open(FILE, "/proc/cpuinfo"))
    {
	my $l;
	$parJobs = 1;
	while ($l = <FILE>)
	{
	    $parJobs++ if $l =~ /processor/;
	}
	close(FILE);
	$parJobs *= 3;
    }
    $parJobs = 3 if $parJobs < 3;
}



if ($print)
{
    $CheckPar->print();
    exit 0;
}

my $prLog = printLog->new('-kind' => ['I:INFO', 'W:WARNING', 'E:ERROR',
				      'S:STATISTIC', 'D:DEBUG', 'V:VERSION']);

#$prLog->print('-kind' => 'V',
#	      '-str' => ["$VERSIONpName, $main::STOREBACKUPVERSION, " .
#			 "build $VERSIONsvnID"]);

$prLog->print('-kind' => 'E',
	      '-str' => ["missing parameter backupDir\n$Help"],
	      '-exit' => 1)
    unless defined $backupDir;
$prLog->print('-kind' => 'E',
	      '-str' => ["backupDir directory <$backupDir> does not exist " .
	      "or is not accesible"],
	      '-exit' => 1)
    unless -r $backupDir;


my $allLinks = lateLinks->new('-dirs' => [$backupDir],
			      '-kind' => 'recursiveSearch',
			      '-verbose' => 0,
			      '-prLog' => $prLog);

my $allStbuDirs = $allLinks->getAllStoreBackupDirs();


# filter the relevant backups
my (@dirsToCheck) = ();
if (@backupRoot)
{
    my $d;
    foreach $d (@backupRoot)
    {
	unless ($d =~ m#\A/#)
	{
	    $d = "$backupDir/$d";
	}
	$prLog->print('-kind' => 'E',
		      '-str' => ["directory <$d> does not exist " .
				 "or is not accesible"],
		      '-exit' => 1)
	    unless -r $d;
	$d = &::absolutePath($d);
	$prLog->print('-kind' => 'E',
		      '-str' => ["directory <$d> is not a subdirectory " .
				 "of backupDir <$backupDir>"],
		      '-exit' => 1)
	    unless $d =~ /\A$backupDir/;

	# now get all dirs from @$allStbuDirs below $d
	my $a;
	foreach $a (@$allStbuDirs)
	{
	    push @dirsToCheck, $a
		if $a =~ /\A$d\//s or $a =~ /\A$d\z/s;
	}
    }
    (@dirsToCheck) = sort { $a cmp $b } @dirsToCheck;
}
else
{
    (@dirsToCheck) = sort { $a cmp $b } @$allStbuDirs;
}


$prLog->print('-kind' => 'E',
	      '-str' => ["nothing to search, no backup directories specified"],
	      '-exit' => 1)
    unless @dirsToCheck;

{
    my (@out, $d);
    foreach $d (@dirsToCheck)
    {
	push @out, "  $d";
    }
    $prLog->print('-kind' => 'I',
		  '-str' => ["backup directories to check", @out]);
}

my $parFork = parallelFork->new('-maxParallel' => $parJobs,
				'-prLog' => $prLog);
my $tinySched = tinyWaitScheduler->new('-prLog' => $prLog);

my $rcsf = undef;
my ($meta, $postfix, $uncompr, @uncomprPar);
my $dirToCheck = undef;
my $jobToDo = 1;
my $parForkToDo = 0;
my (%usedInodes, %filesFromMD5CheckSumFile, $prevDirToCheck);
while (defined($rcsf) or $jobToDo > 0 or $parForkToDo > 0)
{
    ############################################################
    my $old = $parFork->checkOne();
    if ($old)
    {
	my ($tmpN, $fn) = @{$old->get('-what' => 'info')};
	local *IN;
	unless (open(IN, "< $tmpN"))
	{
	    $prLog->print('-kind' => 'E',
			  '-str' => ["cannot temporary information file " .
				     "<$tmpN> of <$fn>"]);
	    next;
	}
	my $l;
	while ($l = <IN>)
	{
	    chop $l;
	    my ($what, @l) = split(/\s/, $l, 2);
	    if ($what eq 'corrupt')
	    {
		delete $usedInodes{$l[0]};
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["md5 sum mismatch for <$fn>"]);
	    }
	    else
	    {
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["unknown command <$what> in information file " .
			       "<$tmpN> of $fn"]);
	    }
	}
	unlink $tmpN;
    }

    ############################################################
    $jobToDo = @dirsToCheck;
    if (($rcsf or $jobToDo > 0) and $parFork->getNoFreeEntries() > 0)
    {
	unless ($rcsf)
	{
	    $prevDirToCheck = $dirToCheck;
	    $dirToCheck = shift @dirsToCheck;
	    last unless $dirToCheck;

	    unless (-r "$dirToCheck/$checkSumFile" or
		    -r "$dirToCheck/$checkSumFile.bz2")
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["no readable <$checkSumFile> in " .
					 "<$dirToCheck> ... skipping"]);
		next;
	    }
	    if (-f "$dirToCheck/$checkSumFile.notFinished")
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["backup <$dirToCheck> not finished" .
					 " ... skipping"]);
		next;
	    }

#	    my $fm;
#	    foreach $fm (keys %filesFromMD5CheckSumFile)
#	    {
#		print "###$prevDirToCheck##$fm###\n";
#	    }
	    &checkAllFiles($prevDirToCheck, \%filesFromMD5CheckSumFile,
			   $prLog)
		if %filesFromMD5CheckSumFile;
	    %filesFromMD5CheckSumFile = ();

	    $rcsf =
		readCheckSumFile->new('-checkSumFile' =>
				      "$dirToCheck/$checkSumFile",
				      '-prLog' => $prLog);
	    $meta = $rcsf->getMetaValField();
	    $postfix = ($$meta{'postfix'})->[0];    # postfix for compression
	    my $writeExcludeLog = ($$meta{'writeExcludeLog'})->[0];
	    my $logInBackupDir = ($$meta{'logInBackupDir'})->[0];
	    my $compressLogInBackupDir =
		($$meta{'compressLogInBackupDir'})->[0];
	    ($uncompr, @uncomprPar) = @{$$meta{'uncompress'}};
	    $filesFromMD5CheckSumFile{'.md5BlockCheckSums.bz2'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums.bz2'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums.info'} = 1;
	    $filesFromMD5CheckSumFile{'.storeBackupLinks'} = 1;
	    $filesFromMD5CheckSumFile{'.storeBackup.log'} = 1
		if $logInBackupDir eq 'yes';
	    $filesFromMD5CheckSumFile{'.storeBackup.log.bz2'} = 1
		if $logInBackupDir eq 'yes' and
		$compressLogInBackupDir eq 'yes';
	    $filesFromMD5CheckSumFile{'.storeBackup.notSaved.bz2'} = 1
		if $logInBackupDir eq 'yes';


	    $prLog->print('-kind' => 'I',
			  '-str' => ["checking <$dirToCheck> ..."])
		if $verbose;
	}
	my ($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	    $size, $uid, $gid, $mode, $f);
	if ((($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
		 $size, $uid, $gid, $mode, $f) = $rcsf->nextLine()) > 0)
	{
	    $f .= $postfix if $compr eq 'c';
	    $filesFromMD5CheckSumFile{$f} = 1;
#print "new file <$f>\n";
	    my $filename = "$dirToCheck/$f";
	    if (length($md5sum) == 32)
	    {
#print "-1-\n";
		if ($compr ne 'b')
		{
		    $filesFromMD5CheckSumFile{$f} = 2; # blocked file
		    my $inode = (stat($filename))[1];
#print "-2- inode = $inode, $f\n";
		    if ($inode)
		    {
#print "-2.5- inodes = ", join(' ', keys %usedInodes), "\n";
			if (exists $usedInodes{$inode})
			{
#print "-3-\n";
			    next;
			}
			$usedInodes{$inode} = 1;
		    }
		}
		unless (-e $filename)
		{
		    $prLog->print('-kind' => 'E',
				  '-str' => ["file <$filename> is missing"]);
#print "-4-\n";
		    next;
		}
		if (index('ucb', $compr) < 0)
		{
		    $prLog->print('-kind' => 'E',
				  '-print' =>
				  ["unknown value compr =<$compr> at <" .
				   "$dirToCheck/$checkSumFile>, filename = <$f>"]);
#print "-5-\n";
		    next;
		}

#print "-6-\n";
		my $tmpName = &::uniqFileName("/tmp/storeBackup-block.");
		$parFork->add_noblock('-function' => \&checkMD5,
				      '-funcPar' =>
				      [$dirToCheck, $filename, $md5sum, $compr,
				       $postfix, $uncompr, \@uncomprPar,
				       10*1024**2, $tmpName, $prLog],
				      '-info' => [$tmpName, $filename]);
	    }
	    elsif ($md5sum eq 'dir')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["directory <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a directory!"])
		    unless -d $filename;
	    }
	    elsif ($md5sum eq 'symlink')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["<$filename> is missing or not a symlnk!"])
		    unless -l $filename;
	    }
	    elsif ($md5sum eq 'pipe')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["named pipe <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a named pipe!"])
		    unless -p $filename;
	    }
	    elsif ($md5sum eq 'pipe')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["named pipe <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a named pipe!"])
		    unless -p $filename;
	    }
	    elsif ($md5sum eq 'socket')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["socket <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a socket!"])
		    unless -S $filename;
	    }
	    elsif ($md5sum eq 'blockdev')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["block device <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a block device!"])
		    unless -b $filename;
	    }
	    elsif ($md5sum eq 'chardev')
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["char device <$filename is missing>"])
		    unless -e $filename;
		$prLog->print('-kind' => 'E',
			      '-str' => ["<$filename> is not a char device!"])
		    unless -c $filename;
	    }

	    $tinySched->reset();
	}
	else
	{
#print "1 rcsf = undef\n";
	    $rcsf = undef;
	}
    }

    ############################################################
    $tinySched->wait();

    $parForkToDo = $parFork->getNoUsedEntries();
#print "2 rcsf = $rcsf, parForkToDo = $parForkToDo\n";
}
#print "--------------ENDE---------\n";
#	    my $fm;
#	    foreach $fm (keys %filesFromMD5CheckSumFile)
#	    {
#		print "###$dirToCheck##$fm###\n";
#	    }
&checkAllFiles($dirToCheck, \%filesFromMD5CheckSumFile,
	       $prLog)
    if %filesFromMD5CheckSumFile;


my $enc = $prLog->encountered('-kind' => 'W');
my $S = $enc > 1 ? 'S' : '';
if ($enc)
{
    $prLog->print('-kind' => 'W',
		  '-str' => ["-- $enc WARNING$S OCCURED DURING THE CHECK! --"])
}
else
{
    $prLog->print('-kind' => 'I',
		  '-str' => ["-- no WARNINGS OCCURED DURING THE CHECK! --"]);
}

$enc = $prLog->encountered('-kind' => 'E');
$S = $enc > 1 ? 'S' : '';
if ($enc)
{
    $prLog->print('-kind' => 'E',
		  '-str' => ["-- $enc ERROR$S OCCURED DURING THE CHECK! --"]);
}
else
{
    $prLog->print('-kind' => 'I',
		  '-str' => ["-- no ERRORS OCCURED DURING THE CHECK! --"]);
}


if ($prLog->encountered('-kind' => "E"))
{
    exit 1;
}
else
{
    exit 0;
}


############################################################
sub checkMD5
{
    my ($dirToCheck, $f, $md5sum, $compr, $postfix, $uncompr, $uncomprPar,
	$blockSize, $tmpName, $prLog) = @_;

    local *OUT;
    open(OUT, "> $tmpName") or
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot open <$tmpName>"],
		      '-add' => [__FILE__, __LINE__],
		      '-exit' => 1);

# print "-------------$f----------, $md5sum\n";
    if (length($md5sum) == 32)
    {
	if ($compr eq 'u' or $compr eq 'c')
	{
	    my $md5All = Digest::MD5->new();
	    local *FILE;
	    my $fileIn = undef;
	    unless (-e $f)
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["file <$f> is missing"]);
		return 1;
	    }
	    if ($compr eq 'u')
	    {
		unless (sysopen(FILE, $f, O_RDONLY))
		{
		    $prLog->print('-kind' => 'E',
				  '-str' => ["cannot open <$f>"],
				  '-add' => [__FILE__, __LINE__]);
		    return 1;
		}
	    }
	    else
	    {
		$fileIn =
		    pipeFromFork->new('-exec' => $uncompr,
				      '-param' => \@uncomprPar,
				      '-stdin' => $f,
				      '-outRandom' => '/tmp/stbuPipeFrom10-',
				      '-prLog' => $prLog);
	    }
	    my $buffer;
	    while ($fileIn ? $fileIn->sysread(\$buffer, $blockSize) :
		   sysread(FILE, $buffer, $blockSize))
	    {
		$md5All->add($buffer);
	    }

	    if ($md5sum ne $md5All->hexdigest())
	    {
		my $inode = (stat($f))[1];
		print OUT "corrupt $inode $f\n";
#		print "md5 sum does not match at <$f>\n";
	    }
	    else
	    {
#		print "checked <$f>\n";
	    }
	    if ($fileIn)
	    {
		$fileIn->close();
		$fileIn = undef;
	    }
	    else
	    {
		close(FILE);
	    }
	}
	elsif ($compr eq 'b')
	{
	    # read all files in directory
	    local *DIR;
	    unless (opendir(DIR, $f))
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["cannot open <$f>"],
			      '-add' => [__FILE__, __LINE__]);
		return 1;
	    }
	    my ($entry, @entries, %inode2md5);
	    while ($entry = readdir DIR)  # one entry per inode
	    {
		next unless $entry =~ /\A\d/;
		
		push @entries, $entry;
	    }
	    close(DIR);
	    my $fileIn =
		pipeFromFork->new('-exec' => 'bzip2',
				  '-param' => ['-d'],
				  '-stdin' => "$f/.md5BlockCheckSums.bz2",
				  '-outRandom' => '/tmp/stbuPipeFrom11-',
				  '-prLog' => $prLog);

	    my $l;
#	    while ($l = <FILE>)
	    while ($l = $fileIn->read())
	    {
		chomp $l;
		my ($l_md5, $l_compr, $l_f, $n);
		$n = ($l_md5, $l_compr, $l_f) = split(/\s/, $l, 3);
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["strange line in <$f/.md5BlockCheckSums.bz2> " .
			       "in line $.:", "\t<$l>"],
			      '-exit' => 1)
		    if ($n != 3);
		if (-e "$dirToCheck/$l_f")
		{
		    my $inode = (stat("$dirToCheck/$l_f"))[1];
		    $inode2md5{$inode} = $l_md5;
#		    print "$l_f: $inode -> $l_md5\n";
		}
		else
		{
		    $prLog->print('-kind' => 'E',
				  '-str' => ["<$l_f> is missing"]);
		}
	    }
#	    close(FILE);
	    $fileIn->close();
	    $fileIn = undef;

	    my $md5All = Digest::MD5->new();
	    foreach $entry (sort @entries)
	    {
		my $inode = (stat("$f/$entry"))[1];

		local *FROM;
		my $fileIn = undef;
		if ($entry =~ /$postfix\Z/)    # compressed block
		{
		    $fileIn =
			pipeFromFork->new('-exec' => $uncompr,
					  '-param' => \@uncomprPar,
					  '-stdin' => "$f/$entry",
					  '-outRandom' => '/tmp/stbuPipeFrom12-',
					  '-prLog' => $prLog);
		}
		else           # block not compressed
		{
		    unless (sysopen(FROM, "$f/$entry", O_RDONLY))
		    {
			$prLog->print('-kind' => 'E',
				      '-str' => ["cannot read <$f/$entry>"]);
			return 1;
		    }
		}
		my $buffer;
		my $md5Block = Digest::MD5->new();
		my $size;
		while ($size = $fileIn ? $fileIn->sysread(\$buffer, $blockSize) :
		       sysread(FROM, $buffer, $blockSize))
		{
		    $md5All->add($buffer);
		    $md5Block->add($buffer);
		}
		if ($fileIn)
		{
		    $fileIn->close();
		    $fileIn = undef;
		}
		else
		{
		    close(FILE);
		}

		my $digest = $md5Block->hexdigest();
#		print "$f/$entry:\n";
#		print "\t$digest = digest\n";
#		print "\t", $inode2md5{$inode}, " = inode ($inode)\n";
		if ($digest ne $inode2md5{$inode})
		{
		    $prLog->print('-kind' => 'E',
				  '-str' =>
				  ["calculated md5 sum of <$f/$entry> is " .
				   "different from the one in " .
				   "<$f/.md5BlockCheckSums.bz2>"]);
		}
	    }

	    if ($md5sum ne $md5All->hexdigest())
	    {
		my $inode = (stat($f))[1];
		print OUT "corrupt $inode";
	    }
	    else
	    {
#		print "checked <$f>\n";
	    }
	}
    }

    close(OUT);
    return 0;
}


############################################################
# check if all files in $dir are in the hash
sub checkAllFiles
{
    my ($dir, $relFiles, $prLog) = @_;

    &_checkAllFiles(length($dir)+1, $dir, $relFiles, $prLog);
}


############################################################
sub _checkAllFiles
{
    my ($length, $dir, $relFiles, $prLog) = @_;

#print "_checkAllFiles: $length, $dir\n";

    my $rel = undef;
    if (length($dir) > $length)
    {
	$rel = substr($dir, $length);
    }
    if ($rel)
    {
#	print "\tcheck-> <$rel>\n";
	$prLog->print('-kind' => 'E',
		      '-str' => ["<$rel> is not listed in .md5CheckSum"])
	    unless exists $$relFiles{$rel};
    }

    local *DIR;
    unless (opendir(DIR, $dir))
    {
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot opendir <$dir>"]);
	return;
    }
    my $e;
    while ($e = readdir DIR)
    {
	next if ($e eq '.' or $e eq '..');
	my $de = "$dir/$e";
	if (-d $de and not -l $de)   # is a directory
	{
	    if ($$relFiles{$rel} == 1)  # not a blocked file
	    {
		&_checkAllFiles($length, $de, $relFiles, $prLog);
	    }
	    next;
	}
	$rel = substr($de, $length);
#	print "\tcheck-> <$rel>\n";
	$prLog->print('-kind' => 'E',
		      '-str' => ["<$rel> is not listed in .md5CheckSum"])
	    unless exists $$relFiles{$rel};
    }
    closedir(DIR) or
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot closedir <$dir>"]);
}
