92 lines
3.3 KiB
Swift
92 lines
3.3 KiB
Swift
import Foundation
|
|
import PDFKit
|
|
import UIKit
|
|
|
|
@MainActor
|
|
final class DefaultLeadDetailRepository: LeadDetailRepository {
|
|
private struct FileKey: Hashable {
|
|
let reference: LeadFileReference
|
|
let leadID: String
|
|
let salesCode: String
|
|
}
|
|
|
|
private let remoteService: any LeadDetailRemoteService
|
|
private let cacheDirectory: URL
|
|
private var cachedFiles: [FileKey: URL] = [:]
|
|
private var downloads: [FileKey: Task<URL, Error>] = [:]
|
|
|
|
init(remoteService: any LeadDetailRemoteService,
|
|
cacheDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("LeadFiles")) {
|
|
self.remoteService = remoteService
|
|
self.cacheDirectory = cacheDirectory
|
|
}
|
|
|
|
func detail(id: String, salesCode: String) async throws -> LeadDetail {
|
|
do {
|
|
return try await remoteService.fetchDetail(id: id, salesCode: salesCode).toDomain(expectedID: id)
|
|
} catch {
|
|
throw DefaultLeadRepository.map(error, leadID: id)
|
|
}
|
|
}
|
|
|
|
func file(_ reference: LeadFileReference, leadID: String, salesCode: String) async throws -> URL {
|
|
try Task.checkCancellation()
|
|
let key = FileKey(reference: reference, leadID: leadID, salesCode: salesCode)
|
|
if let url = cachedFiles[key], FileManager.default.fileExists(atPath: url.path) {
|
|
return url
|
|
}
|
|
if let download = downloads[key] {
|
|
let url = try await download.value
|
|
try Task.checkCancellation()
|
|
return url
|
|
}
|
|
let directory = cacheDirectory
|
|
let service = remoteService
|
|
// A shared download survives one viewer closing; that viewer checks cancellation below.
|
|
let download = Task {
|
|
let data: Data
|
|
do {
|
|
data = try await service.download(reference, leadID: leadID, salesCode: salesCode)
|
|
} catch LeadRemoteError.http(404) {
|
|
throw LeadFileError.notFound
|
|
} catch {
|
|
throw DefaultLeadRepository.map(error)
|
|
}
|
|
return try await Task.detached {
|
|
try Self.persist(data, kind: reference.kind, directory: directory)
|
|
}.value
|
|
}
|
|
downloads[key] = download
|
|
do {
|
|
let url = try await download.value
|
|
downloads[key] = nil
|
|
cachedFiles[key] = url
|
|
try Task.checkCancellation()
|
|
return url
|
|
} catch {
|
|
downloads[key] = nil
|
|
throw error
|
|
}
|
|
}
|
|
|
|
nonisolated private static func persist(_ data: Data, kind: LeadFileKind, directory: URL) throws -> URL {
|
|
switch kind {
|
|
case .pdf:
|
|
guard let document = PDFDocument(data: data), document.pageCount > 0, !document.isLocked else {
|
|
throw LeadFileError.invalidFile
|
|
}
|
|
case .image:
|
|
guard UIImage(data: data) != nil else { throw LeadFileError.invalidFile }
|
|
}
|
|
do {
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
let url = directory.appendingPathComponent(UUID().uuidString)
|
|
.appendingPathExtension(kind == .pdf ? "pdf" : "png")
|
|
try data.write(to: url, options: .atomic)
|
|
return url
|
|
} catch {
|
|
throw LeadFileError.storageUnavailable
|
|
}
|
|
}
|
|
}
|