#!/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.
#
# $Id: cdsplit.in,v 1.7 2002/03/06 08:09:58 jjgignac Exp $

$slack = 65536;      # Amount of space, in bytes, to allocate for ISO info
                        # Eventually, it would be nice if this could be
                        # replaced with a proper prediction of ISO
                        # filesystem space overhead
$| = 1;

# The following seem to be constants for CD-Rs.
$minsize = 300 * 2048;     # The minimum session size.
$sess1waste = 4500 * 2048; # Allocate this much extra space for first session
$fixatepad = 6902 * 2048;  # The space required to fixate a session.

sub usage {
	print
	"cdsplit copies the output of any shell command onto one or more\n".
	"CD-R(W)s.  It is useful as an interactive front-end for multi-disk\n".
	"CD backups.\n".
	"\n".
	"Usage: $0 [OPTION]... DEVICE COMMAND\n".
	"Options:\n".
	" -1, --single        Burn last disk in single session mode\n".
	" -b, --blank         Blank all disks before burning\n".
	" -h, --help          Show this help message\n".
	" -I, --no-iso        Don't create ISO images (Requires --test)\n".
	" -n, --name=NAME     Set the filename prefix, as it will appear on CD\n".
	" -s, --speed=SPEED   Burn at specified speed (default: 2)\n".
	" -S, --cdsize=SIZE   Specify the size of the storage media in bytes\n".
	" -t, --test          Split only, don't burn CD (default cdworkdir=./)\n".
	" -w, --workdir=PATH  Set working directory (default: /tmp/cdworkdir)\n".
	" -V, --version       Show version info\n";
	exit 1;
}

# Option defaults
$blankfirst = 0;
$multiok = 1;
$filename = "cdbkup";
$cdspeed = "2";
$cdsize_all = 0;
$test = 0;
$iso = 1;
$workdir = "/tmp/cdworkdir";

# 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') {
		$multiok = 0;
	} elsif( $_ eq '-b' || $_ eq '--blank') {
		$blankfirst = 1;
	} elsif( $_ eq '-I' || $_ eq '--no-iso') {
		$iso = 0;
	} elsif( $_ eq '-m' || $_ eq '--multi') {
		print STDERR "Warning: --multi option is deprecated.\n";
	} elsif( $_ eq '-n' || $_ eq '--name') {
		$filename = shift;
	} elsif( $_ eq '-s' || $_ eq '--speed') {
		$cdspeed = int( shift);
	} elsif( $_ eq '-S' || $_ eq '--cdsize') {
		$cdsize_all = int( shift);
	} elsif( $_ eq '-t' || $_ eq '--test') {
		$test = 1;
	} elsif( $_ eq '-w' || $_ eq '--workdir') {
		$workdir = shift;
	} elsif( $_ eq '-V' || $_ eq '--version') {
		print "cdsplit-" . '1.0' . "\n";
		exit 0;
	} else {
		usage();
	}
}
usage() if( scalar @ARGV != 2);
$cddevice = shift;
$backupcmd = shift;

die "--no-iso can not be used without --test.\n" if (!$iso && !$test);

sub netsize {
	if ($iso) {
		$size = (2047 + shift) & ~2047;
		return $slack + $fixatepad + ($size > $minsize ? $size : $minsize);
	} else {
		$size = shift;
		return $size;
	}
}

die "The specified device size ($cdsize_all) is too small.  It must be\n".
	"at least ".netsize(0)." in order to account for ISO filesystem\n".
	"and CD burning overhead.\n"
	if( $iso && $cdsize_all > 0 && $cdsize_all < netsize(0));

$workdir = "." if ($test && $workdir eq "/tmp/cdworkdir");

$mkiso = "/usr/pkg/bin/mkisofs -quiet -R -J";
$cdrec = "/usr/pkg/bin/cdrecord -v -data dev=$cddevice speed=$cdspeed";

$tmpimg = "$workdir/$filename";

# Prepare the working directory
if( -d $workdir) {
	`/bin/rm -f $tmpimg.*`;
} else {
	mkdir( $workdir, 0700) ||
		die "Unable to create working directory: $!\n";
}

$curdisk = 0;

