#! /usr/bin/env perl

#
#   Copyright (C) Dr. Heinz-Josef Claes (2002-2004)
#                 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 2 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, write to the Free Software
#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#


my $VERSION = '$Id: storeBackupRecover.pl 331 2004-06-06 19:57:12Z hjc $ ';
push @VERSION, $VERSION;


use strict;
use DB_File;           # Berkeley DB version 1


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 'checkParam.pl';
require 'checkObjPar.pl';
require 'prLog.pl';
require 'version.pl';
require 'fileDir.pl';
require 'forkProc.pl';
require 'storeBackupLib.pl';


my $md5CheckSumVersion = '1.1';
my $noRestoreParallel = 12;
my $checkSumFile = '.md5CheckSums';

my $tmpdir = '/tmp';              # default value
$tmpdir = $ENV{'TMPDIR'} if defined $ENV{'TMPDIR'};


my $Help = <<EOH;
This program recovers files saved with storeBackup.pl.

usage:
	$prog -r restore [-b root] -t targetDir [--flat]
		[-o] [--tmpdir] [--noHardLinks] [-p number] [-v] [-n]


--restoreTree	    -r  file or (part of) the tree to restore
		        when restoring a file, the file name in the backup has
		        to be used (eg. with compression suffix)
--backupRoot	    -b  root of storeBackup tree, normally not needed
--targetDir	    -t  directory for unpacking
--flat                  do not create subdirectories
--overwrite	    -o  overwrite existing files
--tmpdir	    -T  directory for temporary file, default is <$tmpdir>
--noHardLinks		do not reconstruct hard links in restore tree
--noRestoreParallel -p  max no of paralell programs to unpack, default is $noRestoreParallel
--verbose	    -v  print verbose messages
--noRestored	    -n  print number of restored dirs, hardlinks, symlinks, files

Copyright (c) 2002-2004 by Heinz-Josef Claes
Published under the GNU General Public License
EOH
    ;


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

my $CheckPar =
    CheckParam->new('-allowLists' => 'no',
		    '-list' => [Option->new('-option' => '-r',
					    '-alias' => '--restoreTree',
					    '-param' => 'yes',
					    '-must_be' => 'yes'),
				Option->new('-option' => '-b',
					    '-alias' => '--backupRoot',
					    '-default' => ''),
				Option->new('-option' => '-t',
					    '-alias' => '--targetDir',
					    '-param' => 'yes',
					    '-must_be' => 'yes'),
				Option->new('-option' => '--flat'),
				Option->new('-option' => '-o',
					    '-alias' => '--overwrite'),
				Option->new('-option' => '-T',
					    '-alias' => '--tmpdir',
					    '-default' => $tmpdir),
				Option->new('-option' => '--noHardLinks'),
				Option->new('-option' => '-p',
					    '-alias' => '--noRestoreParallel',
					    '-default' => $noRestoreParallel),
				Option->new('-option' => '-v',
					    '-alias' => '--verbose'),
				Option->new('-option' => '-n',
					    '-alias' => '--noRestored')
				]
		    );

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

# Auswertung der Parameter
my $restoreTree = $CheckPar->getOptWithPar('-r');
my $backupRoot = $CheckPar->getOptWithPar('-b');
my $targetDir = $CheckPar->getOptWithPar('-t');
my $flat = $CheckPar->getOptWithoutPar('--flat');
my $overwrite = $CheckPar->getOptWithoutPar('-o');
$tmpdir = $CheckPar->getOptWithPar('-T');
my $noHardLinks = $CheckPar->getOptWithoutPar('--noHardLinks');
my $noRestoreParallel = $CheckPar->getOptWithPar('-p');
my $verbose = $CheckPar->getOptWithoutPar('-v');
my $noRestored = $CheckPar->getOptWithoutPar('-n');


my $prLog = printLog->new();

$prLog->print('-kind' => 'E',
	      '-str' => ["target directory <$targetDir> does not exist"],
	      '-exit' => 1)
    unless (-d $targetDir);

my $rt = $restoreTree;
my $restoreTree = &absolutePath($restoreTree);
$restoreTree = $1 if $restoreTree =~ /(.*)\/$/;  # remove trailing '/'

#
# md5CheckSum - Datei finden
$prLog->print('-kind' => 'E',
	      '-str' => ["directory or file <$rt> does not exist"],
	      '-exit' => 1)
    unless (-e $rt);
