dougscripts.com

    rsync dry run

  • Begin the script; initialize variables; get filepaths of files to be copied

Open a new document in Script Editor and save it right away as a compiled script named "rsync_music_folder.scpt" in your [username]/Library/Scripts/ folder.

The script we will be writing will use do shell script quite a bit. This enables AppleScript to run shell programs and--in most cases--grab the results. (Most of the routines I'll be presenting could probably be written as perl or unix scripts as well, but afterall, this is an AppleScript site.)

Below is the rsync command I eventually want to run via do shell script. Sorry that it runs across the page, but we'll pretty it up as we proceed.

rsync -nvauz --ignore-existing --exclude '.DS_Store' '/Users/dougadams/Music/iTunes/iTunes Music/' cleanuser@Intel-iMac.local:'/Users/cleanuser/Music/iTunes/iTunes\\ Music/'

Let me explain that a bit. Essentially, rsync's format is:

rsync [options] source destination

...such that the source and the destination directories are compared and if there are any differences between their files and folders then those differences will be copied from the source directory to the destination directory. Various [options] can be applied to otherwise control rsync's behavior. I'll explain those after we set the initial variables in our AppleScript.

First things first: create the variables that contain the destination machine's username and IP address strings as these will be required later on. (The IP address for a user's machine, as mentioned previously, can be found by selecting "Remote Login" in the Sharing pane of System Preferences and usually looks like "192.168.1.100".) These variables will be declared as global so various handlers can have access to them.1

Next we hard-code a source directory and a destination directory for rsync to compare. These will be the paths to the iTunes Music folder on each machine and we will set two variables to the POSIX-type file path to these folders.

Because the file path strings need to be formatted a couple of different ways--spaces need to be escaped for the destination directory string in consideratioin of ssh and both need to be quoted for unix--we will create the additional variables that contain these variations.

Finally we'll build the entire rsync command and assign it as a string to a variable which will be used with do shell script.

Our script starts like so:

-- == allocate globals

global remUserName, remHostIPAddr, remAddr

-- == init variables

set remUserName to "cleanuser" -- use your remote username
set remHostIPAddr to "192.168.1.206" -- use your remote IP address

set remAddr to (remUserName & "@" & remHostIPAddr) as text

-- use your own local and remote paths to the iTunes Music folders
set locMusicLibrary to "/Users/dougadams/Music/iTunes/iTunes Music/"
set remMusicLibrary to "/Users/cleanuser/Music/iTunes/iTunes Music/"

set locMusicLibraryQuoted to quoted form of locMusicLibrary
set remMusicLibraryEscaped to my replace_chars(remMusicLibrary, " ", "\\ ")
set remMusicLibraryEscapedQuoted to quoted form of remMusicLibraryEscaped

-- ==  rsync dry run

set theCommand to ("rsync -nEvauz --ignore-existing --exclude '.DS_Store'" & space & ¬
	locMusicLibraryQuoted & space & ¬
	remAddr & ":" & remMusicLibraryEscapedQuoted)

set rezList to my text_to_list(do shell script theCommand, ASCII character 13)

log rezList

-- == end of run ==

-- == handlers == == == == == == ==
on replace_chars(txt, srch, repl)
	set AppleScript's text item delimiters to the srch
	set the item_list to every text item of txt
	set AppleScript's text item delimiters to the repl
	set txt to the item_list as string
	set AppleScript's text item delimiters to ""
	return txt
end replace_chars

on text_to_list(txt, delim)
	set saveD to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to {delim}
		set theList to every text item of txt
	on error errStr number errNum
		set AppleScript's text item delimiters to saveD
		error errStr number errNum
	end try
	set AppleScript's text item delimiters to saveD
	return (theList)
end text_to_list

on list_to_text(theList, delim)
	set saveD to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to {delim}
		set txt to theList as text
	on error errStr number errNum
		set AppleScript's text item delimiters to saveD
		error errStr number errNum
	end try
	set AppleScript's text item delimiters to saveD
	return (txt)
end list_to_text


