Simple Windows XP Backup Script

30 November -0001
Jan 3, 2006
by: Justin Klein Keane

The following was a quick Perl based backup script I could use to back up my working documents and projects to make sure I didn't lose anything if I experienced a random crash. I chose to use Perl because I wanted something a little more robust than just a straight copy of items (such as you could do with a batch file). I don't like the windows native backup feature because I've had bad experiences with the backups getting corrupted or being difficult to restore.

I'm copying from my local D:\ drive to a remote SMB mounted U:\ drive. The script only backs up one way, and it won't remove items that have been deleted (I figure in the short term this could be a bonus but it may become a headache over time). The script will recursively drop through directories and copy any files to backup that either have changed or do not appear on the backup drive.

There is also a primitive exclusion list. I have some large files on my local drive in the "My Music" folder that I don't want to bother backing up to the remote directory and some random empty "New Folder" folders that I used for testing. The exclusion file will make sure these directories are ignored.

The other trick with this script was getting it to run without interrupting my work. I decided to use the windows system task scheduler, but created a custom batch file to start the process so it would be minimized. The batch file is pretty simple, I just used the "/min" option in the start command to avoid the hassle of a window popping up and stealing focus. The batch file is simply:

@ECHO OFF
start /min perl backuper.pl

The perl file is much larger, and logs its progress to a file called backuper.log that sits in the same directory as the perl script. I'm sure I could add quite a few more features (like filename wildcards for exclusion so I didn't back up MP3 files for instance) but this bare bones works well enough for now.

#! C:\Perl\bin
#
# Purpose:  This Perl based backup client designed to examine
#           folder contents and update them on the appropriate remote
#           backup system.
#
# Author:  Justin C. Klein Keane (jkeane@madirish.net)
#

use strict;
use File::Copy;
use File::Compare;

#log file options
my $logFile = 'backuper.log';
my $outputLog = checkLogFile($logFile) or die("Error opening log file -- $@");
my @exclusion_list = ('.', '..', 'My Music', 'New Folder');

my %directories = ( 'D:\Documents and Settings\jukeane\My Documents', 'U:\mydocs',
                    'D:\Documents and Settings\jukeane\Desktop\bin', 'U:\bin');

while ( (my $ldir, my $rdir) = each(%directories) ) {
  chdir $ldir or fatal_error("Couldn't change to $ldir");
  if (! -d $rdir) {
    fatal_error("Couldn't open the remote directory $rdir");
  }
  
  my $remoteFileName;
  my $tmpDirName;
  my $tmpRemDirName;
  
  read_copy($ldir, $rdir);
}


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

sub read_copy {
  my $local_dir = $_[0] or fatal_error("No local directory specified. $!");
  my $remote_dir = $_[1] or fatal_error("No remote directory specified. $!");
  
  if (! -d $remote_dir ) {
    mkdir($remote_dir) or fatal_error("Failed to create $remote_dir. $!");
  }

  opendir(DIRHANDLE, $local_dir) || fatal_error("Cannot opendir $local_dir. $!");
    foreach my $name (sort readdir(DIRHANDLE)) {
      if (! -d $name ) {
          my $remoteFileName = $remote_dir . '\\' . $name ;
          my $localFileName =  $local_dir . '\\' . $name;
          
          if (! -d $remote_dir) {
              fatal_error("$remoteFileName doesn't seem to exist? $!");
          }
          
          if (! -d $localFileName) {
              if ( checkBackup($localFileName, $remoteFileName) ) {
                copy("$localFileName", "$remoteFileName") or die qq(Cannot copy "$localFileName" to "$remoteFileName": $!) ;
                logIt("Backed up $localFileName to $remoteFileName");
              }
          }
          elsif ( -d $localFileName && (! check_exclusion($name)) ) {
                  my $tmpDirName = $local_dir . '\\' . $name;
                  my $tmpRemDirName = $remote_dir . '\\' . $name;
                  read_copy($tmpDirName, $tmpRemDirName);
          }
      }
      else {
          if (! check_exclusion($name) ) {
              my $dir1 = $local_dir . '\\' . $name;
              my $dir2 = $remote_dir . '\\' . $name;
              read_copy($dir1, $dir2);
          }
      }
      
    }
  closedir(DIRHANDLE);
}

sub checkBackup {
  my $file1 = $_[0] or fatal_error("Missing first file in checkBackup(). $!");
  my $file2 = $_[1] or fatal_error("Missing second file in checkBackup(). $!");
  #check to see if we should back these files up
  if (compare("$file1","$file2") == 0) {
      return 0;
  }
  else {
      return 1;
  }
}


sub check_exclusion {
  #checks the exclusion list for stuff not to copy
  my $file = $_[0];
  foreach my $item (@exclusion_list) {
    if ($file eq $item) {
      return 1;
    }
  }
  return 0;
}

sub fatal_error {
    #die and log the error
    my $error_message = $_[0];
    $error_message = "Fatal Error:  " . $error_message;
    logIt($error_message);
    die ($error_message . "\n");
}

#####################
#     Logging       #
#####################

sub logIt {
    #logs errors.
  my $message = $_[0];
  my @fullTime = localtime(time());
  my $time = ($fullTime[5] + 1900) . "/" . (addZero($fullTime[4] + 1)) . "/";
  $time .= addZero($fullTime[3]). " " . addZero($fullTime[2]) . ":";
  $time .= addZero($fullTime[1]) . ":" . addZero($fullTime[0]);
  my $logMessage = $time . "> $message \n";
    print $outputLog $logMessage;
}

sub checkLogFile {
    #checks the output files to make sure they're valid
    my $file = $_[0];
    my $openFile;
  my $status = (stat($file))[7];
  if (! $status) { $status = 0;}
    if ( $status != 0) {
        open($openFile, ">>" . $file) or die("Couldn't open log file for appending " . $file);
        return $openFile;
    }
    else {
        open($openFile, ">" . $file) or die("Couldn't create new log file " . $file);
        return $openFile;
  }
}

sub addZero {
  #formats output to double digit
  if (length($_[0]) < 2) {
    return "0" . $_[0];
  }
  else {
    return $_[0];
  }
}