Many thanks for the reply and information, Kevin, much appreciated.
Have you filed a bug on this and, if so, what's the bug number? As part of that bug, I'd suggest installing the "iCloud Drive" profile, reproducing the issue a few times, then uploading a sysdiagnose of the failure. See the profile installation instructions for the full details of that process.
I hadn’t filed a bug report yet because I had assumed it was something I was doing wrong given that using replaceItem and a temporary folder is presumably a common pattern. I’ll file a report tomorrow - I’m following the iCloud Drive profile instructions you linked to and am now waiting the 24 hours they say I need to wait before I can get the sysdiagnose. Once I have that I’ll file the report along with a sample project.
Have you tried retrying the save? That appears to work in my testing, though it may not be a workable solution in your case. Beyond that, I'd need a better understanding of exactly how you're interacting with the files and what your full requirements are.
With a bit of refactoring I probably could retry the save. In my app this is all done inside my NSDocument’s writeToURL method. I use my own drop-in replacement for FileWrapper (you helped me with some of the finer points of FileWrapper a few years ago) that incrementally writes changes to a zip file using Libzip, which supports incremental saves on copy-on-write systems such as APFS.
A potential problem with the re-save approach is that my save usually works by copying the zip file at the original location to a temporary location, updating it there, and then moving it into place using replaceItemAt. After this particular replaceItemAt error, however, the original file has in fact been updated despite the error (the error being on the old version of the file which is now in the temporary directory). So if I re-save by making a copy of that and try updating again, I could potentially mess up the file by trying to save into it stuff that has actually already been done. (However, I do keep a snapshot of the older archive around in case of problems, so I might be able to work around this problem using that.)
I wonder, though - given that the original file has in fact been replaced by the temp file despite the error, can I not just check for this and ignore the error if the file seems to have been replaced after all? E.g.:
Before replacement, record the file resource ID of the temp file.
Use replaceItemAt(originalURL, withItemAt: tempURL).
If there’s an error, get the file resource ID for the file at the intended saving location and compare it against the ID I recorded in (1). If they are the same, I know the replacement has succeeded despite the error. In this case, I can just try to delete the temporary folder and move on.
If the file IDs of the current user file and the temp file from before replace don’t match or couldn’t be got, attempt a re-save.
Is there something wrong with this approach? (I’ve attached some sample code below demonstrating how this might work.)
Many thanks,
Keith
// Get a temporary folder appropriate for creating the new file in.
let replacementDirURL = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: savingURL, create: true)
// Create the new file at the temporary location.
let tempURL = replacementDirURL.appendingPathComponent(savingURL.lastPathComponent)
try createNewContentAt(url: tempURL)
// Record the file resource ID of the temp file we created.
let tempFileID = (try? tempURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
// Now try to move the file into place.
do {
// Use replaceItemAt to safely replace the original file with the updated file we created at the temp location.
_ = try fileManager.replaceItemAt(savingURL, withItemAt: tempURL)
// Clean up.
try? fileManager.removeItem(at: replacementDirURL)
} catch {
// Check to see if the original file was in fact replaced despite the error.
if let tempFileID,
let savingFileID = (try? savingURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier,
tempFileID.isEqual(savingFileID) {
// If so, just try to remove the temp dir and move on.
try? fileManager.removeItem(at: replacementDirURL)
} else {
// If we got here, replace really did fail and we need to handle it.
// We should do some more work and try to resave here before throwing an error.
throw error
}
}