#!/usr/bin/perl -w
# --
# scripts/backup.pl - a perl backup script for OTRS based on backup.sh
# Copyright (C) 2005 Michael Gurski <edwin.gurski@amti.com>
# --
# $Id: backup.pl,v 1.3 2005/05/10 20:54:32 gurski Exp $
# --
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
# --

use strict;

# use ../ as lib location
use File::Basename;
use FindBin qw($RealBin);
use lib dirname($RealBin);
use lib dirname($RealBin)."/Kernel/cpan-lib";

use vars qw($VERSION);
$VERSION = '$Revision: 1.3 $';
$VERSION =~ s/^\$.*:\W(.*)\W.+?$/$1/;

use Kernel::Config;
use Kernel::System::Log;

use POSIX;
use Getopt::Long;
use File::Which;
use File::NCopy;
use File::Remove;
use Date::Pcalc qw(Today Today_and_Now Add_Delta_Days);
use Cwd;

my $ConfigObject = Kernel::Config->new();

print "backup.pl - a perl backup script for OTRS <$VERSION>\n";

# removal options
my $REMOVE_OLD = 0;
my $REMOVE_DAYS = 30;

# db dumping options
my $DB_DUMP_PROG = '';
my $DB_DUMP_OPTIONS = '';
my $DB_DUMP_FILE = "database_backup.sql";

# db dump compression options
my $DB_DUMP_COMPRESS_BZIP2 = 1;
my $DB_DUMP_COMPRESS_GZIP = 0;
my $DB_DUMP_COMPRESS_DO = 1;
my $DB_DUMP_COMPRESS_PROG = '';
my $DB_DUMP_COMPRESS_PROG_OPTIONS = '';

# location options
my $BINDIR = '';
my $CFGDIR = '';
my $BACKUPDIR = '';

# archiving options
my $ARCHIVE = 1;
my $ARCHIVE_PROG = '';
my $ARCHIVE_PROG_OPTIONS = '';
my $ARCHIVE_BACKUP = 0;
my $ARCHIVE_BACKUP_COMPRESS_BZIP2 = 0;
my $ARCHIVE_BACKUP_COMPRESS_GZIP = 0;


my $HELP = 0;
my $DB = '';



my($year, $month, $day, $hour, $minute, $secord) = Today_and_Now();
my $SUBBACKUPDIR = sprintf("%04d-%02d-%02d_%02d-%02d",$year,$month,$day,$hour,$minute);

GetOptions(
	   'bindir|b=s' => \$BINDIR,
	   'configdir|c=s' => \$CFGDIR,
	   'backupdir|d=s' => \$BACKUPDIR,
	   
	   'dbdumper|db-dumper=s' => \$DB_DUMP_PROG,
	   'dbdumperoptions|db-dumper-options=s' => \$DB_DUMP_OPTIONS,
	   'dbdumpcompress|db-dump-compress|compressdbdump|compress-db-dump!' => \$DB_DUMP_COMPRESS_DO,
	   'dbdumpcompressor|db-dump-compressor=s' => \$DB_DUMP_COMPRESS_PROG,
	   'dbdumpcompressoroptions|db-dump-compressor-options=s' => \$DB_DUMP_COMPRESS_PROG_OPTIONS,
	   'dbdumpfile|db-dump-file=s' => \$DB_DUMP_FILE,
	   
	   'archive|a!' => \$ARCHIVE,
	   'archiver=s' => \$ARCHIVE_PROG,
	   'archiveroptions|archiver-options=s' => \$ARCHIVE_PROG_OPTIONS,
	   
	   'removeoldbackups|remove-old-backups|r!' => \$REMOVE_OLD,
	   
	   'archivebackup|archive-backup' => \$ARCHIVE_BACKUP,
	   'bzip2|j' => \$ARCHIVE_BACKUP_COMPRESS_BZIP2,
	   'gzip|z' => \$ARCHIVE_BACKUP_COMPRESS_GZIP,
	   
	   'age|g=i' => \$REMOVE_DAYS,
	   
	   'help|h' => \$HELP);

