#!/usr/bin/perl -w

#####################
#					#
#	CONFIGURATION	#
#					#
#####################

#configuration section comes before subroutine section due to dependencies

#controls various factors of major modes of operation (e.g. CD-R/CD-RW vs. tape, etc.)
#note that modes other than 'CD' may not yet be functional or defined
my $mode='CD';

#where we have many local backup programs
my $backup_bin_dir='/home/m/michael/src/backup/bin';

#stuff we do before backup
my $pre_backup=$backup_bin_dir . '/pre_backup';

#file or device we use for spooling data
my $spool='/dev/vg00/scratch';

#maximum bytes we may safely store in $spool
#must not exceed size/capacity of $spool
my $max_spool_bytes=700*1024*1024;

#prefered block size in bytes to use when writing $spool
my $spool_bs=1024;

my $match_cd_dev;
my $limit_cd_speed_to;
my $cdrecord_options;
my $cdrecord_blank_options;
my $cd_size;
my $cd_speed;
my $erasable;
if($mode eq 'CD'){

	#string we use to match CD (CD-R/CD-RW/...) device when used within s
	$match_cd_dev=q|'MATSHITA' 'UJDA745 DVD/CDRW' '1\.02' Removable CD-ROM|;

	#set this as a maximum CD (re)write speed limit we'll use
	#if it's undefined, no specific maximum limit set in advance
	if($match_cd_dev eq q|'MATSHITA' 'UJDA745 DVD/CDRW' '1\.02' Removable CD-ROM|){
		#maximum for this unit,
		#limit to this speed even if media indicates higher speed
		$limit_cd_speed_to=12;
	}
	else{
		undef $limit_cd_speed_to;
	}

	#additional options we want to supply to cdrecord when we write data,
	#note that the following options will automatically be provided:
	#-dao dev=#,#,# speed=# tsize=#s -data -
	$cdrecord_options='-v -eject';
	if($match_cd_dev eq q|'MATSHITA' 'UJDA745 DVD/CDRW' '1\.02' Removable CD-ROM|){
		if($cdrecord_options){
			$cdrecord_options .= ' driveropts=burnproof';
		}
		else{
			$cdrecord_options='driveropts=burnproof';
		}
	}
	#additional options we want to supply to cdrecord when we blank CD,
	#note that the following options will automatically be provided:
	#dev=#,#,# speed=# blank=fast
	$cdrecord_blank_options='-v';

	#maximum data in bytes we write to $media
	#0 implies max of write to eof or nominal media capacity
	#$max_bytes_to_media=650*1024*1024;
	$max_bytes_to_media=0;

	#block size in bytes to use when writing $media
	$media_bs=2048;
}
#elsif(){} ...
else{
	die "$0: don't know \$mode=$mode, aborting:";
}

#stuff we do after backup
$post_backup=$backup_bin_dir . '/post_backup';

#how we generate first data to be backed up (e.g. configuration information,
#backup identifier(s), etc.)
$backup_first_data=$backup_bin_dir . '/backup_first_data';

#how we generate backup data
#use %% for a literal %
#use %d for filesystem device
#use %m for filesystem mount point
#use %t for filesystem type
$generate_backup_data=$backup_bin_dir . '/generate_backup_data' . ' %d %m %t %o';

#must generate list of filesystem (device), mount point and filesystem type,
#and options
#each line must have those items in that order separated by \0
#one line must be present for each filesystem to be backed up
#lines must appear in the desired order to be backed up
#option subfields are separated by comma (,)
$device__mount_point__type__options=$backup_bin_dir . '/device__mount_point__type__options';

#####################
#					#
#	INITIALIZATION	#
#					#
#####################

#initialization section comes before subroutine section due to dependencies

#volume number, starting with 1 for first volume
#we'll increment after we successfully write all desired data to the volume
my $volume_number=1;

#byte offset for start of data in $spool
#note that we treat $spool as if it were a ring buffer (so we can fill it,
#and read from it and write to media amounts that may be smaller than spool;
#then adjust our pointer in $spool to the start of data that we haven't yet
#successfully written to media)
#points to (zero based) offset for next byte to be read
#range 0..($max_spool_bytes-1)
my $spool_bytes_sod=0;
#likewise for end of data
#points to (zero based) offset for next byte to be written
#range 0..($max_spool_bytes-1)
my $spool_bytes_eod=0;
#note that if $spool_bytes_eod==$spool_bytes_sod then $spool is
#precisely empty or precisely full, otherwise
#($spool_bytes_eod-$spool_bytes_sod)%$max_spool_bytes
#tells us data bytes in $spool
#we also track with $data_bytes_in_spool and
#$spool_available(=$max_spool_bytes-$data_bytes_in_spool)
#for this and other reasons

#number of valid backup bytes contained in $spool
my $data_bytes_in_spool=0;

#free bytes we can write in $spool
my $spool_available=$max_spool_bytes;

if($max_spool_bytes%$spool_bs){
	die ("$0: (\$max_spool_bytes=$max_spool_bytes)%(\$spool_bs=$spool_bs)!=0, aborting:");
}

