#!/usr/bin/perl -w
# -------------------------------------------------------
# $Header: /home/cvs/dobackup/db_mkrdiff.pl,v 1.16 2011/12/16 15:31:15 rhardy Exp $
# -------------------------------------------------------
# This code is Copyrighted by: Webcon, Inc.
# All Rights Reserved.

# Main Program by: Robert Hardy

# Description: See ShowSyntax

# Post: Program overwrites files in this directory
# Todo:
#   Need to write .sig and .rdiff files to temporary files instead of final destination.
#   Need to remove temp in the event of failure

#Because of "our" usage
require v5.6.0;

# External Calls:
use strict;
use Getopt::Long; # For easy command line parsing
use File::stat;
use Cwd 'abs_path';
Getopt::Long::Configure ("bundling"); # Allow Commandline option bundling
# End External Calls

# ------------- Cron Setup
# Set environment variables that aren't present when run as a cron job.
$ENV{'TERM'} = "vt100";
$ENV{'LINES'} = "25";
$ENV{'COLUMNS'} = "80";
$ENV{'PATH'} = "$ENV{PATH}:\$HOME/bin";
# --------- End Cron Setup
# ------------- Begin Defaults
# Define all variables available to subfunctions using our() statements
our($ExecutableName);
our($Debug)=0;
our($Version)= '$Id: db_mkrdiff.pl,v 1.16 2011/12/16 15:31:15 rhardy Exp $';
our($MD5,$SIG,$RDIFF,$DIR,$TMPFile); $MD5=''; $SIG=''; $RDIFF=''; $DIR=''; $TMPFile='';
our($BackupLogDir);   $BackupLogDir = '/var/log/backup';
our($OurLogFile); 
our(@Files);
our($rdiffPath);      $rdiffPath = "/usr/bin/rdiff";
our($md5sumPath);     $md5sumPath = "/usr/bin/md5sum";
our($IONice);         $IONice = "/usr/bin/ionice -c3 -p";
our($Renice);         $Renice = "/usr/bin/renice +20";
my($Temp);

$SIG{INT} = sub { &SIGHandler(2, @_); };
$SIG{KILL} = sub { &SIGHandler(9, @_); };
$SIG{__DIE__} = sub { &SIGHandler(11, @_); };

my($File);
($ExecutableName) = ( $0 =~ /([^\/]*)$/ );
# --------------- End Defaults

# ------------- Begin Main Program -----------------------------------------
# Deal with Commandline Argument processing
GetOptions ("debug|d=i" => \$Debug,
            "md5|m"   => \$MD5,
            "sig|s"   => \$SIG,
            "rdiff|r"   => \$RDIFF,
            "dir=s"   => \$DIR,
            "logdir|l=s"   => \$BackupLogDir,
            "help|h"  => \&ShowSyntax,
            "version|v"  => sub{ print "$Version\n"; exit(0); } ) 
    or &ShowSyntax;

$OurLogFile= "$BackupLogDir/db_mkrdiff.log";

# Require dir and at least one of md5 sig or rdiff be set
if ( (! $DIR) && (! ($SIG || $MD5 || $RDIFF)) ) { &ShowSyntax; }

if ( &RdiffNotOkToRun ) { die("ERROR: Requirements missing. exiting..."); exit; }

# db_mkrdiff should be a background idle task and not break server I/O. Be nice/ionice.
# If IONice is there use it.
if ($IONice && $IONice ne "") {
  ($Temp) = ($IONice =~ /^\s*(\S+).*$/ );
  if ( -e "$Temp" ) {
    $Debug && print ("Running $IONice $$\n");
    `$IONice $$`;
  } else {
    $Debug && print ("ERROR: IONice program $IONice not found\n");
  }
}
$Debug && print ("Running $Renice $$\n");
`$Renice $$`;

open (LOGFILE, ">>$OurLogFile") || die "ERROR: Couldn\'t Create OurLogFile: $OurLogFile\n";
binmode ( LOGFILE, ":raw" );

$DIR = abs_path($DIR);
opendir(BIN, $DIR) or die "ERROR: Can't open $DIR: $!";
@Files=sort(readdir BIN);
closedir BIN;

print "Generating ";
$MD5 && print ".md5 ";
$SIG && print ".sig ";
$RDIFF && print ".rdiff ";
print "files in $DIR:\n";

chdir("$DIR") || die "ERROR: Chdir failed $!\n";

foreach $File( @Files ) {
  if ( $File =~ /\.full$/ ) {
    $Debug && print "Processing $File\n";
    if ($MD5) { MD5File($File); }
    if ($SIG) { SIGFile($File); }
    if ($RDIFF) { RDIFFFile($File); }
  }
  if ( $File =~ /\.rdiff$/ ) {
    $Debug && print "Processing $File\n";
    if ($MD5) { MD5File($File); }
  }
}