my $isFile = 1 if (-f $rt);

if ($backupRoot)
{
    $prLog->print('-kind' => 'E',
		  '-str' => ["directory <$backupRoot> does not exit"],
		  '-exit' => 1)
	unless (-d $backupRoot);
    $backupRoot = &absolutePath($backupRoot);
}
else
{
    my $dir = $restoreTree;
    $backupRoot = undef;
    do
    {
	# feststellen, ob eine .md5sum Datei vorhanden ist
	if (-f "$dir/$checkSumFile" or -f "$dir/$checkSumFile.bz2")
	{
	    $prLog->print('-kind' => 'I',
			  '-str' => ["found info file <$checkSumFile> in " .
				     "directory <$dir>"])
		if ($verbose);
	    $prLog->print('-kind' => 'E',
			  '-str' =>
			  ["found info file <$checkSumFile> a second time in " .
			   "<$dir>, first time found in <$backupRoot>"],
			  '-exit' => 1)
		if ($backupRoot);

	    $backupRoot = $dir;
	}

	($dir, $_) = &splitFileDir($dir);
    } while ($dir ne '/');


    $prLog->print('-kind' => 'E',
		  '-str' => ["did not find info file <$checkSumFile>\n"],
		  '-exit' => 1)
	unless ($backupRoot);
}

#$restoreTree =~ s/$backupRoot\/*//;
$restoreTree = substr($restoreTree, length($backupRoot) + 1);


# ^^^
# $backupRoot beinhaltet jetzt den Pfad zum Archiv
# $restoreTree beinhaltet jetzt den relativen Pfad innerhalb des Archivs

unless ($flat)
{
    # Subtree unter dem Zieldirectory erzeugen
    my $t = "$targetDir/$restoreTree";
    if ($isFile)         # Filenamen entfernen
    {
	($t, $_) = &splitFileDir($t);
    }

    my $l = forkProc->new('-exec' => 'mkdir',
			  '-param' => ['-p', $t],
			  '-prLog' => $prLog);
    $l->wait();
}

#
# Jezt Infofile einlesen und die gewünschten Dateien aussortieren
#

my $rcsf = readCheckSumFile->new('-checkSumFile' =>
				 "$backupRoot/$checkSumFile",
				 '-prLog' => $prLog);

my $fork = parallelForkProc->new('-maxParallel' => $noRestoreParallel,
				 '-prLog' => $prLog);

my $meta = $rcsf->getMetaVal();

my ($uncompr, @uncomprPar) = split(/\s+/, $$meta{'uncompress'});
my ($cp, @cpPar) = ('cp');
my $postfix = $$meta{'postfix'};

# dbm-File öffnen
my %DBMHardLink;        # key: dev-inode (oder undef), value: filename
my %hasToBeLinked = (); # hier werden die zu linkenden Dateien gesammelt,
                        # bis die Referenzdatei vollständig zurückgesichert ist
unless ($noHardLinks)
{
    dbmopen(%DBMHardLink, "$tmpdir/stbrecover.$$", 0600);
}

my $noFilesCopy = 0;
my $noFilesCompr = 0;
my $noSymLinks = 0;
my $noNamedPipes = 0;
my $noDirs = 0;
my $hardLinks = 0;

# Zurücksichern der Dateien
my $lrestoreTree = length($restoreTree);
my ($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
    $size, $uid, $gid, $mode, $filename);