if($max_bytes_to_media&&$max_bytes_to_media%$spool_bs){
	die ("$0: (\$max_bytes_to_media=$max_bytes_to_media)&&(\$max_bytes_to_media=$max_bytes_to_media)%(\$spool_bs=$spool_bs), aborting:");
}

#meta data for data in spool
my @metatagspool=();

#Illegal seek
my $ESPIPE=29;

my $buffer;

my $index="volume:offset:contents\n";

my @device__mount_point__type__options=();

my $cd_dev;
if($mode eq 'CD'){
	if(! ($cd_dev=&cd_dev())){
		die "$0: &cd_dev() couldn't determine device for CD, aborting:";
	}
}#elsif($mode eq ...
else{
		die "$0: don't know \$mode=$mode, aborting:";
}

my $open_dmto="$device__mount_point__type__options |";
open(DMTO,$open_dmto) or
	die (	"$0: open(DMTO,'$open_dmto') failed: ",
			&diag_bang,
			", aborting:"
	);
while(<DMTO>)	{
	chomp;
	@_=split /\0/;
	if($#_==3){
		push @device__mount_point__type__options,[@_];
	}
	else{
		die "$0: failed to parse $_ (\$#_=$#_) from $device__mount_point__type__options, aborting:";
	}
}
if(!&diag_close(close(DMTO),$open_dmto,"$0: close(DMTO('$open_dmto')) error(s): ")
){
	die "$0: aborting:";
}

my $open_spool="+< $spool";
open(SPOOL,$open_spool) or
	die (	"$0: open(SPOOL,'$open_spool')) failed: ",
			&diag_bang,
			", aborting:"
	)
;

my $abort_write='';

#index state
$index_state='';
#'' (false) - "normal" case, haven't started to deal with writing index to media
#'start_flush' - initial flush (or attempt thereof) to media
#'spooled' - index built, stat already written to media, remainder spooled

#####################
#					#
#	SUBROUTINES		#
#					#
#####################

#$! diagnostic
sub diag_bang{
	return ("\$!=" . (0+$!) . "=$!");
}

sub min{
	if($#_<0){
		return undef;
	}
	while($#_){
		if($_[0]<$_[$#_]){
			pop;
		}
		else{
			shift;
		}
	}
	return $_[0];
}

#$? diagnostic(s)
sub diag_q{
	my @ret=();
	if($?){
		push @ret,"\$?=$?";
		if(my $e=$? >> 8){
			push @ret,"returned exit value $e";
		}
		if(my $s=$? & 0x7f){
			push @ret,"got signal: $s";
		}
		if($? & 0x80){
			push @ret,'dumped core';
		}
	}
	return @ret;
}

sub diag_close{
	#$_[0] is return value from close
	#$_[1] is string we used with open()
	#$_[2] is initial string portion for diagnostics
	#return true if things went fine as far as we care, false otherwise
	if($_[0]){
		return $_[0];
	}
	if($_[1] =~ /^\s*\||\|\s*$/o){
		#pipe
		if(defined $!){
			if($! == $ESPIPE){
				return 1;
			}
			elsif($!){
				print	STDERR	(	"$_[2]",
									&diag_bang,
									"\n"
				);
				return undef;
			}
			else{
				if($! != 0 || ! $?){
					#assertion failure
					die	(
							"$0: in diag_close() assertion failed: ",
							"(\$!=$!=",0+$!,") != 0 || ! (\$?=$?)",
							", aborting:"
					);
				}
				print	STDERR	(	"$_[2]",
									join(', ',&diag_q),
									"\n"
				);
				return undef;
			}
		}
		else{
			#! defined $!
			#assertion failure
			#(shouldn't have close return false and $! not defined)
			die "$0: in diag_close() assertion failed: close() returned false but \$!==undef, aborting:";
		}
	}
	else{
		#not pipe
		if($!){
			print	STDERR	(	"$_[2]",
								&diag_bang,
								"\n"
			);
			return undef;
		}
		else{
			#(shouldn't have close return false and $! false)
			die "$0: in diag_close() assertion failed: close() returned false but \$! is also false and we don't seem to be dealing with a pipe, aborting:";
		}
	}
}

sub diag_system{
	#$_[0] is return value from system
	#$_[1] is initial string portion for diagnostics
	#return 0 if everything went fine, true if something went wrong
	if(!$_[0]){
		if(!$?){
			return 0;
		}
		print	STDERR	(	"$_[1]",
							join(', ',&diag_q),
							"\n"
		);
		return $?;
	}
	if($_[0]==-1){
		print	STDERR	(	"$_[1]",
							&diag_bang,
							"\n"
		);
		return $!;
	}
	#$_[0] is true and $_[0] != -1
	if($_[0]!=$?){
		#we don't really expect $_[0]!=$?, but just in case:
		die "$0: $? != system(), aborting:";
	}
	#$_[0] is true and $_[0] != -1 and $_[0] == $?
	#therefore $? is true and != -1
	print	STDERR	(	"$_[1]",
						join(', ',&diag_q),
						"\n"
	);
	return $?;
}

#get cdrecord style device (dev=) information (\d+,\d+,\d+)
#returns device (in \d+,\d+,\d+ format) or undef
sub cd_dev{
	my $open_cd_dev='cdrecord -scanbus |';
	open(CD_DEV,$open_cd_dev) or
		print	STDERR	(	"$0: open(CD_DEV,'$open_cd_dev') failed: ",
							&diag_bang,
							"\n"
		),
		return undef;
	while(<CD_DEV>)	{
		chomp;
		if(s/^	(\d+,\d+,\d+)	  \d+\) $match_cd_dev$/$1/){
			if(&diag_close(close(CD_DEV),$open_cd_dev,"$0: close(CD_DEV('$open_cd_dev')) error(s): ")){
				return $_;
			}
			else{
				return undef;
			}
		}
	}
	&diag_close(close(CD_DEV),$open_cd_dev,"$0: close(CD_DEV('$open_cd_dev')) error(s): ");
	return undef;
}

