Copy a file with an existing file at the target

NSFileManager’s copyItemAtPath method does not overwrite an existing file and instead returns an error. So, to overwrite an existing file, it seems reasonable to first employ removeItemAtPath, and that’s the approach employed in the script below.

This all seems reasonable until the copy fails for some reason. Then, the script has deleted the existing file and can’t copy the new file. I can only think of two alternatives. The first is to rename the existing file and then to rename it back if the copy is not successful. The second is to move the existing file to NSTemporaryDirectory and then to move it back if the copy fails. Is there a better way to handle this? Thanks.

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (choose file)
set theFolder to POSIX path of (choose folder)
copyFile(theFile, theFolder)

on copyFile(POSIXPath, folderPath) -- from Shane's book with modification
	set POSIXPath to current application's NSString's stringWithString:POSIXPath
	set folderPOSIXPath to current application's NSString's stringWithString:folderPath
	set theName to POSIXPath's lastPathComponent()
	set newPath to folderPOSIXPath's stringByAppendingPathComponent:theName
	set fileManager to current application's NSFileManager's defaultManager()
	-- remove existing file - not in Shane's script
	if (fileManager's fileExistsAtPath:newPath) then (fileManager's removeItemAtPath:newPath |error|:(missing value))
	set {theResult, theError} to fileManager's copyItemAtPath:POSIXPath toPath:newPath |error|:(reference)
	if (theResult as boolean) is false then
		error (theError's localizedDescription() as text)
	end if
end copyFile

It looks like the “replaceItemAtURL:withItemURL:backupItemName:resultingItemURL:error:” NSFileManager function might be the better way to do it, although I’ve not tried it myself, but a previous forum posting here shows an example of how to use it.

And it seems to me, that backing up the file in a temporary directory first would be the wise move.

Regards Mark

Thanks Mark for the response. I tested that method and it works just as I want but it does a move rather than a copy. I guess I could copy the file back from the destination to the source but that doesn’t seem very good. I’ll have to give this some more thought.

My test script:

use framework "Foundation"
use scripting additions

set sourceFile to POSIX path of (path to desktop) & "Test File.txt"
set destinationFile to POSIX path of (path to documents folder) & "Test File.txt"

copyAndReplace(sourceFile, destinationFile)

on copyAndReplace(sourceFile, destinationFile)
	set sourceURL to current application's |NSURL|'s fileURLWithPath:sourceFile
	set destinationURL to current application's |NSURL|'s fileURLWithPath:destinationFile
	set filemanager to current application's NSFileManager's defaultManager
	set {theResult, theError} to filemanager's replaceItemAtURL:destinationURL withItemAtURL:sourceURL backupItemName:(missing value) options:0 resultingItemURL:(missing value) |error|:(reference)
end copyAndReplace

The suggested safe approach is to create a temporary directory, copy to that, use replaceItemAtURL::::: with the copied version, then delete the temp directory. You use URLForDirectory:inDomain:appropriateForURL:create:error: to create the (NSItemReplacementDirectory) directory, which will usually be in the same directory as the target.

This is the handler from my FileManagerLib library:

-- This handler is called by other handlers
on atomicReplaceFromURL:sourceURL toURL:destinationURL
	set theFileManager to NSFileManager's |defaultManager|()
	set {tempURLDir, theError} to theFileManager's URLForDirectory:99 inDomain:1 appropriateForURL:destinationURL create:true |error|:(reference) --  1 = NSUserDomainMask, 99 = NSItemReplacementDirectory
	if tempURLDir is missing value then error (theError's |localizedDescription|() as text) number (theError's code() as integer)
	set tempDestURL to tempURLDir's URLByAppendingPathComponent:(destinationURL's lastPathComponent())
	set {theResult, theError} to (theFileManager's copyItemAtURL:sourceURL toURL:tempDestURL |error|:(reference))
	if not theResult as boolean then
		theFileManager's removeItemAtURL:tempURLDir |error|:(missing value)
		error (theError's |localizedDescription|() as text) number (theError's code() as integer)
	end if
	set {theResult, theError} to theFileManager's replaceItemAtURL:destinationURL withItemAtURL:tempDestURL backupItemName:(missing value) options:1 resultingItemURL:(missing value) |error|:(reference) -- 1 = NSFileManagerItemReplacementUsingNewMetadataOnly
	theFileManager's removeItemAtURL:tempURLDir |error|:(missing value)
	-- if replacement failed, return error
	if not theResult as boolean then error (theError's |localizedDescription|() as text) number (theError's code() as integer)
end atomicReplaceFromURL:toURL:

Thanks Shane. Your description of the safe approach and of how replaceItemAtURL is utilized in this approach is excellent and makes perfect sense.