120 lines
4.0 KiB
Swift
120 lines
4.0 KiB
Swift
import CloudKit
|
|
import Foundation
|
|
|
|
/// Service for uploading and managing shared cards in CloudKit public database.
|
|
struct SharedCardCloudKitService: SharedCardProviding {
|
|
private let container: CKContainer
|
|
private let database: CKDatabase
|
|
private let ttlDays: Int
|
|
|
|
init(
|
|
containerID: String = AppIdentifiers.cloudKitContainerIdentifier,
|
|
ttlDays: Int = 7
|
|
) {
|
|
self.container = CKContainer(identifier: containerID)
|
|
self.database = container.publicCloudDatabase
|
|
self.ttlDays = ttlDays
|
|
}
|
|
|
|
@MainActor
|
|
func uploadSharedCard(_ card: BusinessCard) async throws -> SharedCardUploadResult {
|
|
let vCardData = card.vCardFilePayload
|
|
let expiresAt = Date.now.addingTimeInterval(TimeInterval(ttlDays * 24 * 60 * 60))
|
|
let recordID = CKRecord.ID(recordName: UUID().uuidString)
|
|
var photoAssetFileURL: URL?
|
|
|
|
let sharedCard = SharedCardRecord(
|
|
recordID: recordID,
|
|
displayName: card.vCardName.isEmpty ? card.fullName : card.vCardName,
|
|
role: card.role,
|
|
company: card.company,
|
|
vCardData: vCardData,
|
|
expiresAt: expiresAt
|
|
)
|
|
|
|
do {
|
|
photoAssetFileURL = try makePhotoAssetFile(from: card.photoData)
|
|
} catch {
|
|
throw SharedCardError.uploadFailed(error)
|
|
}
|
|
|
|
defer {
|
|
if let photoAssetFileURL {
|
|
try? FileManager.default.removeItem(at: photoAssetFileURL)
|
|
}
|
|
}
|
|
|
|
let record = sharedCard.toCKRecord(photoAssetFileURL: photoAssetFileURL)
|
|
|
|
do {
|
|
_ = try await database.save(record)
|
|
} catch {
|
|
throw SharedCardError.uploadFailed(error)
|
|
}
|
|
|
|
guard let appClipURL = AppIdentifiers.appClipURL(recordName: recordID.recordName) else {
|
|
throw SharedCardError.invalidURL
|
|
}
|
|
|
|
return SharedCardUploadResult(
|
|
recordName: recordID.recordName,
|
|
appClipURL: appClipURL,
|
|
expiresAt: expiresAt
|
|
)
|
|
}
|
|
|
|
func cleanupExpiredCards() async {
|
|
let predicate = NSPredicate(format: "expiresAt < %@", Date.now as NSDate)
|
|
let query = CKQuery(recordType: SharedCardRecord.recordType, predicate: predicate)
|
|
|
|
do {
|
|
let (results, _) = try await database.records(matching: query)
|
|
let recordIDs = results.compactMap { result -> CKRecord.ID? in
|
|
guard case .success = result.1 else { return nil }
|
|
return result.0
|
|
}
|
|
|
|
guard !recordIDs.isEmpty else { return }
|
|
|
|
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
|
|
operation.qualityOfService = .utility
|
|
database.add(operation)
|
|
} catch {
|
|
// Cleanup is best-effort; log but don't throw
|
|
}
|
|
}
|
|
|
|
private func makePhotoAssetFile(from photoData: Data?) throws -> URL? {
|
|
guard let photoData, !photoData.isEmpty else { return nil }
|
|
let fileURL = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).jpg")
|
|
try photoData.write(to: fileURL, options: .atomic)
|
|
return fileURL
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
/// Errors that can occur during shared card operations.
|
|
enum SharedCardError: Error, LocalizedError {
|
|
case invalidURL
|
|
case uploadFailed(Error)
|
|
case fetchFailed(Error)
|
|
case recordNotFound
|
|
case recordExpired
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidURL:
|
|
return String(localized: "Failed to create share URL")
|
|
case .uploadFailed(let error):
|
|
return String(localized: "Upload failed: \(error.localizedDescription)")
|
|
case .fetchFailed(let error):
|
|
return String(localized: "Could not load card: \(error.localizedDescription)")
|
|
case .recordNotFound:
|
|
return String(localized: "Card not found")
|
|
case .recordExpired:
|
|
return String(localized: "This card has expired")
|
|
}
|
|
}
|
|
}
|