(The replace_chars(), text_to_list(), and list_to_text() handlers--or any handlers used subsequently--may not always appear in later examples. But you can assume they are present in the script if they are called.)

The file paths happen to be the default locations for each user's iTunes Music folder, but, of course, the iTunes Music folder can be located anywhere (it is the path assigned in iTunes > Preferences > Advanced). Make sure that the path starts with the machine's root directory, "/", and ends with a final "/" after the last directory. The path to an iTunes Music folder located on an external drive might look like "/Volumes/NameOfExternalDrive/iTunes Music/".

The variable setting routines should be self-explanatory. Let's look at the rsync command:

set theCommand to ("rsync -nEvauz --ignore-existing --exclude '.DS_Store'" & space & ¬
	locMusicLibraryQuoted & space & ¬
	remAddr & ":" & remMusicLibraryEscapedQuoted)

...and specifically at the rsync options:

-n
puts rsync into "dry run" mode. rsync will return a list of files that would have been copied but will not actually copy anything.
-E
extended-attributes such as Mac-centric ACLs and resource forks will be preserved.
-v
verbose mode tells rsync to output results and messages and what not. This will be contained in the result of the do shell script.
-a
archive mode tells rsync to--essentially--copy files recursively through sub-directories while maintaining permissions.
-u
update mode prevents newer files at the destination directory from being over-written.
-z
compression mode can speed up copying by compressing/uncompressing file data before/after being copied.
--ignore-existing
ignore any files that already exist in the destination directory. We don't want to copy a file twice, update or overwrite current files.
--exclude '.DS_Store'
Do not copy over any invisible DS_Store files. You can add other exlude rules and there will be more on this later.

It may seem that using -u and --ignore existing may be redundant but they are actually complimentary. The -u option prevents newer files from being over-written and the --ignore existing option prevents older files from being over-written. I really want to make sure that only the whole files and folders that don't yet exist in the destination directory get copied. I do not want them to be updated if, say, a user alters an album name or comment which would alter the file and cause it to be copied again (since the source and destination versions would then be different). That might change things for the other user's music collection. Also, I could use a variation of this script on "cleanuser's" end such that I could mirror the folders, and I wouldn't want any destructive copying going on under those circumstances either.

The colon (":") that is between the machine address and destination folder tells rsync to use a remote shell command to communicate with the destination. This will be ssh by default.

If everything has been set up properly to now and this script is run, it will return something like this in the rezList variable:

building file list ... done created directory Users/cleanuser/Music/iTunes/iTunes Music ./ Cut Copy/ Cut Copy/Bright Like Neon Love/ Cut Copy/Bright Like Neon Love/01 Time Stands Still.mp3 Cut Copy/Bright Like Neon Love/02 Future.mp3 Cut Copy/Bright Like Neon Love/03 Saturdays.mp3 Cut Copy/Bright Like Neon Love/04 Saturdays (Reprise).mp3 Cut Copy/Bright Like Neon Love/05 Going Nowhere.mp3 Cut Copy/Bright Like Neon Love/06 DD-5.mp3 Cut Copy/Bright Like Neon Love/07 That Was Just a Dream.mp3 Cut Copy/Bright Like Neon Love/08 Zap Zap.mp3 Cut Copy/Bright Like Neon Love/09 The Twilight.mp3 Cut Copy/Bright Like Neon Love/10 Autobahn Music Box.mp3 Cut Copy/Bright Like Neon Love/11 Bright Neon Payphone.mp3 Cut Copy/Bright Like Neon Love/12 A Dream.mp3 sent 2272 bytes received 338 bytes 5220.00 bytes/sec total size is 335561031 speedup is 128567.44