#print "restoreTree = <$restoreTree>\n";
#print "lrestoreTree = <$lrestoreTree>\n";
#print "isFile = <$isFile>\n";
while ((($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	 $size, $uid, $gid, $mode, $filename) = $rcsf->nextLine()) > 0)
{
    my $f = $filename;
    if ($isFile and
	($md5sum ne 'dir' or $md5sum ne 'symlink' or $md5sum ne 'pipe'))
    {
	$f .= $postfix if ($compr eq 'c');
    }

#print "from .md5CheckSums: $f\n";
    if ($restoreTree eq '' or "$restoreTree/" eq substr($f, 0, $lrestoreTree + 1)
	or ($isFile and $restoreTree eq $f))
    {
#print "---> restore!\n";
	my $targetFile;
	if ($flat)
	{
	    ($_, $targetFile) = &splitFileDir($filename);
	    $targetFile = "$targetDir/$targetFile";
	}
	else
	{
	    $targetFile = "$targetDir/$filename";
	}

	if ($md5sum eq 'dir')
	{
	    if (not $flat and not -e $targetFile)
	    {
		++$noDirs;
		$prLog->print('-kind' => 'E',
			      '-str' =>
			      ["cannot create directory <$targetFile>"],
			      '-exit' => 1)
		    unless mkdir $targetFile;
		chown $uid, $gid, $targetFile;
		chmod $mode, $targetFile;
		utime $atime, $mtime, $targetFile;
		$prLog->print('-kind' => 'I',
			      '-str' => ["mkdir $targetFile"])
		    if ($verbose);
	    }
	}
	elsif ($md5sum eq 'symlink')
	{
	    my $linkTo = readlink "$backupRoot/$filename";
	    if (not $overwrite and -e $targetFile)
	    {
		$prLog->print('-kind' => 'W',
			      '-str' => ["target $targetFile already exists:",
					 "\tln -s $linkTo $targetFile"]);
	    }
	    else
	    {
		++$noSymLinks;
		symlink $linkTo, $targetFile;

		# bei einigen Betriebssystem (z.B. Linux) wird bei Aufruf
		# des Systemcalls chmod bei symlinks nicht der Symlink selbst
		# geaendert, sondern die Datei, auf die er verweist.
		# (dann muss lchown genommen werden -> Inkompatibilitaeten!?)
		my $chown = forkProc->new('-exec' => 'chown',
					  '-param' => [$uid, $gid,
						       "$targetFile"],
					  '-outRandom' => "$tmpdir/chown-",
					  '-prLog' => $prLog);
		$chown->wait();
		utime $atime, $mtime, $targetFile;
		$prLog->print('-kind' => 'I',
			      '-str' => ["ln -s $linkTo $targetFile\n"])
		    if ($verbose);
	    }
	}
	elsif ($md5sum eq 'pipe')
	{
	    my $mknod = forkProc->new('-exec' => 'mknod',
				      '-param' => ["$targetFile", 'p'],
				      '-outRandom' => "$tmpdir/mknod-",
				      '-prLog' => $prLog);
	    $mknod->wait();
	    my $out = $mknod->getSTDOUT();
	    $prLog->print('-kind' => 'E',
			  '-str' =>
			  ["STDOUT of <mknod $targetFile p>:", @$out])
		if (@$out > 0);
	    $out = $mknod->getSTDERR();
	    $prLog->print('-kind' => 'E',
			  '-str' =>
			  ["STDERR of <mknod $targetFile p>:", @$out])
		if (@$out > 0);
	    chown $uid, $gid, $targetFile;
	    chmod $mode, $targetFile;
	    utime $atime, $mtime, $targetFile;
	}
	else     # normal file
	{

# Idee zur Lösung des parallelitäts-Prolems beim Zurücksichern
# in Verbindung mit dem Setzen der hard links:
# erste Datei:
# dev-inode => '.' in dbm-file (%DBMHardLink)
# fork->add
# wenn fertig, dann dev-inode => filename in dbm-file
#
# zweite Datei (hard link)
# nachsehen in dbm-file
# wenn '.' -> in Warteschlange hängen (hash)
# wenn filename -> linken
# unten immer Warteschlange in dbm-file überprüfen
	    my ($old, $new) = (undef, undef);

	    unless ($noHardLinks) # Hard Link überprüfen
	    {
		if (exists($DBMHardLink{$devInode}))   # muss nur gelinkt werden
		{
		    $hasToBeLinked{$targetFile} = [$devInode, $uid, $gid, $mode,
						   $atime, $mtime];
		    $hardLinks++;
		    goto contLoop;
		}
		else
		{
		    $DBMHardLink{$devInode} = '.';   # ist in Bearbeitung
		}
	    }
	    if ($compr eq 'u')    # war nicht komprimiert
	    {
		if (not $overwrite and -e $targetFile)
		{
		    $prLog->print('-kind' => 'W',
				  '-str' =>
				  ["target $targetFile already exists:",
				   "\t$cp @cpPar $backupRoot/$filename " .
				   "$targetFile"]);
		}
		else
		{
		    ++$noFilesCopy;
		    ($old, $new) =
			$fork->add('-exec' => $cp,
				   '-param' => [@cpPar, "$backupRoot/$filename",
						"$targetFile"],
				   '-outRandom' => "$tmpdir/recover-",
				   '-info' => [$targetFile, $uid, $gid, $mode,
					       $atime, $mtime, $devInode]);
		    $prLog->print('-kind' => 'I',
				  '-str' =>
				  ["cp $backupRoot/$filename $targetFile"])
			if ($verbose);
		}
	    }
	    else                   # war komprimiert
	    {
		if (not $overwrite and -e $targetFile)
		{
		    $prLog->print('-kind' => 'W',
				  '-str' =>
				  ["target $targetFile already exists:",
				   "\t$uncompr @uncomprPar " .
				   "< $backupRoot/$filename$postfix " .
				   "> $targetFile"]);
		}
		else
		{
		    ++$noFilesCompr;
		    ($old, $new) =
			$fork->add('-exec' => $uncompr,
				   '-param' => [@uncomprPar],
				   '-stdin' => "$backupRoot/$filename$postfix",
				   '-stdout' => "$targetFile",
				   '-delStdout' => 'no',
				   '-outRandom' => "$tmpdir/recover-",
				   '-info' => [$targetFile, $uid, $gid, $mode,
					       $atime, $mtime, $devInode]);
		    $prLog->print('-kind' => 'I',
				  '-str' => ["$uncompr @uncomprPar < " .
					     "$backupRoot/$filename$postfix > " .
					     "$targetFile"])
			if ($verbose);
		}
	    }

	    if ($old)
	    {
		my ($f, $oUid, $oGid, $oMode, $oAtime, $oMtime, $oDevInode) =
		    @{$old->get('-what' => 'info')};
		unless ($noHardLinks)
		{                                 # File in DBM vermerken
		    $DBMHardLink{$oDevInode} = $f;
		}
		chown $oUid, $oGid, $f;
		chmod $oMode, $f;
		utime $oAtime, $oMtime, $f;
	    }

	    goto finish if $isFile;    # aufhören, ist nur _eine_ Datei
	}
    }

contLoop:;
# nachsehen, ob offene Links gesetzt werden können
    &setHardLinks(\%hasToBeLinked, \%DBMHardLink, $prLog, $verbose)
	unless $noHardLinks;

}

