#!/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 <kirk@kaybee.org>
#
# 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 <source_directory>\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>;
      $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 = <FILE>) {
      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 = <DISCID>;
      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.
#####################################################################################

