#!/usr/bin/env perl

#
#   Copyright (C) Dr. Heinz-Josef Claes (2012-2022)
#                 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/>.
#


$main::STOREBACKUPVERSION = undef;


use POSIX;
use strict;
use warnings;



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 (unshift @INC, "$req";
$req, $prog) = &libPath($0);



require 'checkParam2.pl';
require 'checkObjPar.pl';
require 'prLog.pl';
require 'version.pl';
require 'fileDir.pl';
require 'forkProc.pl';

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


=head1 NAME

storeBackupMergeIsolatedBackup.pl - copy isolated backups to main one

=head1 DESCRIPTION



=head1 SYNOPSIS

based on configuration file:

storeBackupMergeIsolatedBackup.pl -f isolateConfigFile [-v] [--move]
                                  [--force] [-T tmpdir]

no configuration file:

    storeBackupSetupIsolatedMode.pl -i isolateBackupDir -b backupDir
    				    [-S series] [-v] [--move] [--force]
    				    [-T tmpdir]

=head1 OPTIONS

=over 8

=item B<--configFile>, B<-f>

    configuration file used for isolated mode and
    generated by storeBackupSetupIsolatedMode.pl
    (contains key "mergeBackupDir" which points to your
    main backup directory)

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

    isolated backup directory from which meta data has to be
    copied to backupDir.

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

    backup directory to which your backup data has to be
    copied (to your main backup directory)
    This is the directory from where storeBackupSetupIsolatedMode.pl
    copied the meta data to your isolated backup directory

=item B<--series>, B<-S>

    series of which meta data have to be copied to targetDir.
    Has to be specified if more than one series exist
    in backupDir

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

    generate verbose messages

=item B<--move>

    move instead of copy; only supported if isolatedBackupDir
    and backupDir are on the same filesystem

=item B<--force>

    force copying; do not finally ask

=item B<--tmpdir>, B<-T>

    directory for temporary files, default is </tmp>

=back

=head1 COPYRIGHT

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

=cut

my $Help = &::getPod2Text($0);

&printVersion(\@ARGV, '-V', '--version');

my $CheckPar =
    CheckParam->new('-allowLists' => 'no',
		    '-list' => [Option->new('-name' => 'configFile',
					    '-cl_option' => '-f',
					    '-cl_alias' => '--configFile',
					    '-param' => 'yes',
		    '-only_if' => 'not [isolateBackupDir] and not [backupDir]'),
				Option->new('-name' => 'isolateBackupDir',
					    '-cl_option' => '-i',
					    '-cl_alias' => '--isolateBackupDir',
					    '-param' => 'yes',
		    '-only_if' => '[backupDir] and not [configFile]'),
				Option->new('-name' => 'backupDir',
					    '-cl_option' => '-b',
					    '-cl_alias' => '--backupDir',
					    '-param' => 'yes',
		    '-only_if' => '[isolateBackupDir] and not [configFile]'),
				Option->new('-name' => 'series',
					    '-cl_option' => '-S',
					    '-cl_alias' => '--series',
					    '-param' => 'yes',
					    '-only_if' => '[isolateBackupDir]'),
				Option->new('-name' => 'verbose',
					    '-cl_option' => '-v',
					    '-cl_alias' => '--verbose'),
				Option->new('-name' => 'move',
					    '-cl_option' => '--move'),
				Option->new('-name' => 'force',
					    '-cl_option' => '--force'),
				Option->new('-name' => 'skipSync',
					    '-hidden' => 'yes',
					    '-cl_option' => '--skipSync'),
				Option->new('-name' => 'tmpdir',
					    '-cl_option' => '-T',
					    '-cl_alias' => '--tmpdir',
					    '-default' => $tmpdir)
				]);

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


my $configFile = $CheckPar->getOptWithPar('configFile');
my $isolateBackupDir = $CheckPar->getOptWithPar('isolateBackupDir');
my $backupDir = $CheckPar->getOptWithPar('backupDir');
my $series = $CheckPar->getOptWithPar('series');  # must be specified if
                                                  # more than 1 series exist
my $verbose = $CheckPar->getOptWithoutPar('verbose');
my $move = $CheckPar->getOptWithoutPar('move');
my $force = $CheckPar->getOptWithoutPar('force');
my $skipSync = $CheckPar->getOptWithoutPar('skipSync');
$tmpdir = $CheckPar->getOptWithPar('tmpdir');

my $prLog;
my ($prLogKind) = ['A:BEGIN',
		   'Z:END',
		   'I:INFO',
		   'W:WARNING',
		   'E:ERROR'];
$prLog = printLog->new('-kind' => $prLogKind,
		       '-tmpdir' => $tmpdir);


$prLog->print('-kind' => 'E',
	      '-str' => ["please define <configFile> or <isolateBackupDir>\n$Help"],
	      '-exit' => 1)
    unless ($configFile or $isolateBackupDir);