finish:;
close(FILE);

my $job;
while ($job = $fork->waitForAllJobs())
{
    my ($f, $oUid, $oGid, $oMode, $oAtime, $oMtime, $oDevInode) =
	@{$job->get('-what' => 'info')};
    unless ($noHardLinks)
    {                                 # File in DBM vermerken
	$DBMHardLink{$oDevInode} = $f;
    }
    chown $oUid, $oGid, $f;
    chmod $oMode, $f;
    utime $oAtime, $oMtime, $f
}

unless ($noHardLinks)
{
    &setHardLinks(\%hasToBeLinked, \%DBMHardLink, $prLog, $verbose);
    dbmclose(%DBMHardLink);
    unlink "$tmpdir/stbrecover.$$";
}

$prLog->print('-kind' => 'I',
	      '-str' =>
	      ["$noDirs dirs, $hardLinks hardlinks, $noSymLinks symlinks, " .
	       "$noNamedPipes pipes, " .
	       "$noFilesCopy copied, $noFilesCompr uncompressed"])
    if ($noRestored);

exit 0;


############################################################
sub setHardLinks
{
    my ($hasToBeLinked, $DBMHardLink, $prLog, $verbose) = @_;

    my $f;
    foreach $f (keys %$hasToBeLinked)
    {
	my ($di, $uid, $gid, $mode, $atime, $mtime) = @{$$hasToBeLinked{$f}};
	if (exists($$DBMHardLink{$di}) and $$DBMHardLink{$di} ne '.')
	{
	    my $oldF = $$DBMHardLink{$di};
	    if (-e $f)
	    {
		$prLog->print('-kind' => 'W',
			      '-str' => ["cannot link <$f> to itself"]);
	    }
	    else
	    {
		
		if (link $oldF, $f)
		{
		    $prLog->print('-kind' => 'I',
				  '-str' => ["link $oldF $f"])
			if ($verbose);
		}
		else
		{
		    $prLog->print('-kind' => 'E',
				  '-str' => ["failed: link $oldF $f"]);
		}
		chown $uid, $gid, $f;
		chmod $mode, $f;
		utime $atime, $mtime, $f;
	    }
	    delete $$hasToBeLinked{$f};
	}
    }
}
