// // DraftsMigrator.swift // ComposeUI // // Created by Shadowfacts on 4/22/23. // import Foundation import OSLog import UniformTypeIdentifiers import Pachyderm import PencilKit import CoreData struct DraftsMigrator { private init() {} private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsMigrator") private static let decoder = PropertyListDecoder() static func migrate(from url: URL, to context: NSManagedObjectContext) -> Result<(), any Error> { do { let data = try Data(contentsOf: url) let container = try decoder.decode(DraftsContainer.self, from: data) for old in container.drafts.values { let new = Draft(context: context) new.id = old.id new.lastModified = old.lastModified new.accountID = old.accountID new.text = old.text new.contentWarningEnabled = old.contentWarningEnabled new.contentWarning = old.contentWarning new.inReplyToID = old.inReplyToID new.visibility = old.visibility new.localOnly = old.localOnly new.initialText = old.initialText if let oldPoll = old.poll { let newPoll = Poll(context: context) newPoll.draft = new new.poll = newPoll newPoll.multiple = oldPoll.multiple newPoll.duration = oldPoll.duration for oldOption in oldPoll.options { let newOption = PollOption(context: context) newOption.text = oldOption.text newOption.poll = newPoll newPoll.options.add(newOption) } } for oldAttachment in old.attachments { let newAttachment = DraftAttachment(context: context) newAttachment.draft = new new.attachments.add(newAttachment) newAttachment.id = oldAttachment.id newAttachment.attachmentDescription = oldAttachment.attachmentDescription switch oldAttachment.data { case .asset(let assetID): newAttachment.assetID = assetID case .image(let data, originalType: let type): newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: type) newAttachment.fileType = type.identifier case .video(_): fatalError("unreachable, video attachments weren't encodable") case .drawing(let drawing): newAttachment.drawing = drawing case .gif(let data): newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: .gif) newAttachment.fileType = UTType.gif.identifier } } } try FileManager.default.removeItem(at: url) } catch { logger.error("Error migrating: \(String(describing: error))") return .failure(error) } return .success(()) } // MARK: Supporting Types struct DraftsContainer: Decodable { let drafts: [UUID: OldDraft] init(drafts: [UUID: OldDraft]) { self.drafts = drafts } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.drafts = try container.decode([UUID: SafeDraft].self, forKey: .drafts).compactMapValues(\.draft) } enum CodingKeys: CodingKey { case drafts } } // a container that always succeeds at decoding // so if a single draft can't be decoded, we don't lose all drafts struct SafeDraft: Decodable { let draft: OldDraft? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.draft = try? container.decode(OldDraft.self) } } struct OldDraft: Decodable { let id: UUID let lastModified: Date let accountID: String let text: String let contentWarningEnabled: Bool let contentWarning: String let attachments: [OldDraftAttachment] let inReplyToID: String? let visibility: Visibility let poll: OldPoll? let localOnly: Bool let initialText: String init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(UUID.self, forKey: .id) self.lastModified = try container.decode(Date.self, forKey: .lastModified) self.accountID = try container.decode(String.self, forKey: .accountID) self.text = try container.decode(String.self, forKey: .text) self.contentWarningEnabled = try container.decode(Bool.self, forKey: .contentWarningEnabled) self.contentWarning = try container.decode(String.self, forKey: .contentWarning) self.attachments = try container.decode([OldDraftAttachment].self, forKey: .attachments) self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID) self.visibility = try container.decode(Visibility.self, forKey: .visibility) self.poll = try container.decode(OldPoll?.self, forKey: .poll) self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false self.initialText = try container.decode(String.self, forKey: .initialText) } enum CodingKeys: String, CodingKey { case id case lastModified case accountID case text case contentWarningEnabled case contentWarning case attachments case inReplyToID case visibility case poll case localOnly case initialText } } struct OldDraftAttachment: Decodable { let id: UUID let data: OldDraftAttachmentData let attachmentDescription: String init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(UUID.self, forKey: .id) self.data = try container.decode(OldDraftAttachmentData.self, forKey: .data) self.attachmentDescription = try container.decode(String.self, forKey: .attachmentDescription) } enum CodingKeys: String, CodingKey { case id case data case attachmentDescription } } enum OldDraftAttachmentData: Decodable { case asset(String) case image(Data, originalType: UTType) case video(URL) case drawing(PKDrawing) case gif(Data) init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) switch try container.decode(String.self, forKey: .type) { case "asset": let identifier = try container.decode(String.self, forKey: .assetIdentifier) self = .asset(identifier) case "image": let data = try container.decode(Data.self, forKey: .imageData) if let type = try container.decodeIfPresent(UTType.self, forKey: .imageType) { self = .image(data, originalType: type) } else { guard let image = UIImage(data: data) else { throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "CompositionAttachment data could not be decoded into UIImage") } let jpegData = image.jpegData(compressionQuality: 1)! self = .image(jpegData, originalType: .jpeg) } case "drawing": let drawingData = try container.decode(Data.self, forKey: .drawing) let drawing = try PKDrawing(data: drawingData) self = .drawing(drawing) default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing") } } enum CodingKeys: CodingKey { case type case imageData case imageType /// The local identifier of the PHAsset for this attachment case assetIdentifier /// The PKDrawing object for this attachment. case drawing } } struct OldPoll: Decodable { let options: [OldPollOption] let multiple: Bool let duration: TimeInterval init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.options = try container.decode([OldPollOption].self, forKey: .options) self.multiple = try container.decode(Bool.self, forKey: .multiple) self.duration = try container.decode(TimeInterval.self, forKey: .duration) } enum CodingKeys: String, CodingKey { case options case multiple case duration } } struct OldPollOption: Decodable { let text: String init(from decoder: Decoder) throws { self.text = try decoder.singleValueContainer().decode(String.self) } } }