if ($configFile)      # read isolateConfigFile
{
    my $isolateConf =
	CheckParam->new('-configFile' => '-f',
			'-list' => [
			    Option->new('-name' => 'isolateConfigFile',
					'-cl_option' => '-f',
					'-param' => 'yes'),
			    Option->new('-name' => 'backupDir',
					'-cf_key' => 'backupDir',
					'-param' => 'yes'),
			    Option->new('-name' => 'mergeBackupDir',
					'-cf_key' => 'mergeBackupDir',
					'-param' => 'yes'),
			    Option->new('-name' => 'series',
					'-cf_key' => 'series',
					'-default' => 'default')
			]);
    $isolateConf->check('-argv' => ['-f' => $configFile],
			'-help' => "cannot read file <$configFile>\n",
			'-ignoreAdditionalKeys' => 1);

    $isolateBackupDir = $isolateConf->getOptWithPar('backupDir');
    $backupDir = $isolateConf->getOptWithPar('mergeBackupDir');
    $series = $isolateConf->getOptWithPar('series');

    $prLog->print('-kind' => 'I',
		  '-str' => ["extracted from $isolateBackupDir:",
			     "\tbackupDir = <$backupDir>",
			     "\tisolateBackupDir = <$isolateBackupDir>",
			     "\tseries = <$series>"])
	if $verbose;
}
elsif (not $series)     # no config file, series not set, try to
{                       # detect name of series
	local *DIR;
	opendir(DIR, $isolateBackupDir) or
	    $prLog->print('-kind' => 'E',
			  '-str' => ["cannot opendir <$isolateBackupDir>, exiting"],
			  '-exit' => 1);
	my ($entry, @entries);
	while ($entry = readdir DIR)
	{
	    next if ($entry eq '.' or $entry eq '..');
	    my $e = "$backupDir/$entry";
	    next if (-l $e and not -d $e);   # only directories
	    next unless -d $e;
	    if ($entry =~
		/\A(\d{4})\.(\d{2})\.(\d{2})_(\d{2})\.(\d{2})\.(\d{2})\Z/o)
	    {
		$series = '.';
		$prLog->print('-kind' => 'I',
			      '-str' => ["found series <.> in <$isolateBackupDir>"]);
		last;
	    }
	    push @entries, $entry;
	}
	closedir(DIR);

	unless ($series)
	{
	    $prLog->print('-kind' => 'E',
			  '-str' =>
			  ["found more than one backup series in <$isolateBackupDir>",
			   ", please specify series with option --series; found:",
			   sort @entries],
			  '-exit' => 1)
		if @entries > 1;
	}
	$prLog->print('-kind' => 'E',
		      '-str' =>
		      ["no series found in <$isolateBackupDir>, exiting"],
		      '-exit' => 1)
	    if @entries == 0;

	$series = $entries[0];
}

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

#
# load backup directories
#
my ($metaDataBackup, @isolatBackups);
{
    local *DIR;
    my $isolDir = "$isolateBackupDir/$series";
    opendir(DIR, $isolDir) or
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot opendir <$isolDir>, exiting"],
		      '-exit' => 1);
    my ($entry, @entries);
    while ($entry = readdir DIR)
    {
	next if (-l $entry and not -d $entry);   # only directories
	push @entries, $entry
	    if $entry =~
	    /\A(\d{4})\.(\d{2})\.(\d{2})_(\d{2})\.(\d{2})\.(\d{2})\Z/o;
    }
    closedir(DIR);

    $prLog->print('-kind' => 'E',
		  '-str' =>
		  ["didn't find a backup in $isolDir, exiting"],
		  '-exit' => 1)
	if @entries < 2;

    ($metaDataBackup, @isolatBackups) = sort (@entries);

#print "metaDataBackup = $metaDataBackup\n";
#print "isolatBackups = @isolatBackups\n";
}

#
# double check metaDataBackup in isolateBackupDir == backupDir
#
{
    my $metaIsol = "$isolateBackupDir/$series/$metaDataBackup";
    my $metaOrig = "$backupDir/$series/$metaDataBackup";

#print "metaIsol = $metaIsol\n";
#print "metaOrig = $metaOrig\n";

    $prLog->print('-kind' => 'E',
		  '-str' =>
     ["found meta data of backup <$metaDataBackup> in <$isolateBackupDir/$series>,".
     " but cannot find <$metaDataBackup> in <$backupDir/$series>"],
		  '-exit' => 1)
	unless -d $metaOrig;

    my $f = undef;
    if (-f "$metaIsol/.md5CheckSums")
    {
	$f = ".md5CheckSums";
    }
    elsif (-f "$metaIsol/.md5CheckSums.bz2")
    {
	$f = ".md5CheckSums.bz2";
    }
    $prLog->print('-kind' => 'E',
		  '-str' => ["cannot read <$metaIsol/.md5CheckSums[.bz2]>"],
		  '-exit' => 1)
	unless $f;
    $prLog->print('-kind' => 'E',
		  '-str' => ["cannot read <$metaOrig/$f>"],
		  '-exit' => 1)
	unless -f "$metaOrig/$f";

    my $isolMd5 = &::calcMD5("$metaIsol/$f", $prLog);
    my $origMd5 = &::calcMD5("$metaOrig/$f", $prLog);
#print "isolMd5 = $isolMd5\n";
#print "origMd5 = $origMd5\n";
    $prLog->print('-kind' => 'E',
		  '-str' => ["$f differs in <$metaIsol> and <$metaOrig>, exiting"],
		  '-exit' => 1)
	unless $isolMd5 eq $origMd5;
}

