256 lines
9.9 KiB
Swift
256 lines
9.9 KiB
Swift
|
//
|
||
|
// 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)
|
||
|
}
|
||
|
}
|
||
|
}
|