close(LOGFILE);
exit;
# --------------- End Main Program -----------------------------------------

# ------------------------
# ShowSyntax -- subroutine TESTED
# ------------------------

# Description: Prints Syntax for program
# Reads: $ExecutableName

sub ShowSyntax {
  print"$Version\n";
  print <<EOF;

Usage: $ExecutableName <options>

This program generates md5, rdiff delta .rdiff and rdiff .sig signatures for
dobackup files. It is designed to help minimize bandwidth usage when dealing
with offsite copies of backups.

Usage: $ExecutableName [--dir <PATH>] [--md5 | --sig | --rdiff] <options> 
Supported options:
  --help | -h: Prints this help message
  --debug n | -d n
    Changes the program's debug level.
    The default level is 0.  Higher numbers increase verbosity.
  --dir <PATH> 
    Sets path to your dobackup .full files. ex. --dir /mnt/backups
  --logdir | -l: <PATH> 
    Sets logdir path for your dobackup. Default is /var/log/backup
  --md5 | -m:
    Requests generation of .md5 files for each .full and .rdiff.
    Will not overwrite existing files unless 0 bytes.
  --rdiff | -r:
    Requests generation of rdiff .sig signature files for each dobackup.full.
  --sig | -s:
    Requests generation of rdiff .sig signature files for each dobackup.full.
  --version | -v: Prints version information (actually an RCS Id string)

EOF
  exit;
} # ---------------------- End ShowSyntax

# -------------------------------
# SIGHandler -- subroutine
# -------------------------------
# Description: Handles Signals
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax:
# Todo:

sub SIGHandler { # 1st argument is signal name
  my($sig) = @_;
  if ($sig == 2) { # INT
    print "Caught INT signal, cleaning up...\n";
    if (-e "$TMPFile") {
      print LOGFILE "ERROR occured when creating $TMPFile, deleting file!\n";
      unlink("$TMPFile");
    }
    close(LOGFILE);
    exit(2);
  }
  if ($sig == 9) { # __DIE__
    print "Caught KILL signal, cleaning up...\n";
    if (-e "$TMPFile") {
      print LOGFILE "ERROR occured when creating $TMPFile, deleting file!\n";
      unlink("$TMPFile");
    }
    close(LOGFILE);
    exit(9);
  }
  if ($sig == 11) { # __DIE__
    print "Caught DIE signal, cleaning up...\n";
    if (-e "$TMPFile") {
      print LOGFILE "ERROR occured when creating $TMPFile, deleting file!\n";
      unlink("$TMPFile");
    }
    close(LOGFILE);
    exit(11);
  }
} # ----------------------------- End SIGHandler


# -------------------------------
# MD5File -- subroutine
# -------------------------------
# Description: Given a File generate a .md5 file for it.
# Calls: md5sum
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads: $md5sumPath
# Returns:
# Syntax:
# Todo: Return codes from system call

sub MD5File {
  my($MyFile)=$_[0];
  $TMPFile="$MyFile.md5.$$";
  if ( -e "$MyFile.md5" && stat("$MyFile.md5")->size eq 0 ) {
    warn("$MyFile.md5 is a zero byte file. Deleting it!");
    unlink("$MyFile.md5");
  }
  if ( ! -e "$MyFile.md5" ) {
      print "Generating $TMPFile\n";
     `$md5sumPath $MyFile > $TMPFile`;
      if ( $? == 0 ) {
        print LOGFILE "Generated $TMPFile\n";
      } else {
        print LOGFILE "ERROR $? occured when creating $TMPFile\n";
        unlink("$TMPFile");
      }
  }
  if ( -e "$TMPFile" && (! -e "$MyFile.md5" ) ) {
    rename("$TMPFile", "$MyFile.md5");
  }
} # ----------------------------- End

# -------------------------------
# SIGFile -- subroutine
# -------------------------------
# Description: Given a File generate a rdiff .sig file for it.
# Calls: rdiff
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads: $rdiffPath
# Returns:
# Syntax:
# Todo:

sub SIGFile {
  my($MyFile)=$_[0];
  $TMPFile="$MyFile.sig.$$";
  if ( ! -e "$MyFile.sig" ) {
      print "Generating $TMPFile\n";
     `$rdiffPath signature $MyFile $TMPFile`;
      if ( $? == 0 ) {
        print LOGFILE "Generated $TMPFile\n";
      } else {
        print LOGFILE "ERROR $? occured when creating $TMPFile\n";
        unlink("$TMPFile");
      }
  }
  if ( -e "$TMPFile" && (! -e "$MyFile.sig" ) ) {
    rename("$TMPFile", "$MyFile.sig");
  }
} # ----------------------------- End


