import Foundation /// 上传任务映射持久化(P3 计划 Task 8)。 /// /// `taskId → {注册参数, URLSession taskIdentifier, 执行计数}` 存 Application /// Support 下 plist;对应 Android 侧 taskId→workId 映射由 WorkManager 自持 /// 的职责——iOS 无系统级自持映射,改为本插件显式落盘: /// /// - App 被系统在后台拉起(或用户重启)后,`getAllTasks` 返回的系统任务经 /// taskIdentifier 反查 taskId,重挂事件归属(技术方案 §6.2「重新启动时 /// 重连系统会话并校准本地任务」); /// - 仅存映射所需最小字段;进度快照不落盘(重联后 didSendBodyData 会 /// 继续产生新进度)。 /// /// 线程模型:通道回调在主线程、session delegate 在后台队列访问——全部 /// 经 [lock] 串行化;写盘频率低(注册/终态/取消),同步写原子 plist。 final class UploadTaskStore { /// 单条任务映射(plist 可序列化字段)。 struct Entry { var url: String var method: String var headers: [String: String] var filePath: String var uploadId: String? var attempt: Int /// 已执行传输次数(含首发;镜像 Kotlin MAX_WORK_ATTEMPTS 语义)。 var executions: Int /// 对应 URLSessionTask.taskIdentifier(-1 = 尚未创建系统任务)。 var sessionTaskIdentifier: Int } private let lock = NSLock() private let fileURL: URL private var entries: [String: Entry] = [:] init(directory: URL? = nil, fileName: String = "upload-task-map.plist") { let dir = directory ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("PhotoLiveUploads", isDirectory: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) fileURL = dir.appendingPathComponent(fileName) if let raw = NSDictionary(contentsOf: fileURL) as? [String: [String: Any]] { entries = raw.mapValues { Entry(plist: $0) } } } // ── 读写 API(锁内) ────────────────────────────────────────────── func entry(taskId: String) -> Entry? { lock.lock() defer { lock.unlock() } return entries[taskId] } func taskId(sessionTaskIdentifier: Int) -> String? { lock.lock() defer { lock.unlock() } return entries.first { $0.value.sessionTaskIdentifier == sessionTaskIdentifier }?.key } func allTaskIds() -> [String] { lock.lock() defer { lock.unlock() } return Array(entries.keys) } func upsert(taskId: String, entry: Entry) { lock.lock() entries[taskId] = entry persistLocked() lock.unlock() } /// 原地修改并落盘(不存在则忽略)。 func mutate(taskId: String, _ transform: (inout Entry) -> Void) { lock.lock() guard var entry = entries[taskId] else { lock.unlock() return } transform(&entry) entries[taskId] = entry persistLocked() lock.unlock() } func remove(taskId: String) { lock.lock() entries.removeValue(forKey: taskId) persistLocked() lock.unlock() } // ── plist 编解码 ───────────────────────────────────────────────── private func persistLocked() { let raw = entries.mapValues { $0.asPlist() } as NSDictionary raw.write(to: fileURL, atomically: true) } } private extension UploadTaskStore.Entry { init(plist raw: [String: Any]) { self.init( url: raw["url"] as? String ?? "", method: raw["method"] as? String ?? "PUT", headers: (raw["headers"] as? [String: String]) ?? [:], filePath: raw["filePath"] as? String ?? "", uploadId: raw["uploadId"] as? String, attempt: (raw["attempt"] as? NSNumber)?.intValue ?? 0, executions: (raw["executions"] as? NSNumber)?.intValue ?? 1, sessionTaskIdentifier: (raw["sessionTaskIdentifier"] as? NSNumber)?.intValue ?? -1 ) } func asPlist() -> [String: Any] { [ "url": url, "method": method, "headers": headers, "filePath": filePath, "uploadId": uploadId ?? "", "attempt": attempt, "executions": executions, "sessionTaskIdentifier": sessionTaskIdentifier, ] } }