diff --git a/Delta/Database/Model/Human/GameSave.swift b/Delta/Database/Model/Human/GameSave.swift index 3f66e59..024d23f 100644 --- a/Delta/Database/Model/Human/GameSave.swift +++ b/Delta/Database/Model/Human/GameSave.swift @@ -23,6 +23,12 @@ public class GameSave: _GameSave } } +extension GameSave +{ + // Hacky, but YOLO I'm under time crunch. + public static var ignoredCorruptedIDs = Set() +} + extension GameSave: Syncable { public static var syncablePrimaryKey: AnyKeyPath { @@ -71,27 +77,46 @@ extension GameSave: Syncable public func awakeFromSync(_ record: AnyRecord) throws { - guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) } - - if game.identifier != self.identifier + do { - let fetchRequest = GameSave.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier) + guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) } - if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil + if game.identifier != self.identifier { - // Relink game with its correct gameSave, in case we accidentally misplaced it. - // Otherwise, corrupted records might displace already-downloaded GameSaves - // due to automatic Core Data relationship propagation, despite us throwing error. - game.gameSave = misplacedGameSave + let fetchRequest = GameSave.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier) + + if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil + { + // Relink game with its correct gameSave, in case we accidentally misplaced it. + // Otherwise, corrupted records might displace already-downloaded GameSaves + // due to automatic Core Data relationship propagation, despite us throwing error. + game.gameSave = misplacedGameSave + } + else + { + // Either there is no misplacedGameSave, or there is but it's linked to another game somehow. + game.gameSave = nil + } + + throw SyncValidationError.incorrectGame(game.name) + } + } + catch let error as SyncValidationError + { + guard GameSave.ignoredCorruptedIDs.contains(self.identifier) else { throw error } + + let fetchRequest = Game.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier) + + if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first + { + self.game = correctGame } else { - // Either there is no misplacedGameSave, or there is but it's linked to another game somehow. - game.gameSave = nil + throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)]) } - - throw SyncValidationError.incorrectGame(game.name) } } } diff --git a/Delta/Settings/Syncing/RecordVersionsViewController.swift b/Delta/Settings/Syncing/RecordVersionsViewController.swift index 11d5d4a..8575fb4 100644 --- a/Delta/Settings/Syncing/RecordVersionsViewController.swift +++ b/Delta/Settings/Syncing/RecordVersionsViewController.swift @@ -258,6 +258,8 @@ private extension RecordVersionsViewController { DispatchQueue.main.async { + GameSave.ignoredCorruptedIDs.remove(self.record.recordID.identifier) + CATransaction.begin() CATransaction.setCompletionBlock { @@ -287,17 +289,34 @@ private extension RecordVersionsViewController } catch { - let title: String - - switch self.mode + switch error { - case .restoreVersion: title = NSLocalizedString("Failed to Restore Version", comment: "") - case .resolveConflict: title = NSLocalizedString("Failed to Resolve Conflict", comment: "") + case RecordError.other(let record, let error as SyncValidationError): + // Only allow restoring corrupted records with incorrect games. + guard case .incorrectGame = error else { fallthrough } + + let message = NSLocalizedString("Would you like to download this version anyway?", comment: "") + let alertController = UIAlertController(title: NSLocalizedString("Record Version Corrupted", comment: ""), message: message, preferredStyle: .alert) + alertController.addAction(.cancel) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Download Anyway", comment: ""), style: .destructive) { _ in + GameSave.ignoredCorruptedIDs.insert(record.recordID.identifier) + self.restoreVersion() + }) + self.present(alertController, animated: true, completion: nil) + + default: + let title: String + + switch self.mode + { + case .restoreVersion: title = NSLocalizedString("Failed to Restore Version", comment: "") + case .resolveConflict: title = NSLocalizedString("Failed to Resolve Conflict", comment: "") + } + + let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) + alertController.addAction(.ok) + self.present(alertController, animated: true, completion: nil) } - - let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(.ok) - self.present(alertController, animated: true, completion: nil) } CATransaction.commit()