sub cd_size_speed_erasable{
	my $size;
	my $speed;
	my $erasable;
	if(! $#_)	{
		my $open_cd_size_speed_erasable="cdrecord dev=$_[0] -atip |";
		open(CD_SIZE_SPEED_ERASABLE,$open_cd_size_speed_erasable) or
			print	STDERR	(	"$0: open(CD_SIZE_SPEED_ERASABLE,'$open_cd_size_speed_erasable') failed: ",
								&diag_bang,
								"\n"
			),
			return undef;
		while(my $input=<CD_SIZE_SPEED_ERASABLE>)	{
			chomp $input;
			if($input =~ /^  Is erasable$/){
				$erasable='y';
				#we quietly ignore multiple matches
			}
			elsif($input =~ s/^  ATIP start of lead out: (\d+) \([0-9:\/]+\)$/$1/){
				if(!defined $size){
					$size=$input*$media_bs;
				}
				else{
					#redundant match
					if($size!=$input*$media_bs){
						#but conflicting!
						$size=undef;
					}
				}
			}
			elsif($input =~ s/^  speed low: \d+ speed high: (\d+)$/$1/){
				if(!defined $speed){
					$speed=$input;
				}
				else{
					#redundant match
					if($speed!=$input){
						#but conflicting!
						$speed=undef;
					}
				}
			}
		}
		if(!&diag_close(close(CD_SIZE_SPEED_ERASABLE),$open_cd_size_speed_erasable,"$0: close(CD_SIZE_SPEED_ERASABLE('$open_cd_size_speed_erasable')) error(s): ")){
			return undef;
		}
		if(defined($size) && defined($speed)){
			return	($size,$speed,$erasable);
		}
		else	{
			return undef;
		}
	}
	else	{
		return	undef;
	}
}

sub write_to_spool{
	if($#_){
		die "$0: write_to_spool() bad argument count \$#_=$#_ (expected 0), aborting:";
	}
	my $l=length($_[0]);
	if(!$l){
		die "$0: write_to_spool() $l=length(\$_[0]), aborting:";
	}
	#$spool_bs is prefered (but not mandatory) size
	#if($l%$spool_bs){
	#	die ("$0: write_to_spool() (",$l%$spool_bs,"=(\$l=$l)%(\$spool_bs=$spool_bs))!=0, aborting:");
	#}
	if(! seek SPOOL,$spool_bytes_eod,0){
		die "$0: write_to_spool() seek SPOOL,$spool_bytes_eod,0 failed, aborting:";
	}
	my $sa=$spool_available-$l;
	if($sa<0){
		die "$0: write_to_spool() ((\$spool_available=$spool_available)-(\$l=$l))<0, aborting:";
	}
	#first handle anything that fits before wrap
	my $end_available=$max_spool_bytes-$spool_bytes_eod;
	#we always have at least some room before wrap
	if($l<=$end_available){
		#it will all fit
		if(!print SPOOL $_[0]){
			die "$0: write_to_spool() print SPOOL \$_[0] failed, aborting:";
		}
	}
	else{
		#need to wrap, first handle write to end
		if(!print SPOOL substr($_[0],0,$end_available)){
			die "$0: write_to_spool() print SPOOL substr(\$_[0],0,$end_available) failed, aborting:";
		}
		#now write whatever's left
		if(! seek SPOOL,0,0){
			die "$0: write_to_spool() seek SPOOL,$spool_bytes_eod,0 failed, aborting:";
		}
		if(!print SPOOL substr($_[0],$end_available)){
			die "$0: write_to_spool() print SPOOL substr(\$_[0],$end_available) failed, aborting:";
		}
	}
	#succeeded and done
	$spool_bytes_eod+=$l;
	$spool_bytes_eod%=$max_spool_bytes;
	$data_bytes_in_spool+=$l;
	$spool_available=$sa;
	return $l;
}

