Exchange Files Gotcha

Summary: FSpExchangeFiles() will happily exchange two files even if one is already open for writing, which can lead to some bad behavior.

In your cool whiz-bang application, you’re implementing “Save As” with the following steps:

  • Save document data to a temporary file.
  • If file exists at “Save As” location, swap the temporary file with the real file.
  • Delete the temporary file.

This works swimmingly — unless the real file is open for writing in another application. Let’s call that app “BusyBody”.

You’d think, in that case, you’d get an OS error when you attempt to swap files, wouldn’t you? The filesystem should prevent such access when a file’s open…shouldn’t it?

Turns out it doesn’t prevent such access. It will happily swap open-for-writing files all the live-long day. You won’t get an error until you get to the last step. Then, the OS will tell you the temp file is “busy” (fBsyErr), because, as far as it’s concerned, the temp file is open in BusyBody.

So, if your app is handling errors correctly, at that point it will tell the user “Can’t do that, file’s busy.” But the damage has already been done.

When the user tries to “Save As” again to the same file — because she’s an idiot, or she’s a QA tester — the save succeeds. Why? Since BusyBody thinks it’s got that temp file open, the real file is free and clear for writing. This might not be a problem, but if BusyBody attempts to save again, instead of saving to where the user thinks it should save to, it will save to an invisible temp file. Oops!

The easiest solution to this that I can see is that, before you attempt such a switch, try to opening for writing the real file. If there’s an error with that open step, stop and signal the user, before any harm is done. That works as it should!

I wonder how many developers do the “right thing” by using FSpExchangeFiles(), but fail to check for open files first? Hopefully not many.

Note: I have not tried this with FSExchangeObjects(), though I’ll be getting to that. When I know, I’ll update this!