#!/usr/bin/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: storeBackupVersions.pl 330 2004-04-18 07:39:13Z hjc $ ';
push @VERSION, $VERSION;


use strict;


sub libPath
{
    my $file = shift;

    my $dir;

    # Falls Datei selbst ein symlink ist, solange folgen, bis aufgelst
    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 'humanRead.pl';
require 'dateTools.pl';
require 'storeBackupLib.pl';


my $checkSumFile = '.md5CheckSums';

my $Help = <<EOH;
This program locates different versions of a file saved with storeBackup.pl.

usage:
	$prog -f file [-b root]  [-v]
	 [-l [-a | [-s] [-u] [-g] [-M] [-c] [-m]]]

--file		-f  file name (name in the backup, probably with suffix
		    from compression)
--backupRoot	-b  root of storeBackup tree, normally not needed
--verbose	-v  print verbose messages
--locateSame	-l  locate same file with other names
--showAll	-A  same as: [-s -u -g -M -c -m]
--size		-s  show also size (human readable) of source file
--uid		-u  show also uid of source file
--gid		-g  show also gid of source file
--mode		-M  show also mode of source file
--ctime		-c  show also creation time of source file
--mtime		-m  show also modify time of source file
--atime		-a  show also access time of source file
EOH
    ;


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

my $CheckPar =
    CheckParam->new('-allowLists' => 'no',
		    '-list' => [Option->new('-option' => '-f',
					    '-alias' => '--file',
					    '-param' => 'yes',
					    '-must_be' => 'yes'),
				Option->new('-option' => '-b',
					    '-alias' => '--backupRoot',
					    '-default' => ''),
				Option->new('-option' => '-v',
					    '-alias' => '--verbose'),
				Option->new('-option' => '-l',
					    '-alias' => '--locateSame'),
				Option->new('-option' => '-A',
					    '-alias' => '--showAll',
					    '-only_if' => '[-l]',
					    '-comment' =>
				"-s can only be use in conjunction with -l\n"),
				Option->new('-option' => '-s',
					    '-alias' => '--size',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-u',
					    '-alias' => '--uid',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-g',
					    '-alias' => '--gid',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-M',
					    '-alias' => '--mode',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-c',
					    '-alias' => '--ctime',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-m',
					    '-alias' => '--mtime',
					    '-only_if' => '[-l] and not [-A]'),
				Option->new('-option' => '-a',
					    '-alias' => '--atime',
					    '-only_if' => '[-l] and not [-A]')
				]
		    );

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

# Auswertung der Parameter
my $file = $CheckPar->getOptWithPar('-f');
my $verbose = $CheckPar->getOptWithoutPar('-v');
my $backupRoot = $CheckPar->getOptWithPar('-b');
my $locateSame = $CheckPar->getOptWithoutPar('-l');
my $showAll = $CheckPar->getOptWithoutPar('-A');
my $showSize = $CheckPar->getOptWithoutPar('-s') | $showAll;
my $showUID = $CheckPar->getOptWithoutPar('-u') | $showAll;
my $showGID = $CheckPar->getOptWithoutPar('-g') | $showAll;
my $showMode = $CheckPar->getOptWithoutPar('-M') | $showAll;
my $showCTime = $CheckPar->getOptWithoutPar('-c') | $showAll;
my $showMTime = $CheckPar->getOptWithoutPar('-m') | $showAll;
my $showATime = $CheckPar->getOptWithoutPar('-a') | $showAll;

my $f = $file;
my $file = &absolutePath($file);

my $prLog = printLog->new();

#
# md5CheckSum - Datei finden
$prLog->print('-kind' => 'E',
	      '-str' => ["file <$f> does not exit"],
	      '-exit' => 1)
    unless (-f $f);

if ($backupRoot)
{
    $prLog->print('-kind' => 'E',
		  '-str' => ["directory <$backupRoot> does not exit"],
		  '-exit' => 1)
	unless (-d $backupRoot);
    $backupRoot = &absolutePath($backupRoot);
}
else
{
    my ($dir, $x) = &splitFileDir($file);
    $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, $x) = &splitFileDir($dir);
    } while ($dir ne '/');

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

my $checkSumFileRoot = $checkSumFile;
$checkSumFileRoot .= ".bz2" if (-f "$backupRoot/$checkSumFile.bz2");
$prLog->print('-kind' => 'E',
	      '-str' => ["no info file <$checkSumFileRoot> in <$backupRoot>"],
	      '-exit' => 1)
    unless(-f "$backupRoot/$checkSumFileRoot");

# jetzt $restoreTree relativ zu $backupRoot machen
my $fileWithRelPath = substr($file, length($backupRoot) + 1);
my ($storeBackupAllTrees, $fileDateDir) = &splitFileDir($backupRoot);

