#!/usr/pkg/bin/perl -w
# CD-R(W) backup utility
# Copyright (C) 2001 John-Paul Gignac
#
# 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.

$prefix = '/usr/pkg';
$exec_prefix = "${prefix}";
$sbindir = "${exec_prefix}/sbin";

$cdsplit = "${sbindir}/cdsplit";
$cdappend = "${sbindir}/cdappend";
$ssh = '/usr/bin/ssh';

# Set remote commands to local. They will be overriden later
# if $remotehost is defined.
$cat = '/bin/cat';
$rm = '/bin/rm';
$gnutar = '/usr/pkg/bin/gtar';
$rcat = $cat;
$rrm = $rm;
$rgnutar = $gnutar;

sub esc_shell {
	$_ = shift;
	s/([^-.\/_a-zA-Z0-9])/\\$1/g;
	return $_;
}

sub abs_path {
	$_ = shift;
	if( -d $_) {
		$path = "$_";
		$filename = '';
	} elsif( /^(.*\/)([^\/]+)\/*$/) {
		$path = $1;
		$filename = $2;
		die "$path: No such directory\n" if( !-d $path);
	} else {
		$path = ".";
		$filename = $_;
	}
	$cmd = "cd ".esc_shell($path)." && /bin/pwd";
	$_ = `$cmd`;
	chomp;
	if (substr($_,-1) eq '/' || $filename eq '') {
		return "$_$filename";
	} else {
		return "$_/$filename";
	}
}

sub nice_dir {
	$dir = shift;
	die "$dir: Pathname must be absolute.\n"
		if( $dir !~ /^\//);
	# Get rid of certain anomalies
	$dir =~ s/\/\/+/\//g;
	$dir =~ s/\/$// if( $dir ne '/');
	return $dir;
}

sub usage {
	print
	"cdbkup is used to backup Unix filesystems onto CD-R(W)s. It can\n".
	"be used either interactively for large (multi-CD) backups, or non-\n".
	"interactively for higher-frequency (eg, daily) single-CD backups.\n".
	"\n".
	"Usage: $0 0-9 [OPTION]... DEVICE DUMPDIR\n".
	"Options:\n".
	" 0-9                 Indicates the backup level\n".
	" --help              Show this help message\n".
	" -1, --single        Burn last disk in single session mode\n".
	" -a, --append        Append the backup onto a multisession CD-R(W)\n".
	" -b, --blank         Blank all disks before writing (conflicts with -r)\n".
	" -c, --compress=TYPE Set compression to one of gz|bz2|none. Default: gz\n".
	" -e, --exclude=PATH  Exclude the specified file or directory\n".
	" -h, --host=HOST     Specify the hostname for remote backup\n".
	" -I, --no-iso        Don't create ISO images\n".
	" -l, --label=NAME    Set the volume label\n".
	" -m, --cross-mp      Cross mount points when dumping\n".
	" -r, --recycle       Same as -a, but blank the CD-RW first if data won't fit\n".
	" -s, --speed=SPEED   Burn at specified speed (default: 2)\n".
	" -S, --cdsize=SIZE   Specify the size of the storage media\n".
	" -t, --test          Don't burn the CD - just create archive files\n".
	" -w, --workdir=PATH  Set working directory (default: /tmp/cdworkdir)\n".
	" -z, --zip-here      For remote backups, perform compression locally\n".
	" -V, --version       Show version info\n";
	exit 1;
}

$append = 0;
$recycle = 0;
$blank = 0;
$compress = 'gz';
@excludes = ();
$remotehost = '';
$label = '';
$multi = 1;
$cdspeed = 2;
$test = 0;
$iso = 1;
$crossmp = 0;
$workdir = abs_path("/tmp/cdworkdir");
$ziphere = 0;
$cdsize_all = 0;

# Make sure that there is at least one argument
usage() if( !scalar @ARGV);

# Parse the backup level
$_ = shift;
if( $_ eq '-V' || $_ eq '--version') {
	print "cdbkup-" . '1.0' . "\n";
	exit 0;
}
usage() if( !/^[0-9]$/);
$level = int( $_);

# Parse the options
while( scalar @ARGV) {
	$_ = shift;
	if( substr( $_, 0, 1) ne '-') {
		unshift( @ARGV, $_);
		last;
	}

	$i = index( $_, '=');
	if( $i > 0) {
		unshift( @ARGV, substr( $_, $i+1));
		$_ = substr( $_, 0, $i);
	}

	if( $_ eq '--') {
		last;
	} elsif( $_ eq '-1' || $_ eq '--single') {
		$multi = 0;
	} elsif( $_ eq '-a' || $_ eq '--append') {
		$append = 1;
	} elsif( $_ eq '-b' || $_ eq '--blank') {
		$blank = 1;
	} elsif( $_ eq '-c' || $_ eq '--compress') {
		$compress = shift;
		die "Unknown compression format: $compress\n"
			if($compress ne 'gz' && $compress ne 'bz2' && $compress ne 'none');
	} elsif( $_ eq '-e' || $_ eq '--exclude') {
		$exclude = nice_dir(shift);
		push @excludes, $exclude;
	} elsif( $_ eq '-h' || $_ eq '--host') {
		$remotehost = shift;
		die "Invalid hostname: $remotehost\n"
			if( $remotehost =~ /[^-._a-zA-Z0-9]/);
	} elsif( $_ eq '-I' || $_ eq '--no-iso') {
		$iso = 0;
	} elsif( $_ eq '-l' || $_ eq '--label') {
		$label = shift;
		die "Invalid volume label: $label\n" if( $label =~ /[^-._a-zA-Z0-9]/);
	} elsif( $_ eq '-m' || $_ eq '--cross-mp') {
		$crossmp = 1;
	} elsif( $_ eq '-r' || $_ eq '--recycle') {
		$recycle = 1;
	} elsif( $_ eq '-s' || $_ eq '--speed') {
		$cdspeed = shift;
		die "Invalid burn speed: $cdspeed\n" if( $cdspeed !~ /^[1-9][0-9]*$/);
		$cdspeed = int($cdspeed);
	} elsif( $_ eq '-S' || $_ eq '--cdsize') {
		$cdsize_all = int( shift);
	} elsif( $_ eq '-t' || $_ eq '--test') {
		$test = 1;
	} elsif( $_ eq '-w' || $_ eq '--workdir') {
		$workdir = abs_path(shift);
	} elsif( $_ eq '-z' || $_ eq '--zip-here') {
		$ziphere = 1;
	} else {
		usage();
	}
}
usage() if( scalar @ARGV != 2);
$cddevice = shift;
$dumpdir = nice_dir(shift);

die "You can't use both -a and -r.\n" if( $append && $recycle);
die "You can't use -b with -r.\n" if( $blank && $recycle);
die "--no-iso requires both --test and --cdsize.\n"
	if (!$iso && (!$test || $cdsize_all == 0));

# --recycle implies append mode.
$append = 1 if( $recycle);

# Program is interactive if not in append mode
$| = 1 unless( $append);

if( $remotehost eq '') {
	# Ignore the zip-here option
	$ziphere = 0;

	# Add the workdir to the exclude list
	push @excludes, $workdir;
} else {
	# For remote operations we must use non-absolute paths
	$rrm = 'rm';
	$rcat = 'cat';
	$rgnutar = 'tar';
}

# Figure out the hostname of the machine being backed up.
if( $remotehost eq '') {
	chomp( $hostname = `/bin/hostname`);
} else {
	$hostname = $remotehost;
}

if( $label eq '' && $dumpdir ne '/') {
	print STDERR "When dumping a directory other than '/',";
	print STDERR " --label is required.\n";
	exit 1;
}

# The volume label should default to the hostname.
$label = $hostname if( $label eq '');

chomp( $date = `/bin/date +%Y-%m-%d`);
$datedlabel ="$label-$date-$level";

$name = "$datedlabel.tar";
$name .= ".$compress" if( $compress ne 'none');

if( $test && $remotehost eq '') {
	# Add the test output file to the exclude list
	push @excludes, abs_path($name);
}

$data = "/var/db/cdbkup/$label";
if( -e "$data.info") {
	if( `$rcat $data.info` ne "$hostname $dumpdir\n") {
		print STDERR "The volume label '$label' is already taken.\n";
		print STDERR "Choose a unique label using the --label option.\n";
		exit 1;
	}
} else {
	system( "echo $hostname $dumpdir > $data.info");
}

$listed = "/tmp/cdbkup-$label";
push @excludes, $listed;

sub remotize {
	$cmd = shift;
	return "$ssh $remotehost ".esc_shell($cmd) if( $remotehost ne '');
	return $cmd;
}

sub execute {
	$cmd = shift;
#	print "Executing command: $cmd\n";
	return system( $cmd) == 0;
}

# Locate the appropriate listed-incremental file.
$recent = $level;
$recentdate = 0;
for( $i=0; $i < $level; $i++) {
	open( SNAR, "<$data-$i.snar") || next;
	chomp( $date = <SNAR>);
	close( SNAR);
	if( $date =~ /^[0-9]+$/) {
		$date = int( $date);
		if( $date > $recentdate) {
			$recent = $i;
			$recentdate = $date;
		}
	}
}
if( $recent < $level) {
	execute( "$cat $data-$recent.snar | ".remotize( "$rcat > $listed")) ||
		die "Can't copy the snar file.\n";

	($sec,$min,$hour,$mday,$mon,$year,$dummy,$dummy) = localtime($recentdate);
	print "Dumping at level $level, relative to level $recent, dated ".
		sprintf("%04d-%02d-%02d %02d:%02d:%02d.\n",
			$year+1900, $mon+1, $mday, $hour, $min, $sec);
} else {
	execute( remotize( "$rrm -f $listed" ) ) ||
		die "Can't remove stray snar file.\n";
	print "Dumping at level $level.\n";
}

$tarcmd = "cd ".esc_shell($dumpdir)." && $rgnutar -f - -cS";
$tarcmd .= "l" unless ($crossmp);
unless( $ziphere) {
	$tarcmd .= "z" if( $compress eq 'gz');
	$tarcmd .= "j" if( $compress eq 'bz2');
}

$tarcmd .= " --listed-incremental=$listed".
	" --label=".esc_shell($datedlabel);
foreach $exclude (@excludes) {
	$prefix = ($dumpdir eq '/') ? $dumpdir : "$dumpdir/";
	if( substr( $exclude, 0, length($prefix)) eq $prefix) {
		$tarcmd .= " --exclude=./".
			esc_shell(esc_shell(substr($exclude, length($prefix))));
	}
}
$tarcmd .= " .";

$tarcmd = remotize( $tarcmd);
$tarcmd = "($tarcmd)";
if( $ziphere) {
	$tarcmd .= " | /usr/bin/gzip" if( $compress eq 'gz');
	$tarcmd .= " | /usr/bin/bzip2" if( $compress eq 'bz2');
}

if( $test && $cdsize_all == 0) {
	execute( "$tarcmd > ".esc_shell($name)); # Must ignore exit code
} elsif( $append && !$test) {
	$options = "--name=".esc_shell($name);
	$options .= " --speed=$cdspeed";
	$options .= " --workdir=".esc_shell($workdir);
	$options .= " --blank" if( $blank);
	$options .= " --recycle" if( $recycle);
	$options .= " --test" if( $test);
	$options .= " --cdsize=$cdsize_all" if( $cdsize_all > 0);
	$options .= " -- ".esc_shell( $cddevice);
	execute( "$tarcmd | $cdappend $options") || exit 1;
} else {
	$options = "--name=".esc_shell($name);
	$options .= " --speed=$cdspeed";
	$options .= " --workdir=".esc_shell($workdir);
	$options .= " --blank" if( $blank);
	$options .= " --single" if( !$multi);
	$options .= " --test" if( $test);
	$options .= " --cdsize=$cdsize_all" if( $cdsize_all > 0);
	$options .= " --no-iso" unless ($iso);
	$options .= " -- ".esc_shell( $cddevice);
	$options .= " ".esc_shell( $tarcmd);
	execute( "$cdsplit $options") || exit 1;
}

execute( remotize("($rcat $listed && $rrm $listed)").
	" | $cat > $data-$level.snar") || die "Can't copy the snar file.\n";
