# This my first posting in this section so be kind (well, unless it's for my own good ;-) ). # The program was intended to synchronise MP3 files on my hard disk with # those on my portable MP3 jukebox thingy # (Archos Jukebox 6000). # I rip the CDs onto my local hard disk then copy them across # to the player (which appears as a USB removable hard disk). # I just recently noticed the a module which does a lot of the same things, # as well as lots of other things. That makes this program yet another # re-invented wheel but I enjoyed writing it. (NOTE: I can't # remember what the module was called though!) #!perl -w # MP3sync - synchronises MP3 files/directories between # two directories # Usage: # MP3sync [configfile] # Where: # configfile is a configuration file (as described below), # the program defaults to reading mp3sync.cfg # Configuration file format: # Each line begins with a setting name, followed by '=' and # the value to be assigned to the setting. Case is significant. # # Settings: [default in square brackets] # Source - directory with (possibly new) MP3s # [C:/My Documents/My Music/] # Dest - directory where MP3s should be copied (if necessary) # [G:/] # (each of these specifies a path which should be the root directory # of the tree containing the files and directories to be synchronised) # important stuff use strict; use diagnostics; # we want to calculate checksums for files use Digest::MD5; # for reading directories use IO::Dir; # for copying files use File::Copy; # Default configuration file name my $configFile = "mp3sync.cfg"; # Hash to hold configuration settings my %config = ( Source => 'C:/My Document/My music/', Dest => 'G:/' ); # Sub: ReadConfig - reads in a configuration file # Usage: # $status = ReadConfig($configFile, \%configHash); # Where: # $status will be true on successfull reading of the file. # (error messages will be output to STDERR) # $configFile is the name of the file to read # \%ConfigHash is a reference to a hash holding configuration # settings sub ReadConfig { my $filename = shift; my $configHash = shift; # check the parameters - filename first if (!defined($filename)) { warn("You must supply a filename to ReadConfig"); return undef; }; # has the user specified a hash? if (!defined($configHash)) { warn("You must supply a configuration hash reference to ReadConfig"); return undef; }; # is it really a hash ref? if (ref($configHash) ne "HASH") { warn("The second parameter to ReadConfig must be a hash reference"); return undef; }; # open the file - warn that we're using the defaults if it can't be opened unless (open(CONFFILE, '<', $filename)) { warn("Couldn't open config file $filename: $!\n"); warn("Using defaults"); return 1; }; # now read each line my $line; while (defined($line = )) { # if it's a valid line store it in the hash if ($line =~ /(\w+)\=(.+)/) { $configHash->{$1} = $2; }; # ignore invalid lines silently }; # close the file unless (close(CONFFILE)) { WARN("Couldn't close config file $filename: $!\n"); return undef; }; # success! return 1; }; # Sub: PopulateTree # Usage: # $status = PopulateTree($path, \%hash); # Where: # $status will be true if the function succeeded # $path is the directory to examine (terminate in '/') # \%hash is a reference to a hash to be filled with directory # tree information. Each item in the hash will be a hash # containing the following keys: # Type (files and dirs) - 'File' or 'Dir' # Contents (dirs only) - an array containing the directory # contents, in this form # Digest (files only) - the MD5 digest of the file sub PopulateTree { # get our parameters my $path = shift; if (!defined($path)) { warn("PopulateTree needs a path"); return undef; }; my $hashref = shift; if (!defined($hashref)) { warn("PopulateTree needs a hash reference"); return undef; }; # check that $hashref really is a hash reference if (ref($hashref) ne 'HASH') { warn("PopulateTree needs a HASH reference"); print ref($hashref), "\n"; return undef; }; print "Looking in $path...\n"; # create a Digest::MD5 object my $digest = new Digest::MD5; # read the directory my $dir = new IO::Dir; my @dirContents; unless ($dir->open($path)) { warn("Couldn't open directory $path: $!\n"); return undef; }; unless (@dirContents = $dir->read()) { warn("Couldn't read directory $path: $!\n"); return undef; }; # now look at each item and decide what to do with it ITEM: foreach my $item (@dirContents) { if (($item eq '.') || ($item eq '..')) { next ITEM; }; if (-d $path.$item) { # it's a directory, create an item entry for it $hashref->{$item} = { Type => 'Dir', Contents => {} }; # get its contents unless (PopulateTree($path.$item."/", $hashref->{$item}->{Contents})) { warn("PopulateTree failed"); return undef; }; } elsif (-f _) { # it's an ordinary file, pass it to Digest::MD5 unless (open(*FILE, '<', $path.$item)) { warn("Couldn't open $path$item: $!\n"); return undef; }; $digest->addfile(*FILE); my $fileDigest = $digest->hexdigest(); unless (close(*FILE)) { warn("Couldn't close $path$item: $!\n"); return undef; }; # create an item entry for it $hashref->{$item} = { Type => 'File', Digest => $fileDigest }; }; # ignore other item types }; # success return 1; }; # Sub: CompareTrees # Usage: # ($status, @doList) = # CompareTrees($sourcePath,$destPath,\%sourceTree, \%destTree); # Where: # $status will be true if the function succeeded. # @doList will contain a list of things to do to make the two trees # identical, or be undefined on failure. Each item in the list will be # a hash containing two keys: 'Action' and 'Detail'. 'Action' will be # either 'Copy' or 'Create', 'Detail' will be the full path to create (for # 'Create') or a two item array (for 'Copy'). The first item is the source # file, the second is the file to copy it to both have full paths. # $sourcePath is the full path to the source tree root # $destPath is the full path to the destination tree root # \%sourceTree is a reference to a tree as returned by PopulateTree. # \%destTree is a reference to a tree as returned by PopulateTree. # Note: # The destTree hash is modified by this function, keep a copy of it if # you want the original data. sub CompareTrees { # get the paths my $sourcePath = shift; unless ($sourcePath) { warn("You must provide a source path to CompareTrees"); return undef; }; my $destPath = shift; unless ($destPath) { warn("You must provide a destination path to CompareTrees"); return undef; }; # get our two tree refs my $sourceRef = shift; unless ($sourceRef) { warn("You must provide a source tree reference to CompareTrees"); return undef; }; my $destRef = shift; unless ($destRef) { warn("You must provide a destination tree reference to CompareTrees"); return undef; }; # check that they are hash refs if ((ref($sourceRef) ne 'HASH') || (ref($destRef) ne 'HASH')) { warn("CompareTrees takes two hash references"); return undef; }; # an array to return our result in my @retArray = (); my $item; # look at each item contained in the source tree CMPITEM: foreach $item (keys(%{$sourceRef})) { # is there a corresponding item in the destination tree? if (defined($destRef->{$item})) { # if it's a directory we know it exists, if it's a file we want to # check it's identical if ($destRef->{$item}->{Type} eq 'File') { # compare the MD5 digests if ($destRef->{$item}->{Digest} ne $sourceRef->{$item}->{Digest}) { # indicate that we want to make a copy push(@retArray, { Action => 'Copy', Detail => [$sourcePath.$item, $destPath] }); }; }; # no further action is required, look at the next item next CMPITEM; } else { # if it's a directory then we need to create it, if it's a file we need # to copy it if ($sourceRef->{$item}->{Type} eq 'File') { # indicate that we want to make a copy push(@retArray, { Action => 'Copy', Detail => [$sourcePath.$item, $destPath.$item] }); } else { # it's a directory, indicate that we want to create it push(@retArray, { Action => 'Create', Detail => $destPath.$item }); # create it in the destination tree $destRef->{$item} = { Contents => {}, Type => 'Dir' }; # we also need to recurse down into it my ($status, @tempArray) = CompareTrees( $sourcePath.$item.'/', $destPath.$item.'/', $sourceRef->{$item}->{Contents}, $destRef->{$item}->{Contents}); unless ($status) { warn("CompareTrees recursion failed"); return undef; }; push(@retArray, @tempArray); }; # item type is 'Dir' }; # !defined($destRef->{$item}) }; # foreach # success return (1, @retArray); }; # Sub: DoActions - carry out the actions to sync two trees # Usage: # $status = DoActions(@actionArray); # Where: # $status will be true if successful # @actionArray is an array of actions as returned by CompareTrees sub DoActions { # get the array my @actions = @_; # carry out the actions foreach my $action (@actions) { # what type of action is it? if ($action->{Action} eq 'Copy') { # it's a copy, get the source and destination print "Copying $action->{Detail}->[0]...\n"; unless (copy($action->{Detail}->[0], $action->{Detail}->[1])) { warn("Couldn't copy $action->{Detail}->[0] to $action->{Detail}->[1]: $!\n"); return undef; }; } else { # it's a create, get the directory name print "Creating $action->{Detail}...\n"; unless(mkdir $action->{Detail}) { warn("Couldn't create directory $action->{Detail}: $!\n"); return undef; }; }; }; # foreach Action return 1; }; # # Main # # read the config file ReadConfig($configFile, \%config) or die("Couldn't read config file $configFile\n"); # storage for the two trees my (%sourceTree, %destTree); # read the source tree PopulateTree($config{Source}, \%sourceTree) or die("Couldn't read source tree\n"); # and the destination tree PopulateTree($config{Dest}, \%destTree) or die("Couldn't read dest tree\n"); # find the differences my ($status, @actions) = CompareTrees($config{Source}, $config{Dest}, \%sourceTree, \%destTree) or die("Couldn't compare trees\n"); # check whether there are any actions if (scalar(@actions) == 0) { print "Nothing to do, source and destination trees are identical.\n"; }; DoActions(@actions) or die("Sync failed to create files/directories.\n"); print "Finished."; # So what do you think? # Kevin O'Rourke.