# -------------------------------
# RDIFFFile -- subroutine
# -------------------------------
# Description: Given a File generate a rdiff delta .rdiff file for it.
# Calls: rdiff
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads: $rdiffPath
# Returns:
# Syntax:
# Todo:

sub RDIFFFile {
  my($MyFile)=$_[0];
  my($PreviousFile)='';
  $TMPFile="$MyFile.rdiff.$$";
  if ( ! -e "$MyFile.rdiff" ) {
    $PreviousFile = &FindPreviousFile($MyFile);
    if ( &FindPreviousFile eq '' ) {
        warn ("WARNING: Can't find basis file for $TMPFile.\n");
        print LOGFILE ("WARNING: Can't find basis file for $TMPFile.\n");
    } else {
      if ( -e "$PreviousFile.sig" ) {
         print "Generating $TMPFile using basis file $PreviousFile\n";
         `$rdiffPath delta $PreviousFile.sig $MyFile $TMPFile`;
         if ( $? == 0 ) {
           `echo "rdiff patch $PreviousFile $MyFile.rdiff $MyFile" > $MyFile.rdiff.src`;
           print LOGFILE "Generated $TMPFile using basis file $PreviousFile\n";
         } else {
           print LOGFILE "ERROR $? occured when creating $TMPFile\n";
           unlink("$TMPFile");
         }
         if ( -e "$TMPFile" && (! -e "$MyFile.rdiff" ) ) {
           rename("$TMPFile", "$MyFile.rdiff");
         }
      } else {
        warn ("WARNING: Missing .sig for $MyFile so I can't create $TMPFile\n");
        print LOGFILE ("WARNING: Missing .sig for $MyFile so I can't create $TMPFile\n");
      }
    }
  }
} # ----------------------------- End RDIFFFile

# -----------------------------
# RdiffNotOkToRun -- subroutine
# -----------------------------
# Description: Verifies that all is ok for a rdiff related functions to proceed.
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns: 0 if ok to proceed with an rdiff
# Syntax: &RdiffNotOkToRun
# Todo:
sub RdiffNotOkToRun{
  # Test LogDir
  if ( ! -e $BackupLogDir ) {
    print("Warning: BackupLogDir $BackupLogDir does not exist.\n",
           "Attempting to Create Directory ...\n");
    if ( (! (mkdir($BackupLogDir, 0640)) &&  (! -e $BackupLogDir)) ) {
       print ("ERROR: Creation of $BackupLogDir Failed!\n");
       return 1;
    } else {
       print ("Directory $BackupLogDir successfully created!\n");
    }
  }
  # Test rdiff program
  if ( ! -e $rdiffPath ) {
    print ("ERROR: rdiff program $rdiffPath not found\n");
    return 1;
  }
  # Test md5sum program
  if ( ! -e $md5sumPath ) {
    print ("ERROR: md5sum program $md5sumPath not found\n");
    return 1;
  }
}

# -----------------------------
# FindPreviousFile -- subroutine
# ------------------------------
# Description: Given a filename find the next older full backup for using by rdiff.
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads: @Files
# Returns:
# Syntax: &FindPreviousFile($CurrentFile);
# Todo:

sub FindPreviousFile {
  my($MyFile)=$_[0];
  my($MyReturn)=0;
  my($MyPreviousFile)='';
  my($MyHead,$MyYear,$MyMonth,$MyDay,$MyHour,$MyMin,$MySec);
  my($CandidateFile)='';
  my($CHead,$CYear,$CMonth,$CDay,$CHour,$CMin,$CSec);
  my($BestGuessFile,$BGHead,$BGYear,$BGMonth,$BGDay,$BGHour,$BGMin,$BGSec); $BestGuessFile='';
  $Debug && print "FindPreviousFile MyFile:$MyFile\n";
  if ( $MyFile =~ /^([^_]+)_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2}).+$/ ) { # Parse date
    $MyHead = $1;
    $MyYear = $2;
    $MyMonth = $3;
    $MyDay = $4;
    $MyHour = $5;
    $MyMin = $6;
    $MySec = $7;
    ($Debug > 1 ) && print "MyFile Head:$MyHead;Year:$MyYear;Month:$MyMonth;Day:$MyDay;Hour:$MyHour;Min:$MyMin;Sec:$MySec\n";

    foreach $CandidateFile ( @Files ) {
      next unless ( $CandidateFile =~ /\.full$/ );
      $Debug && print "FindPreviousFile CandidateFile:$CandidateFile\n";
      if ( $CandidateFile =~ /^([^_]+)_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2}).+$/ ) { # Parse date
        $CHead = $1;
        $CYear = $2;
        $CMonth = $3;
        $CDay = $4;
        $CHour = $5;
        $CMin = $6;
        $CSec = $7;
        ($Debug > 1 ) && print("CandidateFile Head:$CHead;Year:$CYear;Month:$CMonth;Day:$CDay;Hour:$CHour;Min:$CMin;Sec:$CSec\n");
        if ( $MyHead eq $CHead ) {
          if ($BestGuessFile eq '') {
            if ( &IsAOlderThanB($CYear,$CMonth,$CDay,$CHour,$CMin,$CSec,$MyYear,$MyMonth,$MyDay,$MyHour,$MyMin,$MySec) == 1 ) {
              # $CandidateFile is older than $MyFile
              $BestGuessFile = $CandidateFile;
              $BGHead = $CHead;
              $BGYear = $CYear;
              $BGMonth = $CMonth;
              $BGDay = $CDay;
              $BGHour = $CHour;
              $BGMin = $CMin;
              $BGSec = $CSec;
           }
          } else { # BestGuessFile exists
            $MyReturn = &IsAOlderThanB($CYear,$CMonth,$CDay,$CHour,$CMin,$CSec,$MyYear,$MyMonth,$MyDay,$MyHour,$MyMin,$MySec);
            ($Debug > 1) && print("FindPreviousFile IsAOlderThanB MyReturn:$MyReturn\n");
            if ( $MyReturn == 2 ) {
              ($Debug > 1) && warn ("WARNING: CandidateFile $CandidateFile is MyFile $MyFile\n");
              next; # $CandidateFile is $MyFile
            }
            if ( ( $MyReturn == 1 ) && &IsAOlderThanB($BGYear,$BGMonth,$BGDay,$BGHour,$BGMin,$BGSec,$CYear,$CMonth,$CDay,$CHour,$CMin,$CSec) ) {
              # $CandidateFile is older than $MyFile AND $BestGuessFile is older than $CandidateFile
              $BestGuessFile = $CandidateFile;
              $BGHead = $CHead;
              $BGYear = $CYear;
              $BGMonth = $CMonth;
              $BGDay = $CDay;
              $BGHour = $CHour;
              $BGMin = $CMin;
              $BGSec = $CSec;
            }
          } 
        } else { # CHead different than MyHead
          next;
        }
      } else {
        warn ("WARNING: Parse failure in FindPreviousFile on $CandidateFile. Continuing...\n");
        next;
      }
    }
    $MyPreviousFile = $BestGuessFile;
    $Debug && print "Found $MyPreviousFile\n";
    return $MyPreviousFile;
  } else {
    warn "WARNING: I couldn't parse $MyFile in FindPreviousFile\n";
    print LOGFILE "WARNING: I couldn't parse $MyFile in FindPreviousFile\n";
    return '';
  }
} # ----------------------------- End FindPreviousFile

# --------------------------
# IsAOlderThanB -- subroutine
# --------------------------
# Description:
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns: 0 A_is_not_older_than_B
#          1 A_is_older_than_B
#          2 A_is_equal_age_to_B
# Syntax: &IsAOlderThanB($AYear,$AMonth,$ADay,$AHour,$AMin,$ASec,$BYear,$BMonth,$BDay,$BHour,$BMin,$BSec);
# Todo:

sub IsAOlderThanB {
  my($AYear)=$_[0];
  my($AMonth)=$_[1];
  my($ADay)=$_[2];
  my($AHour)=$_[3];
  my($AMin)=$_[4];
  my($ASec)=$_[5];
  my($BYear)=$_[6];
  my($BMonth)=$_[7];
  my($BDay)=$_[8];
  my($BHour)=$_[9];
  my($BMin)=$_[10];
  my($BSec)=$_[11];

  ( $AYear < $BYear ) && return 1;
  ( $AYear > $BYear ) && return 0;
  ( $AMonth < $BMonth ) && return 1;
  ( $AMonth > $BMonth ) && return 0;
  ( $ADay < $BDay ) && return 1;
  ( $ADay > $BDay ) && return 0;
  ( $AHour < $BHour ) && return 1;
  ( $AHour > $BHour ) && return 0;
  ( $AMin < $BMin ) && return 1;
  ( $AMin > $BMin ) && return 0;
  ( $ASec < $BSec ) && return 1;
  ( $ASec > $BSec ) && return 0;
  ( $ASec == $BSec ) && return 2;
  
} # ----------------------------- End IsAOlderThanB

# -------------------------------
#  -- subroutine
# -------------------------------
# Description:
# Calls:
# Global Vars/Files Created:
# Global Vars/Files Changed:
# Global Reads:
# Returns:
# Syntax:
# Todo:

#sub {
#} # ----------------------------- End
