#!/usr/bin/perl # This program backs up the files listed on the command line to DVD. # # Copyright (c) 2008-2011 by Reid Priedhorsky, . # # This script is distributed under the terms of the GNU General Public # License; see http://www.gnu.org/licenses/gpl.txt for more information. # Bugs: # # 1. Files *must* be given with absolute paths. use strict; use warnings; no warnings 'uninitialized'; use Cwd; use File::Basename; use File::Spec; use Log::Log4perl qw(:easy); undef $/; ### Config ### # Verbosity level (e.g. $INFO, $DEBUG) # Note that layout is documented in Log::Log4perl::Layout::PatternLayout Log::Log4perl->easy_init( { level => $INFO, layout => "%d %p %m%n" } ); # DVD burner device my $DVD_DEVICE = '/dev/sr0'; # DVD mount point my $DVD_MOUNT_POINT = '/mnt/cdrom'; # DVD burn speed (99 for maximum) my $DVD_SPEED = 4; # Temporary directory to use my $TMPDIR = '/var/tmp'; # If the burned DVD is smaller than the source files by more than this number # of kilobytes, that's an error. my $DVD_SIZE_DIFF_THRESH = 8192; # PAR2 block size (bytes) my $PAR2_BLOCK_SIZE = 2 * 1024 * 1024; # PAR2 redundancy percentage my $PAR2_PERCENT = 15; # Location of source code to include with the backup my $BURNED_SOURCE_DIR = "/usr/local/src/backup-src"; my $BURNED_SOURCE = "/usr/local/src/backup-src.tar.gz"; ### Globals ### # Staging directory. my $staging_dir = "$TMPDIR/dvd-staging.$$"; # Filesystem image my $udf_file = "$TMPDIR/dvd-staging.$$.udf"; # CWD when invoked, so we can restore. my $old_cwd = cwd(); # List of files to be backed up. This list is assumed to be relatively short; # i.e., it's the caller responsibility to create archive files. my @files = @ARGV; ### Main ### { INFO "Staging directory is \"$staging_dir\"."; do_cmd("mkdir $staging_dir"); chdir($staging_dir) or fatal("Can't chdir to $staging_dir: $!"); INFO "Preparing extras to include."; do_cmd("update-src-deb par2 par2cmdline $BURNED_SOURCE_DIR"); # NOTE: We don't actually call mksquashfs ourselves, but our caller likely # does, and this is small. do_cmd("update-src-deb squashfs-tools squashfs-tools $BURNED_SOURCE_DIR"); do_cmd("cp $BURNED_SOURCE ."); do_cmd("touch `date '+CREATED_%Y-%m-%d_%H-%M'`"); foreach my $file (@files) { do_cmd("ln -s $file ."); # WARNING: Creating UDF must follow symlinks! } INFO "Creating PAR2 redundancy."; do_cmd("par2 create -q -s$PAR2_BLOCK_SIZE -r$PAR2_PERCENT redundancy.par2 * > /dev/null"); INFO "Calculating md5sums."; do_cmd("md5sum * > md5sums"); INFO "Making UDF filesystem."; do_cmd("mkudf-simple . $udf_file"); INFO "Burning DVD."; do_cmd("growisofs -use-the-force-luke=notray -quiet -dvd-compat -speed=$DVD_SPEED -Z $DVD_DEVICE=$udf_file"); # wodim doesn't work with Lite-On Blu-Ray drive? #do_cmd("burn -D $DVD_DEVICE $udf_file"); INFO "Preparing to mount burned DVD."; # All of this garbage is to work around bugs & race conditions in # post-burn growisofs and mounting DVDs. do_cmd("sleep 10"); do_cmd("eject $DVD_DEVICE"); do_cmd("sleep 10"); do_cmd("eject -t $DVD_DEVICE"); do_cmd("sleep 30"); INFO "Verifying md5sums."; verify_md5sums(); INFO "Cleaning up."; chdir($old_cwd) or fatal("Can't chdir back to \"$old_cwd\": $!"); do_cmd("rm -Rf $staging_dir"); do_cmd("rm $udf_file"); INFO "Done backing up to DVD."; } ### Subroutines ### # Verify the md5sums on the burned DVD. sub verify_md5sums { my ($size_expected, $size_actual, $md5sum_output); my ($cmd); do_cmd("mount $DVD_MOUNT_POINT"); $size_expected = `du --summarize --dereference | cut -f1`; $size_expected =~ s/\n$//; $size_actual = `df | grep $DVD_MOUNT_POINT`; $size_actual =~ /^.+?\s+(\d+)/ or fatal("Can't match df output"); $size_actual = $1; DEBUG "Backup size: $size_expected kB on disk, $size_actual kB on DVD"; if ($size_expected - $size_actual > $DVD_SIZE_DIFF_THRESH) { fatal("Expected $size_expected kB on DVD, but found $size_actual (difference exceeds $DVD_SIZE_DIFF_THRESH kB)"); } do_cmd("cd $DVD_MOUNT_POINT && md5sum --check md5sums"); do_cmd("umount $DVD_MOUNT_POINT"); } sub do_cmd { my ($cmd) = @_; DEBUG '$ ' . $cmd; (not system($cmd)) or fatal("Command failed: \"$cmd\""); } sub fatal { # FIXME: should this go to stderr too? FATAL $_[0]; #print STDERR $0, ': ', $_[0], "\n"; exit(1); }