#!/usr/bin/perl -w use strict; use Getopt::Long; use File::Copy; ########################################################### # ImageBackup 1.8 (April 6, 2004) # (Copyright 2003, Kirk Bauer) (see license at end of file) ########################################################### # This program was written by Kirk Bauer # # It is distributed under the MIT license, which basically # says that you can do anything you want with this problem # (even commercial activities) for free, as long as you # give me credit for the program. # # For more information, please visit http://linux.kaybee.org/ ########################################################### ########################################################## # Global Configuration ########################################################## # Staging directory for burning CDs (must end in slash) my $StagingDir = "/mnt/backup/images/staging/"; # Directory to place completed ISO images my $ISODir = "/mnt/backup/images/"; # Location of Disc ID file my $DiscIDFile = "/mnt/backup/images/disc.id"; # Accepted image extentions (case insensitive) my @image_extentions = ('jpg', 'jpeg', 'gif'); # CDROM cutoff size (kbytes) my $SizeCutoff = 660000; # Specify path to AutoScrapbook to index the backup CD (if desired) my $AutoScrapbook = "/home/kirk/projects/misc/AutoScrapbook.pl --nomed --nolarge"; # Command to make the ISO image (needs to accept -V and -o options) my $MakeISO = "mkisofs -J -r"; # Maximum legal size of an ISO image my $MaxISOSize = 681574400; # Do you want to include descriptions in the data files? my $Descriptions = 1; # If you use the jhead program to store descriptions, set this my $JHEAD = "/usr/bin/jhead"; ########################################################## ########################################################## # Helper Functions ########################################################## my $Version = "1.6"; my $Debug = 0; my $Verbose = 0; my $NewDisc = 0; my $CurrSize = 0; my $NewImages = 0; my $SubDir = ''; my (%old_data, %new_data, $base_dir, %ImagesAdded); sub usage() { print STDERR "image_backup, Version $Version, by Kirk Bauer\n"; print STDERR "Usage: [-v] [--disc=xx] $0 \n"; print STDERR "Backup images to staging area: $StagingDir\n"; print STDERR " --disc=xx: Use this as the disc ID\n"; print STDERR " --force: Finalize disc regardless of size\n"; print STDERR " --subdir: Use this subdirectory under $StagingDir\n"; exit 1; } sub CheckSize() { return ($CurrSize >= $SizeCutoff); } sub get_description($$) { my ($dir, $file) = @_; unless ($Descriptions) { return ''; } # Generate potential description filename my $desc_file = "$base_dir$dir.$file"; $desc_file =~ s/\.[^.]+$//; if (-r "$desc_file") { open (DESC, "$desc_file"); my $desc = ; $desc =~ s/\n//g; close (DESC); return $desc; } elsif ($JHEAD and (-x $JHEAD)) { my $mtime = (stat("$base_dir$dir$file"))[9]; # See if the description is stored in the JPEG header $ENV{'EDITOR'} = '/bin/cat'; my $desc = `$JHEAD -ce $base_dir$dir$file 2>/dev/null`; if ($? == 0) { $desc =~ s/Comment not modified//; if ($desc =~ s/Modified: \S+//) { utime $mtime, $mtime, "$base_dir$dir$file"; } if ($desc =~ /Not JPEG/) { # No comment found... return ''; } else { $desc =~ s/AUTOSCRAPBOOK//; $desc =~ s/\n//g; return $desc; } } else { return ''; } } else { return ''; } } sub CopyDirData($) { my ($dir) = @_; mkdir "$StagingDir$SubDir$dir"; foreach (qw(.info .name .order .sizes)) { if (-f "$base_dir$dir$_") { unless (copy "$base_dir$dir$_", "$StagingDir$SubDir$dir$_") { print STDERR "Error copying $base_dir$dir$_: $!\n"; return 0; } } } return 1; } sub PrepareDirectory($) { my ($dir) = @_; my $curr = ''; my $full = $dir; while ($full =~ s=^([^/]*/)==) { $curr .= $1; CopyDirData($curr) or return 0; } CopyDirData($dir) or return 0; return 1; } ########################################################## # Data File Reading/Writing Functions ########################################################## my @data_fields = ('id', 'date', 'img', 'size', 'dir', 'desc'); # File Format (pipe-delimited): # Field0: CD ID # # Field1: Date picture was taken # Field2: Image filename # Field3: Image Size (in kilobytes) # Field4: Original Directory # Field5: Image Description sub ReadDataFile($$;$) { my ($data, $read_dir, $force_dir) = @_; open (FILE, "$read_dir.ImageBackup.data") or return 0; $Debug and print "Reading $read_dir.ImageBackup.data...\n"; while (my $line = ) { chomp($line); my @info = split(/\|/, $line, $#data_fields+1); if ($force_dir) { $info[4] = $force_dir; } my $key = "$info[4]$info[2]"; $Debug and print " $key\n"; foreach (@data_fields) { $data->{$key}->{$_} = shift @info; } } close (FILE); return 1; } sub WriteDataFile($$;$$) { my ($data, $dir, $only_dir, $append) = @_; if ($append) { $append = '>'; } else { $append = ''; } unless (open (FILE, "$append>$dir.ImageBackup.data")) { print STDERR "Could not update file $dir.ImageBackup.data: $!\n"; return 0; } $Debug and print "Writing $dir.ImageBackup.data...\n"; foreach my $entry (values %{$data}) { $Debug and print " $entry->{'img'}... "; if ($only_dir) { if ($entry->{'dir'} ne $only_dir) { $Debug and print "$entry->{'dir'} != $only_dir\n"; next; } } $Debug and print "writing.\n"; foreach (@data_fields) { print FILE $entry->{$_}; if ($_ eq $data_fields[$#data_fields]) { print FILE "\n"; } else { print FILE '|'; } } } close (FILE); return 1; } ########################################################## # Main code ########################################################## # Return >0 on error sub ProcessImage($$) { my ($dir, $img) = @_; my $key = "$base_dir$dir$img"; my $good = 0; foreach (@image_extentions) { if ($img =~ /\.\Q$_\E$/i) { $good = 1; last; } } unless ($good) { $Verbose and print STDERR "INFO: Ignoring non-image file: $key\n"; return 0; } if ($old_data{$key}) { $Verbose and print "Processing $key... [Already On CD $old_data{$key}->{'id'}]\n"; return 0; } print "Processing $key... "; if ($new_data{$key}) { print "[Already Pending]\n"; return 0; } $new_data{$key}->{'id'} = $NewDisc; $new_data{$key}->{'date'} = localtime((stat("$key"))[9]); $new_data{$key}->{'img'} = $img; $new_data{$key}->{'size'} = int ((-s "$key")/1000); $new_data{$key}->{'dir'} = "$base_dir$dir"; $new_data{$key}->{'desc'} = get_description($dir, $img); $CurrSize += $new_data{$key}->{'size'}; PrepareDirectory("$dir") or return 1; # Generate potential description filename my $desc_file = "$base_dir$dir.$img"; $desc_file =~ s/\.[^.]+$//; if (-f $desc_file) { unless (copy "$desc_file", "$StagingDir$SubDir$dir") { print STDERR "ERROR copying file $desc_file: $!\n"; return 1; } } system("mkdir -p $StagingDir$SubDir$dir.thumbs"); if (-f "$base_dir$dir.thumbs/$img") { copy "$base_dir$dir.thumbs/$img", "$StagingDir$SubDir$dir.thumbs/" } unless (copy "$key", "$StagingDir$SubDir$dir") { print STDERR "ERROR copying file $key: $!\n"; return 1; } $ImagesAdded{"$base_dir$dir"}++; $NewImages++; print "[Added to Backup]\n"; return 0; } # Return >0 on error sub BackupDirectory { my ($dir) = @_; my $ret = 0; my @subdirs; # Read current backup data, if any unless (ReadDataFile(\%old_data, "$base_dir$dir", "$base_dir$dir")) { $Verbose and print STDERR "INFO: No backup data exists for $base_dir$dir\n"; } unless (opendir (DIR, "$base_dir$dir")) { print STDERR "Warning: could not open directory $base_dir$dir: $!\n"; return 1; } $Verbose and print STDERR "INFO: Processing directory: $base_dir$dir\n"; while (my $entry = readdir(DIR)) { next if ($entry =~ /^\./); $Debug and print "Processing entry $entry...\n"; if (-d "$base_dir$dir$entry") { push @subdirs, "$dir$entry/"; } elsif (-f "$base_dir$dir$entry") { $ret += ProcessImage($dir, $entry); return $ret if CheckSize(); } else { $Verbose and print STDERR "INFO: Ignoring $base_dir$dir$entry (not a regular file)\n"; } } close (DIR); foreach (@subdirs) { $ret += BackupDirectory($_); return $ret if CheckSize(); } return $ret; } my $Help = 0; my $Force = 0; GetOptions ( 'd|debug' => \$Debug, 'v|verbose' => \$Verbose, 'disc=i' => \$NewDisc, 'subdir=s' => \$SubDir, 'force' => \$Force, 'h|help' => \$Help ) or Usage(); $Help and Usage(); $base_dir = $ARGV[0]; unless (($base_dir) and (-d $base_dir)) { die "Must specify a valid image source dircetory!\n"; } # Make sure it does not end in a trailing slash $base_dir =~ s/\/$//; ReadDataFile(\%new_data, "$StagingDir"); if ($Verbose and (not keys %new_data)) { print STDERR "INFO: Starting with empty staging directory ($StagingDir)\n"; } # Determine the Disc ID unless ($NewDisc) { if (open (DISCID, $DiscIDFile)) { $NewDisc = ; chomp($NewDisc); close(DISCID); } else { $NewDisc = 1; } } # Make sure we are using the same Disc ID for all pending entries # Also total up the sizes so far print "Building Backup CD, Disc $NewDisc\n"; foreach (values %new_data) { $_->{'id'} = $NewDisc; $CurrSize += $_->{'size'}; $ImagesAdded{$_->{'dir'}}++; } if ($SubDir) { $SubDir =~ s/^\///; unless ($SubDir =~ /\/$/) { $SubDir .= '/'; } } unless (-d $StagingDir) { system("mkdir -p $StagingDir$SubDir"); } # Start with the base directory my $ret = BackupDirectory('/'); unless ($ret == 0) { print STDERR "There were errors.\n"; return $ret; } if ($NewImages > 0) { # Update data file in staging directory WriteDataFile(\%new_data, $StagingDir); print "\n$NewImages new image(s) added.\n\n"; } # Now, see if we need to make the disc. if (CheckSize()) { print "\nDisc is full ($CurrSize KB in images)\n"; } else { if ($Force) { print "\nOnly $CurrSize KB in images, but forcing disc on request.\n"; } else { print "\nDone (not enough data for full disc: $CurrSize/$SizeCutoff)\n"; exit -1; } } # Now finalize the disc if ($AutoScrapbook) { print "\nRunning AutoScrapbook...\n\n"; system("$AutoScrapbook '$StagingDir'"); } my $ISOOut = "${ISODir}image_backup_disc_$NewDisc.iso"; print "\nGenerating ISO Image...\n\n"; unless (system("$MakeISO -V 'ImageBackup_Disc$NewDisc' -o '$ISOOut' '$StagingDir'") == 0) { print STDERR "\nCreation of ISO image failed!\n\n"; exit 1; } my $iso_size = (-s $ISOOut); unless ($iso_size < $MaxISOSize) { print STDERR "\nISO image too big ($iso_size)! (check SizeCutoff value)\n\n"; exit 1; } # Finalize the process foreach (keys %ImagesAdded) { $Verbose and print "INFO: $ImagesAdded{$_} new images processed in $_\n"; WriteDataFile(\%new_data, $_, $_); WriteDataFile(\%old_data, $_, $_, 1); } WriteDataFile(\%new_data, "$ISODir$NewDisc"); WriteDataFile(\%old_data, "$ISODir$NewDisc", '', 1); print "\nGenerated ISO for disc $NewDisc ($iso_size/$MaxISOSize)\n\n"; if (open (DISCID, ">$DiscIDFile")) { $NewDisc++; print DISCID "$NewDisc\n"; close(DISCID); } else { print STDERR "Could not store next DiscID in $DiscIDFile\n"; exit 1; } system("rm -rf $StagingDir"); print "\nISO Image Ready: $ISOOut\n"; exit 0; ##################################################################################### # Copyright (c) 2003 Kirk Bauer # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies # of the Software, and to permit persons to whom the Software is furnished to do # so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. #####################################################################################