Thanks again for the reply, and especially for such a thorough and helpful one!
For example, I suspect this doesn't happen if you start with a security-scoped bookmark, which you resolve to a bookmark before each save.
Out of curiosity I just tested this, and I still see the bug. To see it yourself, just use the code from my first post but change the savingURL accessor to use a security-scoped bookmark, as follows:
private var bookmarkData: Data?
// Ask user to choose this via a save panel.
var savingURL: URL? {
get {
var isStale = false
if let bookmarkData,
let url = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) {
if isStale {
// Should really update the bookmark data here...
}
return url
} else {
return nil
}
}
set(newURL) {
bookmarkData = try? newURL?.bookmarkData(options: .withSecurityScope)
setUpSpamSave()
}
}
Then add the following to the top of spamSave() after checking savingURL is non-nil:
let didAccess = savingURL.startAccessingSecurityScopedResource()
if didAccess == false {
print("Failed to start accessing scoped URL.")
}
defer {
if didAccess {
savingURL.stopAccessingSecurityScopedResource()
}
}
The save will fail with the same permissions error every now and then.
Just to clarify, are you:
a) Copying the file once, modifying it over time, then copying that file back for each save.
b) Copying the file prior to each save operation.
I suspect you're doing "a" (and it's probably what I would do), but if you're doing "b”, then that changes things a bit.
I’m actually doing (b), since this is very fast on copy-on-write volumes such as APFS even for large files. (Copy-to-temp file is almost instant; updating the zip file is super-fast too thanks to LibZip’s support for copy-on-write, meaning it doesn’t recreate the entire zip file; then it's just a matter of moving the updated file back into place using replaceItemAt(_:withItemAt:.)
For slower volumes, much like Pages.app, we offer a second, package-based version of our file format which supports in-place saving. (Zip-based is the default since it works everywhere and package-based files don’t work with cloud-based services other than iCloud Drive on iOS. But if saves get particularly slow, we prompt users to consider the package-based option.)
As I mentioned, I keep a snapshot of the zip file from the previous save around (for cases where the user has accidentally deleted the underlying file between saves), and I have done some testing and I can indeed re-save successfully using that. That is slow, though, since it has to recreate the entire archive. On APFS it’s faster to make a backup copy of the temp file before using replaceItemAt and then to try with the copy if it fails - that seems to work well.
Based on your suggestions, though, I’m going to do a bit of refactoring. Keeping the zip file around in the temp folder and making copies from it for replaceItemAt sounds like a great solution with multiple advantages, and since my custom file wrapper already keeps a reference to the previous snapshot, it wouldn’t be difficult to have it keep a reference to a temp file URL too.
Your app copies from the destination to your local storage…. Your app pushes that initial save data to the final target.
This is a little off-topic, but in this case - where you are doing the intensive work on your local storage and then pushing back to the slower volume when done - what is the safest way of replacing the original file? The point of FileManager.url(for: . itemReplacementDirectory…) is to return a temp folder on the same volume as the passed-in URL, since replaceItemAt(_:withItemAt:) won’t work if the original and new URLs are on different volumes. The only other way I can think of risks data loss:
Delete the original file from the destination.
Move the updated file from the local storage to the destination.
We could make a temp copy of the original file before (1), but if the volume is slow, that adds back in some of the slowness we’re avoiding by doing work on another volume.
My own instincts would be to redo the entire save, but if you want to do this, I would do two things:
I’m going to focus on retrying the save. I’m curious though as to whether the bug could occur twice in immediate succession, so that the resave also triggers the error. Although I can’t get this to happen in testing, given that the error seems random, I wonder if it is possible if I ran the test for long enough - a day, say. In that case, I wonder if this approach would work:
Save - encounter some sort of save error.
Retry the save no matter what the error was.
If we get another error, examine the error and if it was caused by the bug, just try deleting the temp file and move on.
I’ve attached some code at the end of this post that scrutinises the error to check it matches the one triggered by this bug. (Although I wonder if I should use .fileContentIdentifier instead of .fileResourceIdentifier.)
Anyway, thanks again, as I’m very close to a solution now.
Topic:
App & System Services
SubTopic:
Core OS
Tags: