#! /usr/bin/env perl

#
#   Copyright (C) Dr. Heinz-Josef Claes (2012)
#                 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 -c backupDir [-v level] [-p number] [-i]

=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<--checkDir>, B<-c>

    backup or top of backups to check

=item B<--backupRoot>, B<-b>

    root of storeBackup tree, normally not needed

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

    generate statistics

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

    number of parallel jobs, default = chosen automatically

=item B<-i>, <--includeRenamedBackups>

    include renamed backups into the check
    renamed backups must follow the convention <backupDir>-<something>

=back

=head1 COPYRIGHT

Copyright (c) 2008-2012 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' => 'no',
		    '-list' => [Option->new('-name' => 'checkBackup',
					    '-cl_option' => '-c',
					    '-cl_alias' => '--checkBackup',
					    '-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'),
				Option->new('-name' => 'includeRenamedBackups',
					    '-cl_option' => '-i',
					    '-cl_alias' =>
					    '--includeRenamedBackups')
		    ]
    );


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

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


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 $prL = printLog->new('-kind' => ['I:INFO', 'W:WARNING', 'E:ERROR',
				    'S:STATISTIC', 'D:DEBUG', 'V:VERSION']);
my $prLog = printLogMultiple->new('-prLogs' => [$prL]);


$backupDir =~ s/\/+\Z//;      # remove / at the end
$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;

if (-l $backupDir)
{
    my $newLink = ::absolutePath($backupDir);
    $prLog->print('-kind' => 'I',
		  '-str' =>
		  ["replacing symlink <$backupDir> with <$newLink>"]);
    $backupDir = $newLink;
}


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

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


# filter the relevant backups
my (@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, $d);
    (@d) = (@dirsToCheck);
    (@dirsToCheck) = ();
    foreach $d (@d)
    {
	unless (-r "$d/$checkSumFile" or
		-r "$d/$checkSumFile.bz2")
	{
	    $prLog->print('-kind' => 'E',
			  '-str' => ["no readable <$checkSumFile> in " .
				     "<$d> ... skipping"]);
	    next;
	}
	if (-e "$d/$checkSumFile.notFinished")
	{
	    $prLog->print('-kind' => 'E',
			  '-str' => ["backup <$d> not finished" .
				     " ... skipping"]);
	    next;
	}
	if (-e "$d/.storeBackupLinks/linkFile.bz2")
	{
	    $prLog->print('-kind' => 'E',
			  '-str' =>
			  ["backup <$d> needs run of " .
			   "~/svn/stbu/bin/storeBackupUpdateBackup.pl" .
			   " ... skipping"]);
	    next;
	}

	push @out, "  $d";
	push @dirsToCheck, $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;
# statistical data per backup:
my $checkedFiles = 0;
my $checkedFilesSize = 0; # uses size of data in backup (eg. compressed)
my $linkedFiles = 0;
my $linkedFilesSize = 0;  # uses size of data in backup (eg. compressed)
# sums of statistical data:
my $checkedFilesAll = 0;
my $checkedFilesSizeAll = 0;
my $linkedFilesAll = 0;
my $linkedFilesSizeAll = 0;

# %usedInodes
## if inode was checked, the inode is used as key, value set to 1
## if md5sum check of file has different value that stored in .md5CheckSums,
## inode is marked as 'corrupt' and deleted from that table (so all affected
## files are reported)
# %filesFromMD5CheckSumFile
## key = relativ filename
## value pre-set to 1, then changed to 2 if file is not a blocked file
## sub checkAllFiles checks, if all files in the backup are in this hash
# %usedBlockInodes
## stores inodes already used in blocks
my (%usedInodes, %filesFromMD5CheckSumFile, $prevDirToCheck,
    %usedBlockInodes);

while (defined($rcsf) or $jobToDo > 0 or $parForkToDo > 0)
{
#print "--o--0--jobToDo=$jobToDo-parForkToDo=$parForkToDo-\n";
    ############################################################
    my $old = $parFork->checkOne();
    if ($old)
    {
	my ($tmpN, $fn) = @{$old->get('-what' => 'info')};
#print "--o--1-$fn-\n";
	local *IN;
	unless (&::waitForFile($tmpN) or
		open(IN, "< $tmpN"))
	{
	    $prLog->print('-kind' => 'E',
			  '-str' => ["cannot open temporary information file " .
				     "<$tmpN> of <$fn>"]);
	    next;
	}
#print "--o--2-\n";
	my $l;
	while ($l = <IN>)
	{
	    chop $l;
#print "--o--3--$l\n";
	    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>"]);
	    }
	    elsif ($what eq 'errors')
	    {
		$prLog->addEncounter('-kind' => 'E',
				     '-add' => $l[0]);
	    }
	    elsif ($what eq 'checkedFiles')
	    {
		$checkedFiles += $l[0];
	    }
	    elsif ($what eq 'checkedFilesSize')
	    {
		$checkedFilesSize += $l[0];
	    }
	    elsif ($what eq 'linkedFiles')
	    {
		$linkedFiles += $l[0];
	    }
	    elsif ($what eq 'linkedFilesSize')
	    {
		$linkedFilesSize += $l[0];
	    }
	    elsif ($what eq 'calcedInodes')
	    {
		while ($l = <IN>)
		{
		    chop $l;
		    $usedBlockInodes{$l} = 1;
		}
		last;
	    }
	    else
	    {
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["unknown command <$what> in information file " .
			       "<$tmpN> of $fn"]);
	    }
	}

	close(IN);
	unlink $tmpN;
    }
#print "--o--4-\n";

    ############################################################
    $jobToDo = @dirsToCheck;
    if (($rcsf or $jobToDo > 0) and $parFork->getNoFreeEntries() > 0)
    {
#print "----0----open $dirToCheck/$checkSumFile\n";
#print "---jobToDo = $jobToDo, freeEntries=", $parFork->getNoFreeEntries(), ", usedEntries=", $parFork->getNoUsedEntries(), "\n";

	unless ($rcsf)
	{
#print "---0.05---\n";
	    # Wait until all files of the previous actual backup
	    # are calculated. Do this so 
	    if ($parFork->getNoUsedEntries() != 0)
	    {
		$tinySched->wait();
		next;
	    }
	    # ok, all files of old backup are finished,
	    # now begin with the next one
	    if ($verbose)
	    {
		&printStat($prLog, '', $checkedFiles, $checkedFilesSize,
			   $linkedFiles, $linkedFilesSize)
		    if $checkedFilesSize + $linkedFilesSize > 0;
		        # avoid first printout before any checks
	    }
	    $checkedFilesAll += $checkedFiles;
	    $checkedFilesSizeAll += $checkedFilesSize;
	    $linkedFilesAll += $linkedFiles;
	    $linkedFilesSizeAll += $linkedFilesSize;

	    $checkedFiles = 0;
	    $checkedFilesSize = 0;
	    $linkedFiles = 0;
	    $linkedFilesSize = 0;

#print "---0.1---\n";
	    $prevDirToCheck = $dirToCheck;
	    $dirToCheck = shift @dirsToCheck;
	    last unless $dirToCheck;

#print "---0.2---\n";

	    &checkAllFiles($prevDirToCheck, \%filesFromMD5CheckSumFile,
			   $prLog)
		if %filesFromMD5CheckSumFile;
	    %filesFromMD5CheckSumFile = ();

	    $rcsf = readCheckSumFile->new('-checkSumFile' =>
					  "$dirToCheck/$checkSumFile",
					  '-prLog' => $prLog);
	    $postfix = $rcsf->getInfoWithPar('postfix');
	    my $writeExcludeLog = $rcsf->getInfoWithPar('writeExcludeLog');
	    my $logInBackupDir = $rcsf->getInfoWithPar('logInBackupDir');
	    my $compressLogInBackupDir =
		$rcsf->getInfoWithPar('compressLogInBackupDir');
	    my $logInBackupDirFileName =
		$rcsf->getInfoWithPar('logInBackupDirFileName');
	    $logInBackupDirFileName .= '.bz2'
		if $compressLogInBackupDir eq 'yes';
	     ($uncompr, @uncomprPar) = @{$rcsf->getInfoWithPar('uncompress')};

	    $filesFromMD5CheckSumFile{'.md5BlockCheckSums.bz2'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums.bz2'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums'} = 1;
	    $filesFromMD5CheckSumFile{'.md5CheckSums.info'} = 1;
	    $filesFromMD5CheckSumFile{'.storeBackupLinks'} = 1;
	    $filesFromMD5CheckSumFile{'.storeBackup.notSaved.bz2'} = 1
		if $writeExcludeLog eq 'yes';
	    if ($logInBackupDir eq 'yes')
	    {
		if ($logInBackupDirFileName)
		{
		    $filesFromMD5CheckSumFile{$logInBackupDirFileName} = 1;
		}
		else
		{
		    $filesFromMD5CheckSumFile{'.storeBackup.log'} = 1;
		    $filesFromMD5CheckSumFile{'.storeBackup.notSaved.bz2'} = 1;

		    $filesFromMD5CheckSumFile{'.storeBackup.log.bz2'} = 1
			if $compressLogInBackupDir 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";
#print "    $dirToCheck/$checkSumFile\n";
	    my $filename = "$dirToCheck/$f";
	    if (length($md5sum) == 32)
	    {
#print "-1-$compr-\n";
		if ($compr ne 'b')
		{
		    $filesFromMD5CheckSumFile{$f} = 2; # not a blocked file
		    my ($inode, $sizeBackup) = (stat($filename))[1,7];
#print "-2- inode = $inode, $f\n";
		    if ($inode)
		    {
#print "-2.5- inodes = ", join(' ', keys %usedInodes), "\n";
			if (exists $usedInodes{$inode})
			{
#print "-3-\n";
			    ++$linkedFiles;
			    $linkedFilesSize += $sizeBackup;
			    next;
			}
			$usedInodes{$inode} = 1;
		    }
		}
#print "-3.5-filesFromMD5CheckSumFile\{$f\}=", $filesFromMD5CheckSumFile{$f}, "-\n";
		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,
				       \%usedBlockInodes,
				       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;
	    }

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

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

    $parForkToDo = $parFork->getNoUsedEntries();
#print "2 parForkToDo = $parForkToDo\n";
}     # end of global while loop over all jobs

#print "--------------END---------\n";
#	    my $fm;
#	    foreach $fm (keys %filesFromMD5CheckSumFile)
#	    {
#print "###$dirToCheck##$fm###",
#		$filesFromMD5CheckSumFile{$fm}, "###\n";
#	    }

#&checkAllFiles($dirToCheck, \%filesFromMD5CheckSumFile,
#	       $prLog)
#    if %filesFromMD5CheckSumFile;

if ($verbose)
{
    &printStat($prLog, '', $checkedFiles, $checkedFilesSize,
	       $linkedFiles, $linkedFilesSize);

    $checkedFilesAll += $checkedFiles;
    $checkedFilesSizeAll += $checkedFilesSize;
    $linkedFilesAll += $linkedFiles;
    $linkedFilesSizeAll += $linkedFilesSize;
    &printStat($prLog, 'overall', $checkedFilesAll, $checkedFilesSizeAll,
	       $linkedFilesAll, $linkedFilesSizeAll);
}

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

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


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


############################################################
# checks calculated md5 sum against stored one(s)
# this function is called for all files (blocked and non-blocked
sub checkMD5
{
    my ($dirToCheck, $f, $md5sum, $compr, $postfix, $uncompr, $uncomprPar,
	$usedBlockInodes, $blockSize, $tmpName, $prLog) = @_;

    my (%inode2md5);   # store calculated md5 sum of new inodes

    # statistical data per checkMD5
    my $checkedFiles = 0;
    my $checkedFilesSize = 0;
    my $linkedFiles = 0;
    my $linkedFilesSize = 0;

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

#print "checkMD5 -------------$f----------, $md5sum, ", length($md5sum), ", compr=$compr\n";
    my $nrErrors = 0;
    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"]);
		++$nrErrors;
		print OUT "errors $nrErrors\n";
		return 1;
	    }
	    if ($compr eq 'u')
	    {
		unless (sysopen(FILE, $f, O_RDONLY))
		{
		    $prLog->print('-kind' => 'E',
				  '-str' => ["cannot open <$f>"],
				  '-add' => [__FILE__, __LINE__]);
		    ++$nrErrors;
		    print OUT "errors $nrErrors\n";
		    return 1;
		}
	    }
	    else
	    {
		$fileIn =
		    pipeFromFork->new('-exec' => $uncompr,
				      '-param' => \@uncomprPar,
				      '-stdin' => $f,
				      '-outRandom' => '/tmp/stbuPipeFrom10-',
				      '-prLog' => $prLog);
	    }

	    my ($inode, $size) = (stat($f))[1,7];
	    ++$checkedFiles;
	    $checkedFilesSize += $size;
	    my $buffer;
	    while ($fileIn ? $fileIn->sysread(\$buffer, $blockSize) :
		   sysread(FILE, $buffer, $blockSize))
	    {
		$md5All->add($buffer);
	    }

	    if ($md5sum ne $md5All->hexdigest())
	    {
		print OUT "corrupt $inode $f\n";
	    }
	    else
	    {
		# ready
	    }
	    if ($fileIn)
	    {
		$fileIn->close();
		$fileIn = undef;
	    }
	    else
	    {
		close(FILE);
	    }
	}                       # if ($compr eq 'u' or $compr eq 'c')
	elsif ($compr eq 'b')
	{
#print "start checking blocked file $f\n";
	    # read all files in directory
	    local *DIR;
	    unless (opendir(DIR, $f))
	    {
		$prLog->print('-kind' => 'E',
			      '-str' => ["cannot open <$f>"],
			      '-add' => [__FILE__, __LINE__]);
		++$nrErrors;
		print OUT "errors $nrErrors\n";
		return 1;
	    }
	    my ($entry, @entries);
	    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 = $fileIn->read())
	    {
		chomp $l;
		my ($l_md5, $l_compr, $l_f, $n);
		$n = ($l_md5, $l_compr, $l_f) = split(/\s/, $l, 3);
		if ($n != 3)
		{
		    ++$nrErrors;
		    print OUT "errors $nrErrors\n";
		    $prLog->print('-kind' => 'E',
				  '-str' =>
				  ["strange line in <$f/.md5BlockCheckSums.bz2> " .
				   "in line " . $fileIn->get('-what' => 'lineNr') .
				   ":", "\t<$l>"],
				  '-exit' => 1)
		}
		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' => ["file <$l_f> is missing"]);
		}
	    }
	    $fileIn->close();
	    $fileIn = undef;

	    my $md5All = Digest::MD5->new();
	    foreach $entry (sort @entries)  # loop over all files in backup
	    {
		my ($inode, $size) = (stat("$f/$entry"))[1,7];

		my $calcMD5 = 0;
		if (exists $$usedBlockInodes{$inode})
		{
#print "inode already calculated: $inode\n";
		    ++$linkedFiles;
		    $linkedFilesSize += $size;
		}
		else
		{
		    ++$checkedFiles;
		    $checkedFilesSize += $size;
		    $calcMD5 = 1;
		}
		$$usedBlockInodes{$inode} = $inode;

		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>"]);
			++$nrErrors;
			print OUT "errors $nrErrors\n";
			return 1;
		    }
		}
		my $buffer;
		my $md5Block = Digest::MD5->new() if $calcMD5;
		while ($fileIn ? $size = $fileIn->sysread(\$buffer, $blockSize) :
		       sysread(FROM, $buffer, $blockSize))
		{
		    $md5All->add($buffer);
		    $md5Block->add($buffer) if $calcMD5;
		}
		if ($fileIn)
		{
		    $fileIn->close();
		    $fileIn = undef;
		}
		else
		{
		    close(FILE);
		}

		my $digest = $md5Block->hexdigest() if $calcMD5;
#print "$f/$entry:\n";
#print "\t$digest = digest\n";
#print "\t", $inode2md5{$inode}, " = inode ($inode)\ if $calcMD5;n";
		if (not exists $inode2md5{$inode})
		{
		    ++$nrErrors;
		    print OUT "errors $nrErrors\n";
		    $prLog->print('-kind' => 'E',
				  '-str' =>
				  ["<$f/$entry> is missing in " .
				   "<$f/.md5BlockCheckSums.bz2>"]);
		}
		elsif ($calcMD5 and $digest ne $inode2md5{$inode})
		{
		    ++$nrErrors;
		    print OUT "errors $nrErrors\n";
		    $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 "end checking blocked file $f\n";
		# ready
	    }
#print "checked <$f>\n";
	}
    }

    print OUT "errors $nrErrors\n";
    print OUT "checkedFiles $checkedFiles\n";
    print OUT "checkedFilesSize $checkedFilesSize\n";
    print OUT "linkedFiles $linkedFiles\n";
    print OUT "linkedFilesSize $linkedFilesSize\n";

    print OUT "calcedInodes\n";
    my $i;
    foreach $i (keys %inode2md5)
    {
	print OUT "$i\n";
    }

    close(OUT);
    return 0;
}


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

#print "-1- keys relFiles=\n\t", join("\n\t", sort keys %$relFiles), "\n";
    &_checkAllFiles(length($dir)+1, $dir, $relFiles, $prLog);
}


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

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

    my $rel = undef;
    if (length($dir) > $length)
    {
	$rel = substr($dir, $length);
#print "\t-3- set1 rel <$rel>\n";
    }
    if ($rel)
    {
#print "\t-4- check1-> <$rel>\n";
	$prLog->print('-kind' => 'E',
		      '-str' => ["<$rel> is not listed in .md5CheckSum (1)"])
	    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";
#print "\t-5- de <$de>\n";

# don't care about blocked files, they are already check
# in sub checkMD5

	$rel = substr($de, $length);
#print "\t-10- set2 rel <$rel>\n";
	$prLog->print('-kind' => 'E',
		      '-str' => ["<$rel> is not listed in .md5CheckSum (2)"])
	    unless exists $$relFiles{$rel};
    }
    closedir(DIR) or
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot closedir <$dir>"]);
}


##################################################
# package printLogMultiple needs this function
sub cleanup
{
    my $signame = shift;
    my $exit = (shift() || 1);

    exit $exit;
}


##################################################
# print statistical data
sub printStat
{
    my ($prLog, $text, $checkedFiles, $checkedFilesSize,
	$linkedFiles, $linkedFilesSize) = (@_);

    my $checkP = 0;

    $checkP = int($checkedFilesSize * 10000 /
		  ($checkedFilesSize + $linkedFilesSize) +.5) / 100
		  if ($checkedFilesSize + $linkedFilesSize) > 0;

    $prLog->print('-kind' => 'S',
		  '-str' => ["$text checked $checkedFiles files (" .
			     (&::humanReadable($checkedFilesSize))[0] .
			     ") (" . $checkP . "%)",
			     "$text linked files were $linkedFiles (" . 
			     (&::humanReadable($linkedFilesSize))[0] . ")"]);
}