unless ($force)
{
    my $answer = 'yes';
    do
    {
	print "in directory <$isolateBackupDir/$series>, copy\n\t<",
	join(">\n\t<",@isolatBackups), ">\nto\n\t<$backupDir/$series>\n",
	"?\nyes / no -> ";
	$answer = <STDIN>;
	chomp $answer;
    } while ($answer ne 'yes' and $answer ne 'no');

    exit 0 if $answer eq 'no';
}

{
    my ($targetLinkFrom, $targetCount) =
	&::readLinkFromEntries("$backupDir/$series/$metaDataBackup", $prLog);
    my ($mergeLinkFrom, $mergeCount) =
	&::readLinkFromEntries("$isolateBackupDir/$series/$metaDataBackup", $prLog);
    foreach my $lf (sort keys %$mergeLinkFrom)
    {
	unless (exists $$targetLinkFrom{$lf})
	{
	    local *FILE;
	    my $f = "$backupDir/$series/$metaDataBackup/.storeBackupLinks";
	    $prLog->print('-kind' => 'I',
			  '-str' =>
			  ["adjusting refernce <$lf> in <$f/$targetCount>"]);
	    open(FILE, '>', "$f/linkFrom$targetCount") or
		$prLog->print('-kind' => 'E',
			      '-str' => ["cannot write <$f/$targetCount>"],
			      '-add' => [__FILE__, __LINE__],
			      '-exit' => 1);
	    print FILE "$lf\n";
	    close(FILE),
	    ++$targetCount;
	}
    }
}

#
# checks for option --move
#
$prLog->print('-kind' => 'E',
	      '-str' => ["directory <$isolateBackupDir/$series> does not exit"],
	      '-exit' => 1)
    unless -e "$isolateBackupDir/$series";
$prLog->print('-kind' => 'E',
	      '-str' => ["directory <$backupDir/$series> does not exit"],
	      '-exit' => 1)
    unless -e "$backupDir/$series";

if ($move)
{
    my $d1 = (stat("$isolateBackupDir/$series"))[0];
    my $d2 = (stat("$backupDir/$series"))[0];
    $prLog->print('-kind' => 'E',
		  '-str' => ["isolatedBackupDir <$isolateBackupDir/$series> and " .
		  "backupDir <$backupDir/$series> are not on the same filesystem",
			     "usage of option --move is not suppported"],
		  '-exit' => 1)
	if $d1 != $d2;
}

#
# copy data from isolated backup to backupDir
#
$prLog->print('-kind' => 'I',
	      '-str' => ["copying data . . ."]);

my $d;
foreach $d (@isolatBackups)
{
    $prLog->print('-kind' => 'I',
		  '-str' => ["copying <$isolateBackupDir/$series/$d> to " .
			     "<$backupDir/$series>"])
	if $verbose;

    if ($move)
    {
	&::mvComm("$isolateBackupDir/$series/$d" => "$backupDir/$series",
		  "$tmpdir/stbuMerge-", $prLog);
    }
    else
    {
	&::copyDir("$isolateBackupDir/$series/$d" => "$backupDir/$series",
		   "$tmpdir/stbuMerge-", $prLog, 0);
    }
}
#
# copy (and check for numbering) linkFrom files back to reference directory
#

unless ($skipSync)
{
    $prLog->print('-kind' => 'I',
		  '-str' => ["syncing ..."]);
    system "/bin/sync";
}

$prLog->print('-kind' => 'I',
	      '-str' => ["finished copying data",
			 "please run",
			 "\tstoreBackupUpdateBackup.pl -b \"$backupDir\""]);


exit 0;


##################################################
sub readLinkFromEntries
{
    my ($dir, $prLog) = @_;

    local *DIR;
    my $d = "$dir/.storeBackupLinks";
    opendir(DIR, $d) or
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot opendir <$dir>"],
		      '-add' => [__FILE__, __LINE__],
		      '-exit' => 1);
    my (%linkFrom, $entry);
    my $count = 0;
    while ($entry = readdir DIR)
    {
	if ($entry =~ /\AlinkFrom\d+\Z/)
	{
	    $count++;
	    local *FILE;
	    open(FILE, '<', "$d/$entry") or
		$prLog->print('-kind' => 'E',
			      '-str' => ["cannot opendir <$d/$entry>"],
			      '-add' => [__FILE__, __LINE__],
			      '-exit' => 1);
	    my $l = <FILE>;
	    chomp $l;
	    $linkFrom{$l} = $entry;
	    close(FILE);
	}
    }
    closedir(DIR);

    return \%linkFrom, $count;
}