# ^^^
# Beispiel:            (/tmp/stbu/2001.12.20_16.21.59/perl/Julian.c.bz2)
# $backupRoot beinhaltet jetzt den Pfad zum Archiv
#                      (/tmp/stbu/2001.12.20_16.21.59)
# $file beinhaltet die Datei mit kompletten, absoluten Pfad
#                      (/tmp/stbu/2001.12.20_16.21.59/perl/Julian.c.bz2)
# $fileWithRelPath beinhaltet jetzt den relativen Pfad innerhalb des Archivs
#                      (perl/Julian.c.bz2)
# $storeBackupAllTrees beinhaltet den Root-Pfad des storeBackup (oberhalb
#      der Datum Directories)
#                      (/tmp/stbu)
# $fileDateDir beinhaltet den Namen des Datum-Dirs des gesuchten files
#                      (2001.12.20_16.21.59)

#print "backupRoot = $backupRoot\n";
#print "file = $file\n";
#print "fileWithRelPath = $fileWithRelPath\n";
#print "storeBackupAllTrees = $storeBackupAllTrees\n";
#print "fileDateDir = $fileDateDir\n\n";


$prLog->print('-kind' => 'I',
	      '-str' => ["checking for <$fileWithRelPath>"])
    if $verbose;

# Versions-Directories unter $backupRoot einlesen
my (@allDirs) = (&::readAllBackupDirs($storeBackupAllTrees, $prLog, 1));
#print "allDirs =\n", join("\n", @allDirs), "\n";

# Zuerst die Dateien direkt aus Existenz berprfen
# dann md5-Summen berechnen, um unterschiedliche Stnde festzustellen
my (@files, @md5sum, @dirs, $entry);
my $lastInode = undef;
foreach $entry (@allDirs)
{
    my $f = $entry . '/' . $fileWithRelPath;
    if (-f $f)
    {
	push @files, $f;
	push @dirs, $entry;

	# erst mal prfen, ob inode identisch ist
	my ($inode, $size) = (stat($f))[1,7];
	if ($inode == $lastInode)
	{
	    push @md5sum, $md5sum[@md5sum - 1];  # letzte md5 Summe kopieren
	    next;              # md5 Summe muss nicht berechnet werden
	}
	$lastInode = $inode;

	# md5 Summe muss berechnet werden
	my $m = forkProc->new('-exec' => 'md5sum',
			      '-param' => [$entry . '/' . $fileWithRelPath],
			      '-prLog' => $prLog,
			      '-workingDir' => '.',
			      '-outRandom' => '/tmp/md5-');
	$m->wait();
	my $out = $m->getSTDERR();
	$prLog->print('-kind' => 'E',
		      '-str' =>
		      ["fork of md5sum generated the following errors:",
		       @$out])
	    if (@$out > 0);
	my $out = $m->getSTDOUT();
	my $l = $$out[0];
	if ($l =~ /\A\\/)  # "\\" am Zeilenanfang -> es wird gequotet
	{
	    $l =~ s/\\n/\n/g;   # "\n" im Namen wird von md5sum zu
                                # "\\n" gemacht, zurckkonvertieren!
	    $l =~ s/\A\\//;     # "\\" am Zeilenende entfernen
	}
	my ($md5, $name) = $l =~ /\A(\w+)\s+(.*)/s;
	push @md5sum, $md5;
    }
}

#print "files = \n", join("\n", @files), "\n";
#print "md5s = \n", join("\n", @md5sum), "\n";

# Unterschiedliche Versionen merken
my ($i, $j);
my (@versionFiles) = $files[0];
my (@versionDirs) = $dirs[0];
my $lastmd5 = $md5sum[0];
printf("%2d %s\n", 1, $versionFiles[0]) unless $locateSame;
for ($j = 0, $i = 1 ; $i < @files ; $i++)
{
    if ($md5sum[$i] ne $lastmd5)
    {
	$lastmd5 = $md5sum[$i];
	++$j;
	$versionFiles[$j] = $files[$i];
	$versionDirs[$j] = $dirs[$i];
	printf("%2d %s\n", $j + 1, $versionFiles[$j]) unless $locateSame;
    }
}

exit 0 unless $locateSame;

my %versionMD5sum;  # md5 Summen mssen aus $checkSumFile gelesen werden,
                    # da ansonsten komprimierte Dateien mit nicht-komprimierten
                    # verglichen wrden!