#lower level writing to MEDIA from SPOOL
#single argument tells us how many bytes to write
#return bytes written if no errors encountered, otherwise return undef
sub read_spool_write_media{
	if($#_){
		return undef;
	}
	if(! defined $_[0] || $_[0] < 0){
		#failed sanity checks
		return undef;
	}
	if(! seek SPOOL,$spool_bytes_sod,0){
		return undef;
	}
	my $yet_to_read=&min($_[0],$data_bytes_in_spool);
	my $sod=$spool_bytes_sod;
	my $yet_to_write=$_[0];
	my $buffer='';
	my $l=0;
	my $read_size;
	for(;;){
		#read and write data until done or error
		for(;;){
			#read until we have >=$media_bs bytes or we've read all
			if(! $yet_to_read){
				#finished reading
				last;
			}
			#align reads to $spool_bs:
			$read_size=$spool_bs-$sod%$spool_bs;
			#shorten if appropriate:
			$read_size=&min($read_size,$yet_to_read);
			#don't read beyond end of SPOOL
			if($sod+$read_size>$max_spool_bytes){
				$read_size=$max_spool_bytes-$sod;
			}
			#don't read beyond $spool_bytes_eod
			if($sod<$spool_bytes_eod&&$sod+$read_size>$spool_bytes_eod){
				$read_size=$spool_bytes_eod-$sod;
			}
			my $rbuffer;
			if((read SPOOL,$rbuffer,$read_size)!=$read_size){
				#we got something unexpected from our read
				return undef;
			}
			#read went okay
			$sod+=$read_size;
			$sod%=$max_spool_bytes; #wrap if we need to
			$yet_to_read-=$read_size;
			$buffer.=$rbuffer;
			$l=length($buffer);
			if($l>=$media_bs){
				last;
			}
		}
		if($abort_write){
			return undef;
		}
		#there may be more to write...
		#let's handle the most common case first:
		if($l==$media_bs){
			if(! print MEDIA $buffer){
				return undef;
			}
			if($abort_write){
				return undef;
			}
			$yet_to_write-=$media_bs;
			$buffer='';
			$l=0;
			next;
		}
		if($l==0){
			#presumably we've read all
			#this implies $yet_to_read==0
			if($yet_to_read!=0){
				eval	{
							die "$0: read_spool_write_media() assertion failure, (\$l=$l)==0, but (\$yet_to_read=$yet_to_read)!=0:";
				};
				print STDERR ($@);
				return undef;
			}
			#do we need to write more?
			if($yet_to_write==0){
				#all done
				return $_[0];
			}
			#$yet_to_write should be >0
			if(!($yet_to_write>0)){
				#if our presumption fails:
				eval	{
							die "$0: read_spool_write_media() !((\$yet_to_write=$yet_to_write)>0):";
				};
				print STDERR ($@);
				return undef;
			}
			my $write_size;
			if($yet_to_write>=$media_bs){
				$write_size=$media_bs;
			}
			else{
				$write_size=$yet_to_write;
				eval	{
							die "$0: read_spool_write_media() (\$write_size=$write_size)<(\$media_bs=$media_bs):";
				};
				print STDERR ($@);
				return undef;
			}
			if(!print MEDIA ("\0" x $write_size)){
				return undef;
			}
			if($abort_write){
				return undef;
			}
			$yet_to_write-=$write_size;
			next;
		}
		elsif($l>0){
			if($l>$media_bs){
				if(!print MEDIA (substr($buffer,0,$media_bs))){
					return undef;
				}
				if($abort_write){
					return undef;
				}
				$l=length($buffer=substr($buffer,$media_bs));
				$yet_to_write-=$media_bs;
				next;
			}
			#else...
			#$l>0&&$l!=$media_bs&&$l<$media_bs
			#we better be done reading, in this case
			if($yet_to_read==0){
				if(!print MEDIA ($buffer . "\0" x ($media_bs-$l))){
					return undef;
				}
				if($abort_write){
					return undef;
				}
				$buffer='';
				$l=0;
				$yet_to_write-=$media_bs;
				next;
			}
			#else...
			#$yet_to_read!=0&&$l>0&&$l!=$media_bs&&$l<$media_bs
			eval	{
						die "$0: read_spool_write_media() some value(s) not in expected range: \$yet_to_read=$yet_to_read \$l=$l \$media_bs=$media_bs:";
			};
			print STDERR ($@);
			return undef;
		}
		#else...
		#$l<0
		eval	{
					die "$0: read_spool_write_media() (\$l=$l)<0:";
		};
		print STDERR ($@);
		return undef;
	}
}

sub update_index{
	#offset with possible wrap relative to $spool_bytes_sod
	my $offset=($_[0]-$spool_bytes_sod)%$max_spool_bytes;
	if($offset%$media_bs){
		$offset=$offset . ' bytes';
	}
	else{
		$offset=($offset/$media_bs) . " blocks of $media_bs bytes";
	}
	$index.="$volume_number:$offset:$_[1]\n";
}

sub update_index_and_metatagspool{
	#$_[0] is bytes of data (possibly wrapped) relative to
	#$spool_bytes_sod
	if(!@metatagspool||!$_[0]){
		#no data or no metadata
		return;
	}
	#my relative $eod from $spool_bytes_sod to $spool_bytes_sod+$_[0] (possibly wrapping)
	my $eod=($spool_bytes_sod+$_[0])%$max_spool_bytes;
	for(;@metatagspool;){
		if($spool_bytes_sod<$eod){
			if($spool_bytes_sod<=$metatagspool[0][0]&&$metatagspool[0][0]<$eod){
				&update_index($metatagspool[0][0],$metatagspool[0][1]);
				shift @metatagspool;
			}
			else{
				return;
			}
		}
		else{
			if($metatagspool[0][0]<$eod||$spool_bytes_sod<=$metatagspool[0][0]){
				&update_index($metatagspool[0][0],$metatagspool[0][1]);
				shift @metatagspool;
			}
			else{
				return;
			}
		}
	}
}

sub write_spool_to_media{
	my $spool_bytes_to_write;
	if($mode eq 'CD'){
		my $cd_status=''; #track basic CD status
		#$cd_status:
		#loaded media loaded, we know size/speed/erasable status
		#write loaded + requested to write
		#blanked loaded + successfully blanked
		#blank loaded + requested to blank
		for(;;){	#loop until successfully written
			#loop until media loaded:
			for(;$cd_status ne 'loaded' && $cd_status ne 'write' && $cd_status ne 'blanked' && $cd_status ne 'blank';){
				print "$0: load media volume $volume_number and enter y to continue: \a";
				$_=<>;
				if(! /^y/io){
					next;
				}
				($cd_size,$cd_speed,$erasable)=&cd_size_speed_erasable($cd_dev);
				if(defined $cd_size && defined $cd_speed){
					$cd_status='loaded';
					last;
				}
				print STDERR "$0: failed to read CD media size and/or speed!\n";
				next;
			}
			if($erasable){
				#loop until we know if we want to blank this media
				for(;$cd_status ne 'write' && $cd_status ne 'blanked' && $cd_status ne 'blank';){
					print "$0: for media volume $volume_number:\nenter w to write or b to blank and then write: \a";
					$_=<>;
					if(! /^[bw]/io){
						next;
					}
					if(/^b/io){
						$cd_status='blank';
						last;
					}
					else{
						#/^w/io
						$cd_status='write';
						last;
					}
				}
			}
			elsif($cd_status eq 'loaded'){
				#!$erasable&&$cd_status eq 'loaded'
				#we can't erase it, so we must want to write it
				$cd_status='write';
			}
			if($cd_status eq 'blank'){
				#blank it first
				my $blank;
				if($cdrecord_blank_options){
					$blank="cdrecord $cdrecord_blank_options dev=$cd_dev speed=$cd_speed blank=fast";
				}
				else{
					$blank="cdrecord dev=$cd_dev speed=$cd_speed blank=fast";
				}
				if(&diag_system(system("$blank"),"$0: $blank error(s): ")){
					#blank attempt failed, so let's go to the top and ask again (may want to load different media)
					print	STDERR "$0: failed to blank volume $volume_number\n";
					$cd_status='';
					next;
				}
				else{
					$cd_status='blanked';
					#fall through to write
				}
			}
			#initial presumption:
			$spool_bytes_to_write=$cd_size;
			if($max_bytes_to_media){
				#possibly modified by $max_bytes_to_media
				$spool_bytes_to_write=&min($max_bytes_to_media,$spool_bytes_to_write);
			}
			#so far $spool_bytes_to_write should still be $media_bs aligned
			if($data_bytes_in_spool<$spool_bytes_to_write){
				$spool_bytes_to_write=$data_bytes_in_spool;
				#$spool_bytes_to_write may no longer be $media_bs aligned
				if(my $remainder_subtract=$spool_bytes_to_write%$media_bs){
					#$spool_bytes_to_write no longer $media_bs aligned, fix!
					$remainder_add=$media_bs-$remainder_subtract;
					#try increasing first:
					my $btw=$spool_bytes_to_write+$remainder_add;
					if($btw<=$cd_size&&(!$max_bytes_to_media||$btw<=$max_bytes_to_media)){
						$spool_bytes_to_write=$btw;
					}
					else{
						#increasing didn't work, try decreasing:
						$btw=$spool_bytes_to_write-$remainder_subtract;
						if($btw>0){
							$spool_bytes_to_write=$btw;
						}
						else{
							#uhm, we shouldn't really be able to get here, right?
							#but just in case:
							die "$0: write_spool_to_media() !((\$btw=$btw)>0), aborting:";
						}
					}
				}
			}
			if($spool_bytes_to_write%$media_bs){
				#assertion failure!
				die ("$0: write_spool_to_media() (",$spool_bytes_to_write%$media_bs,"=(\$spool_bytes_to_write=$spool_bytes_to_write)%(\$media_bs=%$media_bs))!=0, aborting:");
			}
			if(defined $limit_cd_speed_to){
				$cd_speed=&min($limit_cd_speed_to,$cd_speed);
			}
			#is this final where we start appending index on end?
			if(
				!$index_state
				&&
				$spool_bytes_to_write < (
					$max_bytes_to_media
					?
					&min($max_bytes_to_media,$max_spool_bytes,$cd_size)
					:
					&min($max_spool_bytes,$cd_size)
				)
			){
				#this is final where we start appending index on end
				#save these, in case we need to undo (such as if media write
				#fails and then media is changed to lower capacity media which
				#then doesn't have room for start of index)
				$save_index=$index;
				@save_metatagspool=@metatagspool;
				$index_state='start_flush'; #track our state
				&update_index_and_metatagspool($spool_bytes_to_write);
				my $offset=$spool_bytes_to_write;
				if($offset%$media_bs){
					$offset=$offset . ' bytes';
				}
				else{
					$offset=($offset/$media_bs) . " blocks of $media_bs bytes";
				}
				$index.="$volume_number:$offset:index\n";
				my $l=length($index);
				if(my $m=$l%$media_bs){
					#pad
					$l+=$media_bs-$m;
				}
				#will the whole index fit?
				if	(
						$spool_bytes_to_write + $l <= (
							$max_bytes_to_media
							?
							&min($max_bytes_to_media,$cd_size)
							:
							$cd_size
						)
					)
				{
					#yes it will all fit
					$cd_bytes_to_write=$spool_bytes_to_write+$l;
				}
				else{
					#no, it won't all fit,
					#fit as much as we can
					$cd_bytes_to_write=
						$max_bytes_to_media
						?
						&min($max_bytes_to_media,$cd_size)
						:
						$cd_size
					;
				}
			}
			else{
				#no index stuff to tag on at at this point
				$cd_bytes_to_write=$spool_bytes_to_write;
			}
			my $open_media;
			if($cdrecord_options){
				$open_media="| cdrecord -dao $cdrecord_options dev=$cd_dev speed=$cd_speed tsize=" . $cd_bytes_to_write/$media_bs . "s -data -";
			}
			else{
				$open_media="| cdrecord -dao dev=$cd_dev speed=$cd_speed tsize=" . $cd_bytes_to_write/$media_bs . "s -data -";
			}
			open(MEDIA,$open_media) or
				print	STDERR	(	"$0: open(MEDIA,$open_media) failed: ",
									&diag_bang,
									"\n"
				),
				$cd_status='',
				next;
			$abort_write='';
			$SIG{PIPE}=
				sub{
						$abort_write='y';
						#eval	{
						#			die "$0: caught SIGPIPE:";
						#};
						#print STDERR ($@);
				}
			;
			my $CD_bytes_written=&read_spool_write_media($spool_bytes_to_write);
			#possibly tell us a bit about &read_spool_write_media
			if(!defined $CD_bytes_written || $CD_bytes_written!=$spool_bytes_to_write){
				print STDERR ("$0: ",(defined($CD_bytes_written)?$CD_bytes_written:'undef'),"=&read_spool_write_media($spool_bytes_to_write)!=$spool_bytes_to_write\n");
			}
			if($index_state eq 'start_flush' && $CD_bytes_written==$spool_bytes_to_write){
				#flush what we can of the index
				for(
					my $i=0,my $l=length($index),my $buffer;
					!$abort_write&&$CD_bytes_written<$cd_bytes_to_write;
					$i+=$media_bs,$CD_bytes_written+=$media_bs
				)
				{
					if($i<$l){
						$buffer=substr($index,$i,$media_bs);
						if(my $remainder=length($buffer)%$media_bs){
							#pad $buffer
							$buffer.="\0" x ($media_bs-$remainder);
						}
					}
					else{
						#$l<=$i
						$buffer="\0" x $media_bs;
					}
					if(!print MEDIA $buffer){
						$CD_bytes_written=undef;
						last;
					}
					#else reinitialize ...
				}
			}
			#maybe the above writes all worked, maybe they didn't,
			#we don't know completely until we've also checked after
			#close(MEDIA)
			$SIG{PIPE}='DEFAULT';
			$abort_write='';
			if(&diag_close(close(MEDIA),$open_media,"$0: close(MEDIA($open_media)) error(s): ")&&$CD_bytes_written==$cd_bytes_to_write){
				#okay :-)
				last;
			}
			#else (failed to successfully write)
			print	STDERR "$0: failed to write MEDIA volume $volume_number,\n";
			if($index_state eq 'start_flush'){
				#since we didn't successfully write the media,
				#disavow any knowledge of having flushed any or all of the
				#$index to media (i.e. backtrack)
				$index=$save_index;
				@metatagspool=@save_metatagspool;
				$index_state=''; #track our state (reset)
			}
			if($erasable){
				if($cd_status ne 'blanked'){
					print	STDERR "perhaps try blanking it first\n";
				}
				else{
					print	STDERR "although it seems prior blanking succeeded\n";
				}
			}
			else{
				#!$erasable
				print	STDERR "and media does not appear to be erasable\n";
			}
			$cd_status='';
			next;
		}
		#we should only get here via last
		#wrote okay:
		#$spool_bytes_to_write have been written, so ...
		if(!$index_state){
			#"normal" $index and @metatagspool updating
			&update_index_and_metatagspool($spool_bytes_to_write);
		}
		#else already updated $index and @metatagspool earlier
		$spool_bytes_sod+=$spool_bytes_to_write;
		$spool_bytes_sod%=$max_spool_bytes;
		$data_bytes_in_spool-=$spool_bytes_to_write;
		$spool_available+=$spool_bytes_to_write;
		if($index_state eq 'start_flush'){
			#if we wrote at least some of the index to media...
			if	($cd_bytes_to_write < $spool_bytes_to_write + length($index))
			{
				#... and didn't write all of it, spool the remainder
				&write_to_spool
					(
						#spool what's left of the index that we didn't write
						#to media 
						substr
							(
								$index,

								#just wrote these to media
								$spool_bytes_to_write +

								length($index) -

								#just wrote this many bytes to media
								$cd_bytes_to_write
							)
					)
				;
				&pad_spool();
				$index_state='spooled';
			}
			#else wrote all of the index to media
			elsif($data_bytes_in_spool != 0){
				#assertion failure (if we wrote all of the index to media
				#we should be at the very end, and spool should be empty
				die ("$0: assertion failed (expected \$data_bytes_in_spool == 0): (\$index_state=$index_state) eq 'start_flush' && (\$cd_bytes_to_write=$cd_bytes_to_write) >= (\$spool_bytes_to_write=$spool_bytes_to_write) + (",length($index),"=length(\$index)) && (\$data_bytes_in_spool=$data_bytes_in_spool) != 0, aborting:");
			}
		}
		if($max_spool_bytes-$data_bytes_in_spool-$spool_available){
			#assertion failure!
			die	(
					"$0: assertion failed: (",
					$max_spool_bytes-$data_bytes_in_spool-$spool_available,
					"=(\$max_spool_bytes=$max_spool_bytes)-(\$data_bytes_in_spool=$data_bytes_in_spool)-(\$spool_available=$spool_available)",
					")!=0, aborting:"
			);
		}
		print "$0: successfully wrote volume $volume_number\n";
		++$volume_number;
	}#elsif($mode eq ...
	else{
			die "$0: don't know \$mode=$mode, aborting:";
	}
}