$cmdworking = 0;
while(1) {
	$curdisk ++;
	print "Making $curdisk";
	$digit = ($curdisk % 10);
	if( $digit<1 || $digit>3 || ($curdisk%100)-$digit==10 ) { print "th"; }
	elsif( $digit == 1) { print "st"; }
	elsif( $digit == 2) { print "nd"; }
	else { print "rd"; }
	print " image:\n";

	if( !$test) {
		print "\07Insert disk $curdisk, then press Enter :";
		<STDIN>;

		$session = $next_sector = 0;
		$append = !$blankfirst;
	}

	if( $cdsize_all > 0) {
		$cdsize = $cdsize_all;
	} elsif( $test) {
		$cdsize = 650000000;
	} else {
		print "Fetching disc size: ";
		$cdsize = 0;
		open(CDR, "$cdrec -atip 2>&1 |");
		while (<CDR>) {
			if (/ATIP start of lead out: +([0-9]+)/i) {
				$cdsize = $1 * 2048 - $sess1waste;
			}
		}
		close CDR;
		if ($cdsize == 0) {
			$cdsize = 650000000;
			print "(Unknown, using default: $cdsize)\n";
		}
		else {
			print "$cdsize bytes\n";
		}
	}

	if( $blankfirst || $test) {
		$maxsize = $cdsize;
	} else {
		print "Fetching multisession information: ";
		open(CDR, "$cdrec -msinfo 2>&1 |");
		while (<CDR>) {
			if (/^ *([0-9]+,([0-9]+)) *$/) {
				($session, $next_sector) = ($1, $2);
			}
		}
		close CDR;
		$avail = $cdsize - 2048 * $next_sector;
		if ($next_sector == 0) {
			print "blank\n";
			$append = 0;
		}
		else {
			$avail += $sess1waste;
			print "$avail bytes remaining\n";
		}
		$maxsize = $avail;
	}

	$increment = 2048;

	if( !$cmdworking) {
		# Run the command that produces the data.  Note that we don't want
		# to run the command until we need it, since its user interaction
		# might interfere with ours.
		open(INPUT, "$backupcmd|") || die "Can't execute backup command: $!\n";
		$cmdworking = 1;
	}

	print "  Collecting input data...\n";

	# Create the temporary file, watching out for filesize overload
	$imgsize = 0;
	open( TMPIMG, ">$tmpimg") || die "Can't open '$tmpimg': $!\n";
	while( $rc = read( INPUT, $data, $increment)) {
		$imgsize += $rc;
		print TMPIMG $data || die "Can't write data to tmp file: $!\n";
		last if( netsize($imgsize + $increment) > $maxsize);
	}
	if( $rc && !$iso && $maxsize > $imgsize) {
		# Let's make the non-ISO filesize exactly what was asked for.
		if( $rc = read( INPUT, $data, $maxsize - $imgsize)) {
			$imgsize += $rc;
			print TMPIMG $data || die "Can't write data to tmp file: $!\n";
		}
	}

	die "Error reading backup data: $!\n" if( !defined( $rc));
	close( TMPIMG) || die "Error closing tmp file: $!\n";

	# If we are done then we are done.
	# This only happens if the last disk is completely filled.
	last if ($imgsize == 0);

	print "  Temporary file size: $imgsize\n";

	if( $curdisk == 1 && $rc == 0) {
		# The whole thing fits on one CD.  We don't need to append the
		# disk number.
		$suffix = "";
		$thisimg = $tmpimg;
	} else {
		$suffix = ".$curdisk";
		$thisimg = "$tmpimg.$curdisk";
		rename( $tmpimg, $thisimg);
	}

	if( $blankfirst && !$test) {
		TRYBLANK: for(;;) {
			print "Blanking CD-RW...\n";
			if (system("$cdrec blank=fast 2>&1") != 0) {
				print "\07Error attempting to blank CD-RW $curdisk.\n";
				do {
					print "What now?\n".
						"\tr = retry (you may wish to change disks first)\n".
						"\tq = quit\n".
						"\tc = continue as if nothing happened\n: ";
					($ans = lc(<STDIN>)) =~ s/^\s*([^\s]).*$/$1/s;
					next TRYBLANK if ($ans eq "r");

					if( $ans eq "q") {
						print STDERR "Terminated at user request\n";
						exit 2;
					}
				} while ($ans ne "c");
			}
			last TRYBLANK;
		}
	}

	if( $test) {
		print "  Creating file: $filename$suffix";
		print ".iso" if ($iso);
		print "\n";

#		print "  Choosing command ... ";
		if ($iso) {
			$cmd = "$mkiso $thisimg > $filename$suffix.iso";
		} elsif (-e "$filename$suffix") {
			$cmd = "true";
		} else {
			$cmd = "/bin/mv $thisimg $filename$suffix";
		}
#		print "$cmd\n"
	} else {
		$mkisooptions = $append ? " -C $session,$next_sector -M $cddevice" : "";
		$cdrecoptions = ($multiok && $rc == 0) ? " -multi" : "";
		$cmd = "$mkiso$mkisooptions $thisimg | $cdrec$cdrecoptions -";
	}

	# Burn the CD
	TRYBURN: for(;;) {
		if(system($cmd)!=0) {
			print "\07Error saving image $curdisk occured.\n";
			print "Error reported by recording process or moving facility.\n";
			if( !$test) {
				print "You may be able to fixate the disk with:\n";
				print "\t$cdrec -fix\n";
				print "either now in another shell or later.\n";
			}
			do {
				print "What now?\n".
					"\tr = retry (you may wish to change disks first)\n".
					"\tq = quit\n".
					"\tc = continue as if nothing happened\n: ";
				($ans = lc(<STDIN>)) =~ s/^\s*([^\s]).*$/$1/s;
				next TRYBURN if ($ans eq "r");

				if( $ans eq "q") {
					print STDERR "Terminated at user request\n";
					exit 2;
				}
			} while( $ans ne "c");
		}
		last TRYBURN;
	}

	# Remove the file
	unlink( $thisimg) if ($iso);

	last unless( $rc > 0);
}

close( INPUT);

# Remove the file
unlink( $tmpimg) unless (!$iso && ($tmpimg eq $thisimg));
rmdir( $workdir);  # It's okay if this fails
