Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't resolve file conflict - pick a currentVersion.modificationDate #6

Open
Volodymyr-13 opened this issue Apr 15, 2024 · 4 comments

Comments

@Volodymyr-13
Copy link

Volodymyr-13 commented Apr 15, 2024

Hey @drewmccormack,

I hope you're doing well! Firstly, I want to express my appreciation for your work on this project—it's been incredibly valuable.

I've encountered an issue after integrating the latest changes and wanted to bring it to your attention.

Let me walk you through the scenario:

I have two devices linked to the same account, an iPhone XR and an iPad. Here's how I create a conflict:

  1. I initiate a text file on the online device (iPhone XR).
  2. I record something on the offline device (iPad).
  3. I update the file again on the online device (iPhone XR).
  4. I switch the offline device (iPad) back online.

My expectation is that the last recorded text should be retrieved from the file that was updated on the iPhone XR since it was the latest action taken on this device.

However, upon subsequent attempts to read the file, I encounter a conflict. I aim to resolve this conflict in accordance with Apple's guidelines, referencing https://developer.apple.com/documentation/uikit/documents_data_and_pasteboard/synchronizing_documents_in_the_icloud_environment#3743502

My approach is to compare modification dates using NSFileVersion, but strangely, I'm getting identical modification dates for both NSFileVersion.unresolvedConflictVersionsOfItem and NSFileVersion.currentVersionOfItem. Consequently, my function fails to accurately compare these dates.

Below is the snippet of code I'm using:

   private func pickLatestVersion(for documentURL: URL) -> Bool {
        guard let versionsInConflict = NSFileVersion.unresolvedConflictVersionsOfItem(at: documentURL),
              let currentVersion = NSFileVersion.currentVersionOfItem(at: documentURL) else {
            return false
        }
        let currentVersionURLDate: Date? = currentVersion.url.getAttribute(fileAttribute: .modificationDate)
        print("Current Version: ", currentVersion.modificationDate?.formatted(date: .abbreviated, time: .complete))
        print("Current Version URL DATE: ", currentVersionURLDate?.formatted(date: .abbreviated, time: .complete))
        let currentString = try? String(contentsOf: currentVersion.url, encoding: .utf8)
        print("Current String: ", currentString)
        var shouldRevert = false
        var winner = currentVersion
        for version in versionsInConflict {
            let versionURLDate: Date? = version.url.getAttribute(fileAttribute: .modificationDate)
            print("Conflict Version: ", version.modificationDate?.formatted(date: .abbreviated, time: .complete))
            print("Conflict Version URL DATE: ", versionURLDate?.formatted(date: .abbreviated, time: .complete))
            let conflictString = try? String(contentsOf: version.url, encoding: .utf8)
            print("Conflict String: ", conflictString)
            
            
            if let date1 = version.modificationDate, let date2 = currentVersion.modificationDate,
               date1 > date2 {
                winner = version
            }
        }
        if winner != currentVersion {
            do {
                try winner.replaceItem(at: documentURL)
                shouldRevert = true
            } catch {
                print("Failed to replace version: \(error)")
            }
        }
        do {
            try NSFileVersion.removeOtherVersionsOfItem(at: documentURL)
            versionsInConflict.forEach({$0.isResolved = true})
        } catch {
            print("Failed to remove other versions: \(error)")
        }
        return shouldRevert
    }

For illustrative purposes, here are the logs I receive after encountering the conflict. Additionally, I'm puzzled as to why extracting the modificationDate attribute from the conflict file URL yields one value while NSFileVersion.modificationDate returns another.

Conflicts exist for the document file located at the file URL.
Current Version: Optional("Apr 15, 2024 at 5:18:21 PM GMT+3")
Current Version URL DATE: Optional("Apr 15, 2024 at 5:18:21 PM GMT+3")
Current String: Optional("Xr")
Conflict Version: Optional("Apr 15, 2024 at 5:18:21 PM GMT+3")
Conflict Version URL DATE: Optional("Apr 15, 2024 at 5:18:35 PM GMT+3")
Conflict String: Optional(“iPad one)

Any insights into this matter would be greatly appreciated.

Here is also a complete Xcode project for this, which could be easily tested on devices:

iCloudFileConflict.zip

@drewmccormack
Copy link
Owner

I did a bit of work on updating files recently, and realized it is not very reliable as is. In particular, it seems you cannot be sure you will get NSMetadata when you update a file on another device. If these metadata entries did fire properly every time, SwiftCloudDrive itself should do the conflict resolution. You can find the code for that in the MetadataMonitor. But because the metadata notification does not always fire for updates, you are seeing conflicts.

To summarize, SwiftCloudDrive was designed so that it would automatically resolve conflicts, but it is not doing that, because it is not seeing some file updates.

To fix this probably requires quite a bit of rewriting, using NSFilePresenter instead of NSMetadataQuery. I don't really have time to make these changes at this point in time.

Someone else was doing something similar to you, ie, updating files. What they discovered was you could instead delete the file, and create it, instead of updating. This apparently did always trigger the metadata notifications. That might be an option for you.

As for why the dates are the same, I don't have enough experience with the file versioning. I've actually never relied on that. If this is very important to you, you might even consider adding your own versioning. Eg. For a file, you would create a directory, and the directory would contain versions. Each version would be named with a timestamp. If more than one file was present, you would merge them, or delete the older ones. This would probably be much more robust than relying on Apple's janky system.

@drewmccormack
Copy link
Owner

I've just pushed changes to master that use NSFilePresenter for better change monitoring, and also give an option to use your own conflict resolution. If you get a chance, try it out, and let me know if there are issues.

@Volodymyr-13
Copy link
Author

@drewmccormack, thanks for the update. We tried, but in final - our only option was to include a file date into the file itself. I'm still confused about how this is supposed to work in general, from an Apple perspective. It seems like it should just check for the modification date, but that's the biggest problem—it's not working, as the dates reported by iOS are the same...

@drewmccormack
Copy link
Owner

My general advice would be to build in your own metadata if you need that, like you have. iCloud is an odd beast. It isn't really a local file system, more like a cache, so it is less clear what something like "modification date" would mean. eg. is it the last modification made locally? On a different device? By the iCloud synchronization? To do it properly, better to build in something yourself with some extra files, or embedding in the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants