Keep a Directory Tree Update

Project #1

As The title suggest, you will create a program that is used to maintain an up-to-date copy (backup?) of files you are working on. Any files in the work directory tree that are modified or new will be copied to the destination tree replacing any files already there.

Note: The user must delete files from both trees.

A Perl version of my maintenance/backup program is below. My Perl code may be used as a resource/template/design/... for your program.

Project #2

Create a program that automatically deletes files in the destination (backup) directory tree that are no longer in the work directory tree.

My Perl Program

#!/bin/perl -w
#===================================================================
# keep a source directory tree up to date
#
# 1. If there are files in the source tree that are newer that
#    the destination tree, replace the destination tree file.
# 2. If a file in the source tree does not exist in the destination
#    tree copy it to the destination tree.
# 3. A directory in the skip list (and all of its sub-directories)
#    will not be processed (skipped).
#
# Note: This script will not create directories.
#
#===================================================================


use strict;
use FileHandle;
use DirHandle;
use File::Basename;
use File::Copy;


#-------------------------------------------------------------------
# global variables
#
# DELBUG         debug flag
# DIR_P_COUNT    count - directories processed
# DROOTDIR       destination tree root directory
# FAILURE        returned failure code
# FILE_A_COUNT   count - files added to destination tree
# FILE_P_COUNT   count - source files processed
# FILE_U_COUNT   count - files updated in destination tree
# FORCEMODE      force the copy of all files (don't test for latest)
# RDIRLIST       a list of all of the directories in the source tree
#                (relative to the source tree root directory)
# SKIPDIRS       a list of directories to skip
# SROOTDIR       source tree root directories
# STARTTIME      script start time
# SUCCESS        returned success code
# TESTMODE       test mode flag
# VERBOSE        verbose flag
#
#-------------------------------------------------------------------

# -- work computer
# -- copy files from the flash drive to C drive (desktop)
#my $SROOTDIR      = 'U:/Secure-Web-Pages-Class';    # flash drive
#my $DROOTDIR      = 'C:/Secure_Web_Pages-Class';    # C drive

# -- work computer
# -- copy files from C drive (desktop) to flash drive
#my $SROOTDIR      = 'C:/Secure-Web-Pages-Class';    # C drive
#my $DROOTDIR      = 'U:/Secure-Web-Pages-Class';    # flash drive

# -- home computer
# -- copy files from the flash drive to the C drive
#my $SROOTDIR      = 'L:/Secure-Web-Pages-Class';
#my $DROOTDIR      = 'C:/Secure-Web-Pages-Class';

# -- home computer
# -- copy files from the C drive to the flash drive
#my $SROOTDIR      = 'C:/Secure-Web-Pages-Class';
#my $DROOTDIR      = 'L:/Secure-Web-Pages-Class';

# -- copy files from work computer to AFS space
#my $SROOTDIR      = 'C:/Secure-Web-Pages-Class';     # C drive
#my $DROOTDIR      = 'T:/www/Secure-Web-Pages-Class'; # AFS

# -- copy files from flash drive to AFS space
#my $SROOTDIR      = 'U:/Secure-Web-Pages-Class';     # flash drive
#my $DROOTDIR      = 'T:/www/Secure-Web-Pages-Class'; # AFS

# directories to be skipped (by name)

my %SKIPDIRS       = ('x',                    1,
                      'java',                 1,
                      'X',                    1,
                      'instructor',           1,
                      'zz',                   1);

# -- files to be skipped (by name)

my %SKIPFILENAMES  = ('keep_dir_tree_up_to_date.pl', 1,
                      'Thumbs.db',                   1);

# files to be skipped (by regexp pattern)
# (including expression delimiters)

my @SKIPFILEPATTERNS = ('\.vsd$');
#my @SKIPFILEPATTERNS = ();


my $DEBUG          = 0;
my $DIR_P_COUNT    = 0;
my $FAILURE        = 0;
my $FILE_A_COUNT   = 0;
my $FILE_P_COUNT   = 0;
my $FILE_U_COUNT   = 0;
my $FORCEMODE      = 0;
my $MAKEDIRS       = 0;
my @RDIRLIST       = ('');       # initial value '' so that the root
                                 # source directory will be processed
my $STARTTIME      = time;
my $SUCCESS        = 1;
my $TESTMODE       = 1;
my $ADDFILES       = 0;
my $VERBOSE        = 1;


#===================================================================
#===================================================================
# main
#===================================================================
#===================================================================


#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# make sure the root directories do not end with a / or \ character
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

$SROOTDIR =~ s/\/$//;
$DROOTDIR =~ s/\/$//;
$SROOTDIR =~ s/\\$//;
$DROOTDIR =~ s/\\$//;


#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# process command line arguments
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

if (ProcessCommandLineArguments() != $SUCCESS) { exit 1; };


#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create a list of all of the source tree directories,
# skipping the directories in the skip list (and their sub-directories)
# the list values are the relative to the source root directory
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

if ($VERBOSE) { print "\nCreating source tree directory list\n"; }

if (CreateSourceDirList($SROOTDIR) != $SUCCESS) { exit 1; }

if ($DEBUG > 1)
{
   foreach (@RDIRLIST)
   {
     print "Relative source directory ($_)\n";
   }
}


#-------------------------------------------------------------------
# verity that there is a destination directory for every
# source directory
#-------------------------------------------------------------------

if ($VERBOSE) { print "\nVerifying source directories have " .
                      "a matching destination directory\n"; }

if (VerifyDestDirsExist() != $SUCCESS) { exit 1; }


#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update each destination directory with files from its
# corresponding source directory
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

if ($VERBOSE) { print "\nUpdating destination directories\n"; }

my $sdir;                        # source directory

foreach $sdir (@RDIRLIST)
{
   if (UpdateDestDir($sdir) != $SUCCESS)
   {
      exit 1;
   }
}


#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# display processing statistics
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

print "\n";
print "---------------------------------------------------\n";
print "Source directory      : $SROOTDIR\n";
print "Destination directory : $DROOTDIR\n";
print "Test mode             : $TESTMODE\n";
print "Make dirs             : $MAKEDIRS\n";
print "Directories processed : $DIR_P_COUNT\n";
print "Files processed       : $FILE_P_COUNT\n";
print "Files added           : $FILE_A_COUNT\n";
print "Files updated         : $FILE_U_COUNT\n";
print "----------------------------------------------------\n";
print "\n";

exit 0;


#===================================================================
#===================================================================
# subroutines
#===================================================================
#===================================================================


#-------------------------------------------------------------------
# create a list of all of the source tree directories,
# skipping the directories in the skip list 
# (and their sub-directories)
#-------------------------------------------------------------------

sub CreateSourceDirList
{
   my $sdir = $_[0];             # source directory

   my $dh = new DirHandle;        # directory handle
   my $e;                         # list entry
   my $f;                         # file name
   my @fl;                        # list of file names
   my @fs;                        # sorted list of file names

   if ($DEBUG) { print "CreateSourceDirList($sdir)\n"; }

   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # get a list of all of the files in the source directory
   # (this includes other directories)
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   if (! opendir($dh,$sdir))
   {
      print "\nCan't open $sdir: $!\n\n";
      return $FAILURE;
   }

   @fl = readdir $dh;

   closedir $dh;

   # sort the list of file names

   @fs = sort @fl;

   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # process each sub-directory in the current directory
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   foreach $e (@fs)
   {
      if ($e eq '.')     { next; };
      if ($e eq '..')    { next; };

      $f = $sdir . '/' . $e;

      if (-d $f)
      {
         if (defined $SKIPDIRS{$e})
         {
            if ($VERBOSE) { print "\nSkipping directory $e\n"; }
            next;
         }

         $f =~ s/^$SROOTDIR//;   # remove the root dir from complete
                                 # path and (dir) file name leaving a
                                 # relative path and (dir) file name

         push @RDIRLIST,$f;
      }
   }

   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # now process every sub-directory
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   foreach $e (@fs)
   {
      if ($e eq '.')   { next; };
      if ($e eq '..')  { next; };

      $f = $sdir . '/' . $e;

      if (-d $f)
      {
         if (defined $SKIPDIRS{$e}) { next; }

         if (CreateSourceDirList($f) != $SUCCESS)
         {
	       return $FAILURE;
         }
      }
   }

   return $SUCCESS;
}


#-------------------------------------------------------------------
# verity that there is a destination directory for every
# source directory
#-------------------------------------------------------------------

sub VerifyDestDirsExist
{
   if ($DEBUG) { print "VerifyDestDirsExist()\n"; }

   my $ddir;                     # destination directory
   my $rdir;                     # relative directory
   my $sdir;                     # source directory

   foreach $rdir (@RDIRLIST)
   {
      $sdir = $SROOTDIR . $rdir;
      $ddir = $DROOTDIR . $rdir;

      if (! -e $ddir)
      {
         if ($MAKEDIRS)
         {
	       if (mkdir($ddir))
            {
               print "Directory created: $ddir\n";
               next;
            }

            print "\nError: conable to create directory\n";
            print "Dir: $ddir\n";
            print "$!\n";
            return $FAILURE;
         }

         print "\nError: source directory does not " .
               "have a matching destination directory\n";
         print "       Source:      $sdir\n";
         print "       Destination: $ddir\n";
         return $FAILURE;
      }
   }

   return $SUCCESS;
}


#-------------------------------------------------------------------
# update a destination directory
#-------------------------------------------------------------------

sub UpdateDestDir
{
   my $rdir = $_[0];             # relative source directory

   if ($DEBUG) { print "UpdateDestDir($rdir)\n"; }

   $DIR_P_COUNT++;


   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # define the source and destination directories from the
   # relative source directory
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   my $sdir = $SROOTDIR . $rdir; # source directory
   my $ddir = $DROOTDIR . $rdir; # destination directory

   if ($VERBOSE)
   {
      print "Processing: $sdir\n";
   }

   if ($VERBOSE == 3)
   {
      print "Updating destination: $ddir\n";
      print "         source     : $sdir\n";
   }


   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # get a list of all of the files in the source directory
   # (this includes other directories)
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


   my $dh = new DirHandle;        # directory handle
   my $df;                        # destination file name and path
   my $e;                         # list entry
   my $sf;                        # source file name and path
   my @fl;                        # list of file names
   my @fs;                        # sorted list of file names

   if (! opendir($dh,$sdir))
   {
      print "\nCan't open $sdir: $!\n\n";
      return $FAILURE;
   }

   @fl = readdir $dh;

   closedir $dh;

   # sort the list of file names

   @fs = sort @fl;

   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   # process each file in the source directory
   # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

   my $supdate;                  # source file update date
   my $dupdate;                  # destination file update date
   my $t;                        # delta time value
   my $p;                        # pattern

   foreach $e (@fs)
   {
      if ($e eq '.')     { next; };
      if ($e eq '..')    { next; };


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # special case for Perl editor files
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      if ($e =~ /^perl5db\.pl$/) { next; }
      if ($e =~ /\.pltemp\.pl$/) { next; }


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # create source and destination file (path and name)
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      $sf = $sdir . '/' . $e;
      $df = $ddir . '/' . $e;

      if (-d $sf) { next; }      # skip directories

      $FILE_P_COUNT++;


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # skip the file (by name) ?
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      if (exists $SKIPFILENAMES{$e})
      {
         if ($VERBOSE > 1) { print "         S: $sf\n"; }

         next;
      }


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # skip the file by regexp pattern match ?
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      if (SkipFileByPattern($e)) { next; }


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # if in force mode, bypass test file update dates and
      # copy all files to the destination directory
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      if ($FORCEMODE)
      {
         if (! $TESTMODE)
         {
            if (copy($sf,$df) != 1)
            {
               print "\nError: copying file, $!\n";
               print "       Src: $sf\n";
               print "       Dst: $df\n";
               return $FAILURE;
            }
         }

         $FILE_A_COUNT++;

         if ($VERBOSE) { print "         A: $sf\n"; }

	    next;
      }


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # if the file does not exist in the destination directory,
      # copy the file to the destination directory
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      if (! -e $df)
      {
       if (! $TESTMODE)
         {
            if (copy($sf,$df) != 1)
            {
               print "\nError: copying file, $!\n";
               print "       Src: $sf\n";
               print "       Dst: $df\n";
               return $FAILURE;
            }
         }

         $FILE_A_COUNT++;

         if ($VERBOSE) { print "         A: $sf\n"; }

	    next;
      }


      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      # if the file exist in the destination directory,
      # and the source file is newer that the destination file,
      # copy the file to the destination directory
      #
      # Note: There is a 2 second fudge factor built into the code.
      #       This will hopefully mask differences between the system
      #       clocks.
      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

      $supdate = (stat($sf))[9];
      $dupdate = (stat($df))[9];

      $t  = abs($dupdate - $supdate);

      if ($t <= 2) { next; }

      if ($ supdate > $dupdate)
      {
        if (! $TESTMODE)
         {
            if (copy($sf,$df) != 1)
            {
		     print "\nError: copying file, $!\n";
               print "       Src: $sf\n";
               print "       Dst: $df\n";
               print "       Src update: $supdate\n";
               print "       Dst update: $dupdate\n";

               return $FAILURE;
            }
         }

         $FILE_U_COUNT++;

         if ($VERBOSE) { print "         U: $sf\n"; }

         if ($VERBOSE == 3)
         {
            print "            Src: $supdate\n";
            print "            Dst: $dupdate\n";
            print "            Del: $t (sec)\n";
         }
      }
   }

   return $SUCCESS;
}


#-------------------------------------------------------------------
# match filename to skip regexp patterns
#-------------------------------------------------------------------

sub SkipFileByPattern
{
   my $f = $_[0];                 # filename

   my $p;                         # filename pattern

   foreach $p (@SKIPFILEPATTERNS)
   {
      if ($f =~ /$p/) { return 1; }

   }

   return 0;
}


#-------------------------------------------------------------------
# process command line arguments
#-------------------------------------------------------------------

sub ProcessCommandLineArguments
{
   my $e;                         # command line argument
   my $ee;                        # command line argument (uppercase)

   for $e (@ARGV)
   {
      $ee = $e;
      $ee =~ tr/a-z/A-Z/;

      if ($ee eq "DEBUG")      { $DEBUG     = 1; next; }
      if ($ee eq "DEBUG1")     { $DEBUG     = 1; next; }
      if ($ee eq "DEBUG2")     { $DEBUG     = 2; next; }
      if ($ee eq "DEBUG3")     { $DEBUG     = 3; next; }
      if ($ee eq "DEBUG4")     { $DEBUG     = 4; next; }
      if ($ee eq "FORCE")      { $FORCEMODE = 1; next; }
      if ($ee eq "FORCEMODE")  { $FORCEMODE = 1; next; }
      if ($ee eq "VERBOSE")    { $VERBOSE   = 1; next; }
      if ($ee eq "VERBOSE1")   { $VERBOSE   = 1; next; }
      if ($ee eq "VERBOSE2")   { $VERBOSE   = 2; next; }
      if ($ee eq "VERBOSE3")   { $VERBOSE   = 3; next; }
      if ($ee eq "COPY")       { $TESTMODE  = 0; next; }
      if ($ee eq "UPDATE")     { $TESTMODE  = 0; next; }
      if ($ee eq "NOTEST")     { $TESTMODE  = 0; next; }
   }

   return $SUCCESS;
}