my %versionSize;    # key = md5sum (wie oben), value = size aus $checkSumFile
my %versionUID;     # key = md5sum (wie oben), value = uid aus $checkSumFile
my %versionGID;     # key = md5sum (wie oben), value = uid aus $checkSumFile
my %versionMode;    # key = md5sum (wie oben), value = mode aus $checkSumFile
my %versionCTime;   # key = md5sum (wie oben), value = ctime aus $checkSumFile
my %versionMTime;   # key = md5sum (wie oben), value = mtime aus $checkSumFile
my %versionCompr;   # key = md5sum (wie oben), value = c|u aus $checkSumFile
my (@versionMD5s);
my (@dummy);
print "versionDirs = <", join("><", @versionDirs), ">\n";
foreach $entry (@versionDirs)   # jetzt alle zur Datei *gespeicherten*
{                               # unterschiedlichen md5 Summen laden
print "entry = $entry\n";
    my $found = 0;
    my $rcsf = readCheckSumFile->new('-checkSumFile' => "$entry/$checkSumFile",
				     '-prLog' => $prLog);
				     
    my $metaVal = $rcsf->getMetaVal();
    my $postfix = $$metaVal{'postfix'};    # postfix (kompr. oder nicht) merken

    my ($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	$size, $uid, $gid, $mode, $filename);
    while ((($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	    $size, $uid, $gid, $mode, $filename) = $rcsf->nextLine()) > 0)
    {
	$filename .= $postfix if $compr eq 'c';

	if ($fileWithRelPath eq $filename)
	{
	    push @versionMD5s, $md5sum;       # Original md5 Summe merken
	    push @dummy, $entry;
	    $versionMD5sum{$md5sum} = [];
	    $versionSize{$md5sum} = $size;
	    $versionUID{$md5sum} = $uid;
	    $versionGID{$md5sum} = $gid;
	    $versionMode{$md5sum} = $mode;
	    $versionCTime{$md5sum} = $ctime;
	    $versionMTime{$md5sum} = $mtime;
	    $versionCompr{$md5sum} = $compr;
	    $found = 1;
	    last;
	}
    }
    $prLog->print('-kind' => 'E',
                  '-str' => ["cannot find <$fileWithRelPath> in <$entry>"])
        if ($found == 0);
}
(@versionDirs) = (@dummy);

#print "\nOriginal-MD5-Summen:\n";
#print "versionFiles = \n", join("\n", @versionFiles), "\n";
#print "versionMD5s = \n", join("\n", @versionMD5s), "\n";

#
# Alle $checkSumFiles durchgehen und Dateien mit passenden MD5 Summen merken
#
foreach $entry (@allDirs)
{
    $prLog->print('-kind' => 'I',
		  '-str' => ["checking <$entry>"])
	if $verbose;

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

    my ($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	$size, $uid, $gid, $mode, $filename);
    while ((($md5sum, $compr, $devInode, $inodeBackup, $ctime, $mtime, $atime,
	    $size, $uid, $gid, $mode, $filename) = $rcsf->nextLine()) > 0)
    {

	if (exists($versionMD5sum{$md5sum}))
	{
	    push @{$versionMD5sum{$md5sum}}, "$entry/$filename";
#	    print "$md5sum $entry/$filename\n";
	}
    }
#    close(FILE);
}

# Ausgabe:

# Sortieren der gefundenen Dateien pro md5 Summe
foreach $entry (keys %versionMD5sum)
{
    @{$versionMD5sum{$entry}} = sort @{$versionMD5sum{$entry}};
}

# Aufbauen einer Liste, die so sortiert werden kann, da die ltesten
# Dateinamen die ersten Versionsnummern bekommen
my @list;
foreach $entry (keys %versionMD5sum)
{
    push @list, {
	'md5' => $entry,
	'list' => $versionMD5sum{$entry},
	'size' => $versionSize{$entry},
	'uid' => $versionUID{$entry},
	'gid' => $versionGID{$entry},
	'mode' => $versionMode{$entry},
	'ctime' => $versionCTime{$entry},
	'mtime' => $versionMTime{$entry},
	'compr' => $versionCompr{$entry}
    };
}
$i = 1;
foreach $entry ( sort { $a->{'list'}[0] cmp $b->{'list'}[0] } @list )
{
    my $pvs = '';
    if ($showSize)
    {
	$pvs = ' ' . (&humanReadable($entry->{'size'}))[0] . ' ' .
	    $entry->{'size'} . ' bytes ';
    if ($entry->{'compr'} eq 'c')
    {
	$pvs .= '(compressed)';
    }
    else
    {
	$pvs .= '(not compressed)';
    }
    }
    print "$i:$pvs (md5=", $entry->{'md5'}, ")\n";
    my @p;
    push @p, 'uid = ' . $entry->{'uid'} if $showUID;
    push @p, 'gid = ' . $entry->{'gid'}  if $showGID;
    push @p, sprintf("mode = 0%o", $entry->{'mode'}) if $showMode;
    print '    ', join(', ', @p), "\n" if (@p);
    @p = ();
    if ($showCTime)
    {
	my $d = dateTools->new('-unixTime' => $entry->{'ctime'});
	push @p, 'ctime = ' . $d->getDateTime();
    }
    if ($showMTime)
    {
	my $d = dateTools->new('-unixTime' => $entry->{'mtime'});
	push @p, 'mtime = ' . $d->getDateTime();
    }
    if ($showATime)
    {
	my $d = dateTools->new('-unixTime' => $entry->{'atime'});
	push @p, 'atime = ' . $d->getDateTime();
    }
    print '    ', join(', ', @p), "\n" if (@p);
    print "\t";
    print join("\n\t", @{$entry->{'list'}}), "\n";
    ++$i;
}

exit 0;
