Atomically copying a file or directory

In Cocoa we are lucky to deal with high level constructs like NSDocument and NSData who neatly provide atomic writing as features for us. But sometimes you run into a task that they can't quite provide.

One such example is copying a file on disk. NSFileManager provides a routine to do exactly that, but it isn't atomic (rdar://15791104). You could cheat at this point, load the file into memory, and then ask NSData to write it out atomically. It's quite possible though that the file is too big to do this with (particularly plausible in a 32bit process).

Happily, as of OS X 10.6, Cocoa does provide all the pieces to achieve this. Atomicity is achieved by first writing to a temporary location on the same disk as the final destination. If that writing should fail then we can gracefully back out without having touched the true destination.

Once the temporary file is complete, it can be atomically be moved into place, replacing any existing file in the process. Here's a nice demo:

It's worth noting that this tweaks behaviour compared to the NSFileManager's usual copy routine; if the destination already exists it will be overwritten, whereas the file manager will fail in that circumstance.

File Permissions and other Security Attributes

It turns out this code does have an annoying weakness: it can be defeated by source items with an ACL (Access Control List) that forbids deletion, or simply by marking a file as “Locked” in the Finder! In copying the source, NSFileManager faithfully recreates all attributes, leaving us with a file that can't be moved into place (since that counts as effectively deleting/modifying it), and can't then be deleted from the temp directory.

If you enjoy debugging stories, you can read all about how we ran into this in Sandvox.

Non-atomic copies are susceptible to this problem too, but less immediately. Having successfully made a copy in the correct location, your app might later find itself unable to clear up this resource when no longer wanted.

The best solution I’ve found so far is to drop down to Apple's copyfile() routine, which offers finer-grained control. I pass in COPYFILE_DATA and COPYFILE_XATTR, but deliberately leave out the security-related COPYFILE_STAT and COPYFILE_ACL. Oh, and pass in COPYFILE_RECURSIVE too, to handle directories.

This results in a nice copy of the file’s data, plus any extended attributes that might have been applied, without the Locked flag, or any custom ACL or permissions.

The final step is apply any POSIX information which we do care about. In the case of Sandvox, I copy over the file’s various modification and creation dates using -[NSURL setResourceValues:error:].

Ideally, NSFileManager would have some mechanism to handle this for us (radar://15163549).

© Mike Abdullah 2007-2015