...where the components of the Cut Copy album "Bright Like Neon Love" are apparently not in "cleanuser's" iTunes Music folder. (Remember: in "dry run" mode rsync won't really send or receive any files, it just reports what would have happened.) From this result I want to isolate files that can be added to iTunes. I don't want the folders because if I told iTunes to add a folder it would add its entire contents which would be redundant at best and completely wrong at worst; a folder may have been modified for any number of reasons not just because one or more of its files changed. So I just want the files.

As you can see, the result contains partial file paths; just the portion of the file path that comes after the source path. This is fine, because I will be appending the destination source (the path to "cleanuser's" iTunes Music folder) to each of these partial file paths to obtain what will ultimately be the full path to each file after I rsync for real a second time later in the script.

Now, there may be a dozen ways to extract the file paths from that result and lose the junk. I'm going to use a bit of perl because perl works very fast with text. There may also be a dozen ways to do this with perl or grep and so on. Here is how I am doing it (this routine appears in the script right after the "log rezList" line--the full script is listed later):

-- == get file paths

set perlCom to "perl -e'
	@rez=(" & (("\"") & my list_to_text(rezList, "\",\"") & ("\"")) & ");
	$base=\"" & remMusicLibraryEscaped & "\";

 	while(1){
		$res=shift(@rez);
		last unless ($res!~m/.../);
	}

 	while(1){
		$res=pop(@rez);
		if ($res eq \"\"){last}
	}
	
	foreach $z(@rez) {
		push (@final,$base.$z.\"\\n\") unless ($z=~m/.\\/$/);
	}
	
	print sort @final;
'"
set perlRez to do shell script perlCom
if perlRez is "" then return

set filesToAdd to my text_to_list(perlRez, ASCII character 13)

log filesToAdd


The result of this will be an AppleScript list of file paths, constructed using the path to "cleanuser's" iTunes Music folder ($base in the script) and the partial file paths returned from the first rsync routine, eg:

/Users/cleanuser/Music/iTunes/iTunes Music/Cut Copy/Bright Like Neon Love/01 Time Stands Still.mp3

and so on. This result is stored in the variable filesToAdd. Here's the rundown of what's happening:

Start building the perl command as a text string to be run via do shell script. First set two variables: the array @rez will contain each of the lines of the rezList result formatted for perl, and the string $base will be assigned the path to the destination iTunes Music folder.

The two "while" routines remove the beginning and trailing information, leaving just a list of the partial file paths. (Note that there may be an additional line of information following the line that ends with "... done" and I could not easily filter this line reliably or accurately. If a file path does happen to be created using this line, it will error during the add-to-iTunes routine later with no harm done.)

If there are any file paths, the partial file paths are each appended to the $base string, and this string is appended with a carriage return, and the whole string is added to an array (using push), "unless" they are folders (they end with a forward slash).

The result from running the perl is a text string. If the result is an empty string, the script ends. Otherwise, the result will be converted to an AppleScript list (using the carriage return between each file path as a text delimiter) and assigned to filesToAdd.2

The filesToAdd list will be used after the files have been copied with a second run of rsync, next.

 

Get More Information:

See the man page for rsync. As manual pages go, it's very descriptive.

There is quite a lot of information on perl on the web. The routines (using shift, pop, while and foreach loops, push, unless, sort, print) are fairly basic and tutorials can be googled fruitfully.

Apple's Technical Note TN2065 should be read by anyone who uses do shell script, with especial regard to string quoting and escaping.

 

1We will not use the remote machine's hostname ("some-computer.local"). However, if you are certain your DHCP server will resolve a hostname correctly and consistently, then you can use it instead of the IP address. If you don't know what that means then stick with the IP address. Be aware that it is possible for a machine's IP address to be re-assigned by a router if, for example, the router is re-set or loses power, and so on.

2If you think you will be adding hundreds or thousands of tracks at a time, the filesToAdd variable may not be very efficient. You may want to export the finessed file paths to a text file and read from that later when we add the files to the destination iTunes. But for a few dozen files at a time, this will be fine. Or, if you are using this script initially to move thousands of tracks, you might be better off manually copying the first large batch, and then let the script handle transfers in the future.

Site contents © 2001 - 2019 (that's right: 2001) Doug Adams and weblished by Doug Adams. Contact support AT dougscripts DOT com. About.
All rights reserved. Privacy.
AppleScript, iTunes, iPod, iPad, and iPhone are registered trademarks of Apple Inc. This site has no direct affiliation with Apple, Inc.
The one who says "it cannot be done" should not interrupt the one doing it.