#push meta tag for current $spool_bytes_eod
sub metatagspool{
	push @metatagspool,([$spool_bytes_eod,$_[0]]);
}

sub pad_spool{
	#pad $spool to $media_bs boundary relative to $spool_bytes_sod
	#(with possible wrap) if we're not already there,
	#note that we're not guaranteed to have sufficient room in $spool
	#to do this (it may already be full or almost full), in which case we
	#don't pad it
	if((my $remainder=$data_bytes_in_spool%$media_bs)){
		$remainder=$media_bs-$remainder;
		if($spool_available>=$remainder){
			#the easy case - we have room
			&write_to_spool("\0" x $remainder);
		}
		#else insufficient or no room to pad
	}
	#else already on boundary, no padding needed
}

#####################
#					#
#	PRE_BACKUP		#
#					#
#####################

if("$pre_backup")	{
	#print "$0: executing pre_backup: $pre_backup\n";
	if(&diag_system(system("$pre_backup"),"$0: $pre_backup error(s): ")){
		die "$0: aborting:";
	}
	#print "$0: pre_backup successfully completed\n";
}

#####################
#					#
#	FIRST_BACKUP	#
#					#
#####################

if("$backup_first_data")	{
	#print "$0: executing backup_first_data: $backup_first_data\n";
	my $open_backup_data="$backup_first_data |";
	open(BACKUP_DATA,$open_backup_data) or
		die ("$0: open(BACKUP_DATA,\"$backup_first_data |\") failed: ",
				&diag_bang,
				", aborting:"
			)
	;

	&metatagspool("backup_first_data");
	for(;;)	{
		if(! $spool_available) {
			&write_spool_to_media();
			next;
		}
		my $read_size=&min($spool_bs,$spool_available);
		if((my $remainder=$spool_bytes_eod%$spool_bs)!=0){
			#if $spool_bytes_eod is not already $spool_bs aligned ...
			$remainder=$spool_bs-$remainder;
			$read_size=&min($remainder,$read_size);
		}
		$buffer_size=read BACKUP_DATA,$buffer,$read_size;
		if($buffer_size){
			&write_to_spool($buffer);
		}
		elsif(defined $buffer_size){
			#EOF
			if(!&diag_close(close(BACKUP_DATA),$open_backup_data,"$0: close(BACKUP_DATA('$open_backup_data')) error(s): ")){
				die "$0: aborting:";
			}
			#&metatagspool("end of $backup_first_data");
			last;
		}else{
			#ERROR
			die	(	"$0: read BACKUP_DATA,\$buffer,$read_size failed: ",
					&diag_bang,
					", aborting:"
			);
		}
	}

	#pad $spool to $media_bs if needed and possible
	&pad_spool();
	#print "$0: backup_first_data successfully completed\n";
}

#####################
#					#
#	FILESYSTEMS		#
#					#
#####################

while (@device__mount_point__type__options){
	my $device=$device__mount_point__type__options[0][0];
	my $mount_point=$device__mount_point__type__options[0][1];
	my $type=$device__mount_point__type__options[0][2];
	my $options=$device__mount_point__type__options[0][3];
	my $generate_backup_data_substituted='';
	my $generate_backup_data_position=0;
	for(;;){
		if($generate_backup_data_position>=length($generate_backup_data)){
			#nothing left to append or substitute
			last;
		}
		my $generate_backup_data_index=index $generate_backup_data,'%',$generate_backup_data_position;
		if($generate_backup_data_index<0){
			#nothing left to substitute but still have remainder to append
			$generate_backup_data_substituted .= substr($generate_backup_data,$generate_backup_data_position);
			last;
		}
		else{
			#something left to substitute
			if($generate_backup_data_index>$generate_backup_data_position){
				#if there's a non-empty chunk before the %, append it
				$generate_backup_data_substituted .= substr($generate_backup_data,$generate_backup_data_position,$generate_backup_data_index-$generate_backup_data_position);
				$generate_backup_data_position=$generate_backup_data_index;
			}
			#point to position after %
			++$generate_backup_data_position;
			if($generate_backup_data_position>=length($generate_backup_data)){
				#we just had a bare % on the end
				#we didn't specifically define how that should be interpreted;
				#we'll just pass it through unchanged
				$generate_backup_data_substituted .= '%';
				last;
			}
			my $c=substr($generate_backup_data,$generate_backup_data_position,1);
			if($c eq 'd'){
				$generate_backup_data_substituted .= $device;
			}
			elsif($c eq 'm'){
				$generate_backup_data_substituted .= $mount_point;
			}
			elsif($c eq 't'){
				$generate_backup_data_substituted .= $type;
			}
			elsif($c eq 'o'){
				$generate_backup_data_substituted .= $options;
			}
			elsif($c eq '%'){
				$generate_backup_data_substituted .= '%';
			}
			else{
				#we didn't specifically define how that should be interpreted;
				#we'll just pass it through unchanged
				$generate_backup_data_substituted .= "%$c";
			}
			#point past %. we substituted (or left unchanged)
			++$generate_backup_data_position;
		}
		#we check if done at top of the loop
	}
	#print "$0: executing backup_data: $generate_backup_data_substituted\n";
	my $open_backup_data="$generate_backup_data_substituted |";
	open(BACKUP_DATA,$open_backup_data) or
		die (	"$0: open(BACKUP_DATA,'$open_backup_data') failed: ",
				&diag_bang,
				", aborting:"
			)
	;

	&metatagspool(join(' ',$device,$mount_point,$type,$options));
	for(;;)	{
		if(! $spool_available) {
			&write_spool_to_media();
			next;
		}
		my $read_size=&min($spool_bs,$spool_available);
		if((my $remainder=($spool_bytes_eod%$spool_bs)!=0)){
			#if $spool_bytes_eod is not already $spool_bs aligned ...
			$remainder=$spool_bs-$remainder;
			$read_size=&min($remainder,$read_size);
		}
		$buffer_size=read BACKUP_DATA,$buffer,$read_size;
		if($buffer_size){
			&write_to_spool($buffer);
		}
		elsif(defined $buffer_size){
			#EOF
			if(!&diag_close(close(BACKUP_DATA),$open_backup_data,"$0: close(BACKUP_DATA('$open_backup_data')) error(s): ")){
				die "$0: aborting:";
			}
			#&metatagspool("end of $generate_backup_data_substituted");
			last;
		}else{
			#ERROR
			die	(	"$0: read BACKUP_DATA,\$buffer,$read_size failed: ",
					&diag_bang,
					", aborting:"
			);
		}
	}

	#pad $spool to $media_bs if needed and possible
	&pad_spool();
	#print "$0: backup_data: $generate_backup_data_substituted successfully completed\n";
	#reinitialize:
	shift @device__mount_point__type__options;
}

#############################
#							#
#	FLUSH_SPOOL_AND_INDEX	#
#							#
#############################

#flush any remaining $spool data to media and write index to media
for(;$data_bytes_in_spool||!$index_state;){
	&write_spool_to_media();
}

#####################
#					#
#	PRINT_INDEX		#
#					#
#####################

print $index;

#####################
#					#
#	POST_BACKUP		#
#					#
#####################

if("$post_backup")	{
	#print "$0: executing post_backup: $post_backup\n";
	if(&diag_system(system("$post_backup"),"$0: $post_backup error(s): ")){
		die "$0: aborting:";
	}
	#print "$0: post_backup successfully completed\n";
}
