Files
photo-live/apps/mobile/ios/Runner/UploadTaskStore.swift
T

131 lines
4.7 KiB
Swift
Raw Normal View History

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,
]
}
}