my %substituteTable = (
		       backupdir => $BACKUPDIR,
		       subbackupdir => $SUBBACKUPDIR,
		       database => $ConfigObject->{Database},
		       dbdumpfile => $DB_DUMP_FILE,
		       dbhost => $ConfigObject->{DatabaseHost},
		       dbpassword => $ConfigObject->{DatabasePw},
		       dbuser => $ConfigObject->{DatabaseUser},
		       sourcedir => '',
		       targetfile => '',
		       );

my $substituteKeys = "%" . join(" %",(sort keys %substituteTable));

if ($HELP || !$BINDIR || !$CFGDIR || !$BACKUPDIR) {
    showHelp();
}

# figure out what database to use
determineDatabase();

# figure out how to compress the database
determineDBDumpCompressor();

# figure out how to archive the backup (if needed)
determineArchiver();

# remove old backups if needed
deleteOldBackups();

# create target for backup
createBackupSubdir();

# dump out the db
dumpDatabase();

# compress output of database dump
compressDatabaseDump();

# make backups of OTRS' configuration
backupConfigFiles();

# make backups of any on-disk articles
backupArticles();

# make backup an archive, if necessary
archiveBackup();




# --
# Help function, to explain how to use the script
# --
sub showHelp {
    print <<EOE;
Usage: $0 [-j|-z] [-r] [-g <days>] -b <binpath> -c <configpath> -d <backuppath>

  Examples:
    $0 -b /opt/otrs/bin -c /opt/otrs/Kernel/Config/ -d /data/otrs-backup

        Creates a timestamped directory in /data/otrs-backup, with
        default options for everything


    $0 -b /opt/otrs/bin -c /opt/otrs/Kernel/Config/ -d /data/otrs-backup \\
      --archiver zip --archiver-options "-9vr %targetfile.zip %sourcedir" \\
      --archive-backup --nocompressdbdump --noarchive

        Creates a timestamped zip file in /data/otrs-backup, not \\
        compressing the DB dump explicitly before zip file creation, \\
	also doesn\'t individually zip subdirectories which are normally \\
        archived

  Options:
   -b <path> => --bindir <path>     path to the OTRS bin directory
   -c <path> => --configdir <path>  path to the OTRS Kernel/Config/ directory
   -d <path> => --backupdir <path>  path to the backup directory
   -j        => --bzip2             use bzip2 for compression of archives
   -z        => --gzip              use gzip for compression of archives
   -r        => --removeoldbackups  remove old backups
             => --remove-old-backups
   -g <days> => --age <days>        number of days of backups to keep
   --dbdumper <program>             use an alternate database dumper
   --db-dumper <program>            use an alternate database dumper
   --dbdumperoptions <options>      commandline to use with alternate db dumper
   --db-dumper-options <options>    commandline to use with alternate db dumper
   --dbdumpcompressor <program>     use an alternate compressor for database
   --db-dump-compressor <program>   use an alternate compressor for database
   --dbdumpcompressoroptions <options>     commandline to use with alternate db dump compressor
   --db-dump-compressor-options <options>  commandline to use with alternate db dump compressor
   --[no]compressdbdump             [don\'t] compress the DB dump file
   --[no]compress-db-dump           [don\'t] compress the DB dump file
   --[no]archive                    [don\'t] use archiver (tar) to bundle subdirectories
   --archiver <program>             use an alternate archiving program than tar
   --archiveroptions <options>      commandline to use with alternate archiver
   --archiver-options <options>     commandline to use with alternate archiver

     tags for --*options options:
       $substituteKeys
EOE
;
    exit(1);
}



# --
# check for needed programs
# --
sub checkForProg {
    my $prog = shift;

    if($prog ne "" &&
       ! which($prog) ) {
	die "ERROR: Can't locate $prog!\n";
	#exit(1);
    }
}

# --
# substitute and expand tags
# --
sub substituteTags {
    my $originalString = shift;

    foreach my $key (keys %substituteTable) {
	$originalString =~ s/%$key/$substituteTable{$key}/g;
    }

    return $originalString;
}

# --
# determine database type
# --
sub determineDatabase {
    $ConfigObject->{DatabaseDSN} =~ /^(DBI:[^:]+):.*/;
    
    if($DB_DUMP_PROG ne '') {
	$DB = "User-specified";
	# prog and options specified on commandline
    }
    elsif($1 eq "DBI:mysql") {
	$DB = "MySQL";
	$DB_DUMP_PROG = "mysqldump";
	$DB_DUMP_OPTIONS = "-u %dbuser -p%dbpassword -h %dbhost %database > %backupdir/%subbackupdir/%dbdumpfile";
    }
    elsif($1 eq "DBI:Pg") {
	$DB = "PostgreSQL";
	$DB_DUMP_PROG = "pg_dump";
	$DB_DUMP_OPTIONS = "-f %backupdir/%subbackupdir/%dbdumpfile -h %dbhost -U %dbuser %database";
    }
    else {
	die "ERROR: Can't run backup script because there is no support for your database. Better start coding now or specify --db-dumper and --db-dumper-options.\n";
    }

    checkForProg($DB_DUMP_PROG);
}

# --
# determine compression program, if any
# --
sub determineDBDumpCompressor {
    if($DB_DUMP_COMPRESS_DO || $DB_DUMP_COMPRESS_PROG ne "") {
	if($DB_DUMP_COMPRESS_PROG ne "") {
	    # options specified on command line
	    die "Need to specify compressor options when specifying compressor for db dump compression!\n" if($DB_DUMP_COMPRESS_PROG_OPTIONS eq "");
	}
	elsif($DB_DUMP_COMPRESS_BZIP2) {
	    $DB_DUMP_COMPRESS_PROG = "bzip2";
	    $DB_DUMP_COMPRESS_PROG_OPTIONS = "-9 %sourcedir"
		if($DB_DUMP_COMPRESS_PROG_OPTIONS eq "");
	}
	elsif($DB_DUMP_COMPRESS_GZIP) {
	    $DB_DUMP_COMPRESS_PROG = "gzip";
	    $DB_DUMP_COMPRESS_PROG_OPTIONS = "-9 %sourcedir"
		if($DB_DUMP_COMPRESS_PROG_OPTIONS eq "");
	}
	
	checkForProg($DB_DUMP_COMPRESS_PROG);
    }
}

# --
# determine archiving program, if any
# --
sub determineArchiver {
    if($ARCHIVE_PROG ne "") {
	print "ARCHIVE_PROG: $ARCHIVE_PROG\n";
	print "ARCHIVE_PROG_OPTIONS: $ARCHIVE_PROG_OPTIONS\n";
	# options specified on command line
	die "Need to specify compressor options when specifying compressor for db dump compression!\n" if($ARCHIVE_PROG_OPTIONS eq "");
    }
    else {
	$ARCHIVE_PROG = "tar";
 	$ARCHIVE_PROG_OPTIONS = "cj %sourcedir -f %targetfile.tar.bz2"
 	    if($ARCHIVE_PROG_OPTIONS eq "");
    }

    if($ARCHIVE_BACKUP_COMPRESS_BZIP2) {
	$ARCHIVE_BACKUP = 1;
    }
    elsif($ARCHIVE_BACKUP_COMPRESS_GZIP) {
	$ARCHIVE_BACKUP = 1;
 	$ARCHIVE_PROG_OPTIONS = "cz %sourcedir -f %targetfile.tar.gz";
    }
    
    checkForProg($ARCHIVE_PROG);
}

# --
# delete old backups
# --
sub deleteOldBackups {
    if($REMOVE_OLD) {
	my($dyear,$dmonth,$dday) = Add_Delta_Days($year, $month, $day, -$REMOVE_DAYS);
	
	my $OLDBACKUP = sprintf("%04d-%02d-%02d*",$dyear,$dmonth,$dday);
	print "deleting old backups in ${BACKUPDIR}/${OLDBACKUP}...";
	File::Remove::remove \1, "${BACKUPDIR}/${OLDBACKUP}";
	print "done\n";
    }
}

# --
# create backup sub directory
# --
sub createBackupSubdir {
    print "Creating backup directory: ${BACKUPDIR}/${SUBBACKUPDIR}...";
    mkdir "${BACKUPDIR}/${SUBBACKUPDIR}" ||
	die "Making backup dir ${BACKUPDIR}/${SUBBACKUPDIR}: $!";

    print "done\n"
}

# --
# dump database
# --
sub dumpDatabase {
    my $expanded_options = substituteTags($DB_DUMP_OPTIONS);

    print "Preparing to dump $DB rdbms $ConfigObject->{Database}\@$ConfigObject->{DatabaseHost}...";
    system "$DB_DUMP_PROG $expanded_options\n";

    if(WIFEXITED($?) && WEXITSTATUS($?)) {
	die "Dump process exited with status " . WEXITSTATUS($?) . "\n";
    }

    print "done\n"
}

# --
# compress database dump
# --
sub compressDatabaseDump {
    if($DB_DUMP_COMPRESS_DO) {
	print "compressing database dump...";
	$substituteTable{'sourcedir'} = "${BACKUPDIR}/${SUBBACKUPDIR}/${DB_DUMP_FILE}";
	my $expanded_options = substituteTags("$DB_DUMP_COMPRESS_PROG $DB_DUMP_COMPRESS_PROG_OPTIONS");
	system "$expanded_options";
	
	if(WIFEXITED($?) && WEXITSTATUS($?)) {
	    die "Compression process exited with status " . WEXITSTATUS($?) . "\n";
	}
	
	print "done\n";
    }
}

# --
# config files backup
# --
sub backupConfigFiles {
    print "Backing up config files, ${CFGDIR}/* ${CFGDIR}/../Config.pm ...";
    mkdir "${BACKUPDIR}/${SUBBACKUPDIR}/Config/";
    File::NCopy::copy \1,"${CFGDIR}/*", "${BACKUPDIR}/${SUBBACKUPDIR}/Config/";
    File::NCopy::copy "${CFGDIR}/../Config.pm", "${BACKUPDIR}/${SUBBACKUPDIR}/";
    print "done\n";
}

# --
# articles backup
# --
sub backupArticles {
    my $articleDir = $ConfigObject->Get('ArticleDir');

    my $cwd = getcwd();
    if(chdir $articleDir) {
	print "Backing up $articleDir...";

	$substituteTable{'sourcedir'} = ".";
	$substituteTable{'targetfile'} = "${BACKUPDIR}/${SUBBACKUPDIR}/article_backup";
	if($ARCHIVE) {

	    my $expanded_options = substituteTags("$ARCHIVE_PROG $ARCHIVE_PROG_OPTIONS");
	    system "$expanded_options";
	    
	    if(WIFEXITED($?) && WEXITSTATUS($?)) {
		die "Article backup process exited with status " . WEXITSTATUS($?) . "\n";
	    }
	}
	else {
	    mkdir $substituteTable{'targetfile'};
	    File::NCopy::copy \1,$substituteTable{'sourcedir'},$substituteTable{'targetfile'};
	}

	chdir $cwd;
	
	print "done\n";
    }
    
}

# --
# archive backup
# --
sub archiveBackup {
    if($ARCHIVE_BACKUP) {
	print "Compressing ${SUBBACKUPDIR}...";

	my $cwd = getcwd();
	if(chdir ${BACKUPDIR}) {
	    $substituteTable{'sourcedir'} = $SUBBACKUPDIR;
	    $substituteTable{'targetfile'} = $SUBBACKUPDIR;
	    my $expanded_options = substituteTags("$ARCHIVE_PROG $ARCHIVE_PROG_OPTIONS");
	    system "$expanded_options";
	    
	    if(WIFEXITED($?) && WEXITSTATUS($?)) {
		die "Archive compression process exited with status " . WEXITSTATUS($?) . "\n";
	    }

	    File::Remove::remove \1, "${BACKUPDIR}/${SUBBACKUPDIR}";
	    chdir $cwd;
	    
	    print "done\n";
	}
    }
}

