#!/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: cdappend.in,v 1.3 2002/02/28 12:34:22 bubuchka Exp $

$slack = 65536;      # Amount of space, in bytes, to allocate for ISO info

# 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
	"cdappend copies STDIN onto a single CD-R(W).  It is useful for\n".
	"burning high-frequency incremental backups that should always fit\n".
	"on a single CD.\n".
	"\n".
	"Usage: $0 [OPTION]... DEVICE\n".
	"Options:\n".
	" -1, --single        Burn data in single session mode\n".
	" -b, --blank         Blank the CD-RW first (conflicts with -r)\n".
	" -h, --help          Show this help message\n".
	" -n, --name=NAME     Set the filename, as it will appear on CD\n".
	" -r, --recycle       Blank the CD-RW if data won't fit in available space\n".
	" -s, --speed=SPEED   Burn at specified speed (default: 2)\n".
	" -S, --cdsize=SIZE   Specify the size of the storage medium in bytes\n".
	" -t, --test          Don't burn the CDs - just create an archive file\n".
	" -w, --workdir=PATH  Set working directory (default: /tmp/cdworkdir)\n".
	" -V, --version       Show version info\n";
	exit 1;
}

# Option defaults
$multiok = 1;
$blank = 0;
$filename = "cdbkup";
$recycle = 0;
$cdspeed = 2;
$cdsize_all = 0;
$test = 0;
$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') {
		$blank = 1;
	} elsif( $_ eq '-n' || $_ eq '--name') {
		$filename = shift;
	} elsif( $_ eq '-r' || $_ eq '--recycle') {
		$recycle = 1;
	} 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 "cdappend-" . '1.0' . "\n";
		exit 0;
	} else {
		usage();
	}
}
usage() if( scalar @ARGV != 1);
$cddevice = shift;

die "Options -b and -r conflict.\n" if( $blank && $recycle);

$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) {
	die "'$tmpimg' already exists.  Delete it first.\n" if( -e $tmpimg);
} else {
	mkdir( $workdir, 0700) ||
		die "Unable to create working directory: $!\n";
}

$session = $next_sector = 0;
$append = !$blank;

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( $blank || $test) {
	$maxsize = $cdsize;
	$avail = $maxsize;
} 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 = ($recycle ? $cdsize : $avail);
}

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

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( STDIN, $data, 8192)) {
	$imgsize += $rc;
	if( netsize($imgsize) > $maxsize) {
		die "Too much input data to fit on CD.\n";
	}
	print TMPIMG $data || die "Can't write data to tmp file: $!\n";
}
die "Error reading STDIN: $!\n" if( !defined( $rc));
close( TMPIMG) || die "Error closing tmp file: $!\n";
die "Zero-length file.\n" if ($imgsize == 0);

print "Image file size: $imgsize\n";
print "Actual required space on CD-R(W): ".netsize($imgsize)."\n";

if( !$blank && netsize($imgsize) > $avail) {
	die "Insufficient remaining space on this disk.\n" if( !$recycle);
	$blank = 1;
	$append = 0;
}

if( $blank && !$test) {
	print "Blanking CD-RW...\n";
	if (system("$cdrec blank=fast 2>&1") != 0) {
		die "Error attempting to blank CD-RW.\n";
	}
}

if( $test) {
	print "Creating file: $filename.iso\n";
	$cmd = "$mkiso $tmpimg > $filename.iso";
} else {
	$mkisooptions = $append ? " -C $session,$next_sector -M $cddevice" : "";
	$cdrecoptions = $multiok ? " -multi" : "";
	$cmd = "$mkiso$mkisooptions $tmpimg | $cdrec$cdrecoptions -";
}

# Burn the CD
if( system( $cmd) != 0) {
	print "Error burning CD.\n";
	print "Error reported by recording process.\n";
	print "You may be able to fixate the disk with:\n";
	print "\t$cdrec -fix\n";
	exit 2;
}

# Remove the file
unlink( $tmpimg);
rmdir( $workdir);  # It's okay if this fails
