532 lines
21 KiB
Swift
532 lines
21 KiB
Swift
|
|
import Flutter
|
|||
|
|
import Foundation
|
|||
|
|
|
|||
|
|
/// 上传桥接插件(P3 计划 Task 8;协议与 Android `UploadPlugin.kt` 逐字对齐)。
|
|||
|
|
///
|
|||
|
|
/// - MethodChannel `photolive/uploads`:registerTask(入队后台 URLSession
|
|||
|
|
/// upload task)/ cancelTask / queryTask / ackEvents;
|
|||
|
|
/// - EventChannel `photolive/uploads/events`:[UploadEventBus] 终态/进度
|
|||
|
|
/// 事件(终态未 ack 缓存重放 + **跨进程落盘**,见总线文档);
|
|||
|
|
/// - 实现 = URLSession background(identifier `com.photolive.photo_live.uploads`):
|
|||
|
|
/// 传输由系统调度,App 退后台/锁屏后继续;完成后系统在后台拉起 App 投递
|
|||
|
|
/// 回调(`handleEventsForBackgroundURLSession` → 重建会话重挂 delegate)。
|
|||
|
|
///
|
|||
|
|
/// **进程死亡语义**:iOS 对后台会话事件会在需要时重新拉起 App(区别于
|
|||
|
|
/// Android WorkManager 自持重跑)——本插件启动即重建会话(`ensureSession`),
|
|||
|
|
/// delegate 回调照常入总线;终态已落盘,Dart 引擎下次 onListen 时重放,
|
|||
|
|
/// 镜像 Android 的缓存重放语义。用户强制退出后系统不再代跑,重新启动时
|
|||
|
|
/// 由重联 + 重放校准(技术方案 §6.2)。
|
|||
|
|
///
|
|||
|
|
/// 失败分类(didCompleteWithError,镜像 Kotlin `UploadPutExecutor`):
|
|||
|
|
/// 2xx → completed(带 ETag);403 → needsRenew;其余 4xx → failed
|
|||
|
|
/// HTTP_4XX;5xx / NSURLErrorDomain 网络错误 → 原生侧重试(上限
|
|||
|
|
/// [maxWorkAttempts],指数退避初始 10s——镜像 WorkManager 语义;App 挂起
|
|||
|
|
/// 时退避定时器暂停,唤醒后续跑),耗尽后 failed SERVER_ERROR / NETWORK;
|
|||
|
|
/// 源文件缺失(注册时预检)→ failed SOURCE_MISSING。
|
|||
|
|
final class UploadPlugin: NSObject {
|
|||
|
|
|
|||
|
|
static let shared = UploadPlugin()
|
|||
|
|
|
|||
|
|
/// 传输尝试上限(含首发;对齐 Kotlin UploadWorker.MAX_WORK_ATTEMPTS)。
|
|||
|
|
static let maxWorkAttempts = 5
|
|||
|
|
|
|||
|
|
/// 重试退避初始间隔(秒;指数翻倍,对齐 Android WorkManager 10s 初始)。
|
|||
|
|
static let retryBackoffBaseSeconds: Double = 10
|
|||
|
|
|
|||
|
|
/// 进度事件节流(秒;Android 侧每 64KB 块发事件、通知 500ms 节流——
|
|||
|
|
/// iOS 统一在事件层 500ms 节流,终值(sent==total)必发)。
|
|||
|
|
static let progressThrottleSeconds: Double = 0.5
|
|||
|
|
|
|||
|
|
private let lock = NSLock()
|
|||
|
|
private let bus = UploadEventBus.shared
|
|||
|
|
let store = UploadTaskStore()
|
|||
|
|
|
|||
|
|
private var session: URLSession?
|
|||
|
|
private var sessionDelegate: UploadSessionDelegate?
|
|||
|
|
|
|||
|
|
/// 运行期内存态(锁内):活动系统任务 / 进度快照 / 静默取消标记 /
|
|||
|
|
/// 进度节流时间戳 / 系统后台回调。
|
|||
|
|
private var activeTasks: [Int: URLSessionTask] = [:]
|
|||
|
|
private var lastProgress: [String: (sent: Int64, total: Int64)] = [:]
|
|||
|
|
private var quietCancelIdentifiers: Set<Int> = []
|
|||
|
|
private var progressEmittedAt: [String: Double] = [:]
|
|||
|
|
private var backgroundCompletionHandler: (() -> Void)?
|
|||
|
|
|
|||
|
|
// ── 通道注册(AppDelegate 调用) ─────────────────────────────────
|
|||
|
|
|
|||
|
|
static func register(with messenger: FlutterBinaryMessenger) {
|
|||
|
|
FlutterMethodChannel(
|
|||
|
|
name: UploadBridge.methodChannel,
|
|||
|
|
binaryMessenger: messenger
|
|||
|
|
)
|
|||
|
|
.setMethodCallHandler { call, result in
|
|||
|
|
UploadPlugin.shared.handleMethodCall(call, result: result)
|
|||
|
|
}
|
|||
|
|
FlutterEventChannel(
|
|||
|
|
name: UploadBridge.eventChannel,
|
|||
|
|
binaryMessenger: messenger
|
|||
|
|
)
|
|||
|
|
.setStreamHandler(UploadEventStreamHandler())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 后台会话重建(启动 / 系统后台拉起时调用;幂等)。
|
|||
|
|
func ensureSession() {
|
|||
|
|
lock.lock()
|
|||
|
|
guard session == nil else {
|
|||
|
|
lock.unlock()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
let config = URLSessionConfiguration.background(
|
|||
|
|
withIdentifier: UploadBridge.sessionIdentifier
|
|||
|
|
)
|
|||
|
|
config.isDiscretionary = false
|
|||
|
|
config.sessionSendsLaunchEvents = true
|
|||
|
|
// 无网时挂起等待而非立即失败(镜像 Android NetworkType.CONNECTED 约束)。
|
|||
|
|
config.waitsForConnectivity = true
|
|||
|
|
let delegate = UploadSessionDelegate(plugin: self)
|
|||
|
|
sessionDelegate = delegate
|
|||
|
|
session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
|||
|
|
lock.unlock()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 系统后台拉起回调暂存(urlSessionDidFinishEvents 时消费)。
|
|||
|
|
func storeBackgroundCompletionHandler(_ handler: @escaping () -> Void) {
|
|||
|
|
lock.lock()
|
|||
|
|
backgroundCompletionHandler = handler
|
|||
|
|
lock.unlock()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func finishBackgroundEvents() {
|
|||
|
|
DispatchQueue.main.async { [weak self] in
|
|||
|
|
guard let self = self else { return }
|
|||
|
|
self.lock.lock()
|
|||
|
|
let handler = self.backgroundCompletionHandler
|
|||
|
|
self.backgroundCompletionHandler = nil
|
|||
|
|
self.lock.unlock()
|
|||
|
|
handler?()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── MethodChannel(镜像 Kotlin handleMethodCall) ────────────────
|
|||
|
|
|
|||
|
|
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|||
|
|
switch call.method {
|
|||
|
|
case UploadBridge.methodRegister:
|
|||
|
|
handleRegister(call, result: result)
|
|||
|
|
case UploadBridge.methodCancel:
|
|||
|
|
handleCancel(call, result: result)
|
|||
|
|
case UploadBridge.methodQuery:
|
|||
|
|
handleQuery(call, result: result)
|
|||
|
|
case UploadBridge.methodAck:
|
|||
|
|
handleAck(call, result: result)
|
|||
|
|
default:
|
|||
|
|
result(FlutterMethodNotImplemented)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func handleRegister(_ call: FlutterMethodCall, result: FlutterResult) {
|
|||
|
|
guard let raw = call.arguments as? [String: Any] else {
|
|||
|
|
result(FlutterError(code: "bad_args", message: "arguments must be a Map", details: nil))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
do {
|
|||
|
|
let spec = try parseRegisterSpec(raw)
|
|||
|
|
// 重注册(renew 换 URL / 崩溃恢复重驱):作废旧缓存终态 +
|
|||
|
|
// REPLACE 旧系统任务(防同任务双跑;镜像 Kotlin unique work REPLACE)。
|
|||
|
|
bus.purgeTask(taskId: spec.taskId)
|
|||
|
|
try startTask(spec: spec)
|
|||
|
|
result(true)
|
|||
|
|
} catch let error as UploadBridgeBadArgsError {
|
|||
|
|
result(FlutterError(code: "bad_args", message: error.message, details: nil))
|
|||
|
|
} catch {
|
|||
|
|
result(FlutterError(code: "enqueue_failed", message: "\(error)", details: nil))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func handleCancel(_ call: FlutterMethodCall, result: FlutterResult) {
|
|||
|
|
guard let taskId = taskChannelArg(call, key: "taskId") as? String else {
|
|||
|
|
result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
cancelQuietly(taskId: taskId)
|
|||
|
|
bus.purgeTask(taskId: taskId)
|
|||
|
|
result(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func handleQuery(_ call: FlutterMethodCall, result: FlutterResult) {
|
|||
|
|
guard let taskId = taskChannelArg(call, key: "taskId") as? String else {
|
|||
|
|
result(FlutterError(code: "bad_args", message: "missing 'taskId'", details: nil))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
guard let entry = store.entry(taskId: taskId) else {
|
|||
|
|
// 无映射(未注册 / 已终态清理):镜像 Kotlin 无 WorkInfo 的 null。
|
|||
|
|
result(nil)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
lock.lock()
|
|||
|
|
let task = activeTasks[entry.sessionTaskIdentifier]
|
|||
|
|
let progress = lastProgress[taskId]
|
|||
|
|
lock.unlock()
|
|||
|
|
let state: String
|
|||
|
|
if let task = task {
|
|||
|
|
switch task.state {
|
|||
|
|
case .running: state = "RUNNING"
|
|||
|
|
case .suspended: state = "ENQUEUED"
|
|||
|
|
case .canceling: state = "CANCELLED"
|
|||
|
|
case .completed: state = "SUCCEEDED"
|
|||
|
|
@unknown default: state = "ENQUEUED"
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 未重挂(进程重启后):系统仍持有任务,等同等待系统调度。
|
|||
|
|
state = "ENQUEUED"
|
|||
|
|
}
|
|||
|
|
result([
|
|||
|
|
"state": state,
|
|||
|
|
"attempt": entry.executions,
|
|||
|
|
"progress": progress.map { ["sent": $0.sent, "total": $0.total] },
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func handleAck(_ call: FlutterMethodCall, result: FlutterResult) {
|
|||
|
|
let seq: Int?
|
|||
|
|
if let v = taskChannelArg(call, key: "seq") as? NSNumber {
|
|||
|
|
seq = v.intValue
|
|||
|
|
} else if let v = taskChannelArg(call, key: "seq") as? Int {
|
|||
|
|
seq = v
|
|||
|
|
} else {
|
|||
|
|
seq = nil
|
|||
|
|
}
|
|||
|
|
guard let seq = seq else {
|
|||
|
|
result(FlutterError(code: "bad_args", message: "missing or invalid 'seq'", details: nil))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
bus.ack(seq: seq)
|
|||
|
|
result(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// cancelTask/queryTask/ackEvents 的字符串参数取值。
|
|||
|
|
private func taskChannelArg(_ call: FlutterMethodCall, key: String) -> Any? {
|
|||
|
|
(call.arguments as? [String: Any])?[key]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 任务生命周期 ─────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// 创建并启动后台 upload task(registerTask / 原生重试共用;
|
|||
|
|
/// [executions] 为已执行计数——重试路径沿用,注册路径自 1 起)。
|
|||
|
|
private func startTask(spec: UploadRegisterSpec, executions: Int = 1) throws {
|
|||
|
|
ensureSession()
|
|||
|
|
guard let session = session else {
|
|||
|
|
throw NSError(
|
|||
|
|
domain: "com.photolive.photo_live.uploads", code: 1,
|
|||
|
|
userInfo: [NSLocalizedDescriptionKey: "session unavailable"]
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
// REPLACE:作废旧系统任务(静默取消,不产生终态事件)。
|
|||
|
|
cancelQuietly(taskId: spec.taskId)
|
|||
|
|
guard let url = URL(string: spec.url) else {
|
|||
|
|
throw UploadBridgeBadArgsError(message: "'url' invalid")
|
|||
|
|
}
|
|||
|
|
// 源文件缺失预检(镜像 Kotlin UploadPutExecutor 的 File.exists 分类;
|
|||
|
|
// 交由 URLSession 才失败会被误归 NETWORK)。
|
|||
|
|
guard FileManager.default.fileExists(atPath: spec.filePath) else {
|
|||
|
|
bus.emitFailed(taskId: spec.taskId, code: UploadBridge.failSourceMissing, status: nil)
|
|||
|
|
store.remove(taskId: spec.taskId)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var request = URLRequest(url: url)
|
|||
|
|
request.httpMethod =
|
|||
|
|
spec.method.isEmpty ? "PUT" : spec.method.uppercased()
|
|||
|
|
for (name, value) in spec.headers {
|
|||
|
|
// Content-Length 由 uploadTask(fromFile:) 按文件长度自带;
|
|||
|
|
// Content-Type URLSession 不会自动设置,原样透传(与 Kotlin
|
|||
|
|
// 经 RequestBody.contentType 表达等价)。
|
|||
|
|
if name.lowercased() == "content-length" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
request.setValue(value, forHTTPHeaderField: name)
|
|||
|
|
}
|
|||
|
|
let task = session.uploadTask(
|
|||
|
|
with: request,
|
|||
|
|
fromFile: URL(fileURLWithPath: spec.filePath)
|
|||
|
|
)
|
|||
|
|
store.upsert(
|
|||
|
|
taskId: spec.taskId,
|
|||
|
|
entry: UploadTaskStore.Entry(
|
|||
|
|
url: spec.url,
|
|||
|
|
method: request.httpMethod ?? "PUT",
|
|||
|
|
headers: spec.headers,
|
|||
|
|
filePath: spec.filePath,
|
|||
|
|
uploadId: spec.uploadId,
|
|||
|
|
attempt: spec.attempt,
|
|||
|
|
executions: executions,
|
|||
|
|
sessionTaskIdentifier: task.taskIdentifier
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
lock.lock()
|
|||
|
|
activeTasks[task.taskIdentifier] = task
|
|||
|
|
lastProgress[spec.taskId] = nil
|
|||
|
|
progressEmittedAt[spec.taskId] = nil
|
|||
|
|
lock.unlock()
|
|||
|
|
task.resume()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 静默取消(用户取消 / 重注册替换):无终态事件(镜像 Kotlin 取消语义
|
|||
|
|
/// ——Dart 引擎先行置 cancelled 终态,本侧不发事件)。
|
|||
|
|
private func cancelQuietly(taskId: String) {
|
|||
|
|
let identifier = store.entry(taskId: taskId)?.sessionTaskIdentifier ?? -1
|
|||
|
|
if identifier >= 0 {
|
|||
|
|
lock.lock()
|
|||
|
|
quietCancelIdentifiers.insert(identifier)
|
|||
|
|
if let task = activeTasks[identifier] {
|
|||
|
|
task.cancel()
|
|||
|
|
}
|
|||
|
|
lock.unlock()
|
|||
|
|
// 未重挂的任务(进程重启后)经系统任务表兜底取消。
|
|||
|
|
session?.getAllTasks { [weak self] tasks in
|
|||
|
|
for task in tasks where task.taskIdentifier == identifier {
|
|||
|
|
task.cancel()
|
|||
|
|
}
|
|||
|
|
self?.lock.lock()
|
|||
|
|
self?.quietCancelIdentifiers.remove(identifier)
|
|||
|
|
self?.lock.unlock()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
lock.lock()
|
|||
|
|
lastProgress[taskId] = nil
|
|||
|
|
progressEmittedAt[taskId] = nil
|
|||
|
|
lock.unlock()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 终态/静默结束后的运行期清理(store 条目由调用方决定去留)。
|
|||
|
|
private func taskRuntimeCleanup(taskId: String, identifier: Int) {
|
|||
|
|
lock.lock()
|
|||
|
|
activeTasks[identifier] = nil
|
|||
|
|
quietCancelIdentifiers.remove(identifier)
|
|||
|
|
lastProgress[taskId] = nil
|
|||
|
|
progressEmittedAt[taskId] = nil
|
|||
|
|
lock.unlock()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── delegate 回调入口(UploadSessionDelegate 转发) ──────────────
|
|||
|
|
|
|||
|
|
fileprivate func taskId(forSessionTask identifier: Int) -> String? {
|
|||
|
|
store.taskId(sessionTaskIdentifier: identifier)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 进度节流(500ms;终值必发)。返回是否应发事件。
|
|||
|
|
fileprivate func shouldEmitProgress(taskId: String, sent: Int64, total: Int64) -> Bool {
|
|||
|
|
lock.lock()
|
|||
|
|
lastProgress[taskId] = (sent, total)
|
|||
|
|
let now = Date().timeIntervalSince1970
|
|||
|
|
let last = progressEmittedAt[taskId] ?? 0
|
|||
|
|
let terminalValue = total > 0 && sent >= total
|
|||
|
|
if terminalValue || now - last >= UploadPlugin.progressThrottleSeconds {
|
|||
|
|
progressEmittedAt[taskId] = now
|
|||
|
|
lock.unlock()
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
lock.unlock()
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 静默取消判定(用户取消 / 替换;系统 NSURLErrorCancelled 归同处理)。
|
|||
|
|
fileprivate func isQuietlyCancelled(identifier: Int) -> Bool {
|
|||
|
|
lock.lock()
|
|||
|
|
defer { lock.unlock() }
|
|||
|
|
return quietCancelIdentifiers.contains(identifier)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 传输结束分类(didCompleteWithError;分类表见类注释)。
|
|||
|
|
fileprivate func handleTaskCompletion(
|
|||
|
|
taskId: String,
|
|||
|
|
identifier: Int,
|
|||
|
|
response: NSHTTPURLResponse?,
|
|||
|
|
error: Error?
|
|||
|
|
) {
|
|||
|
|
if isQuietlyCancelled(identifier: identifier) {
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if let nsError = error as NSError? {
|
|||
|
|
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 网络层错误(镜像 Kotlin IOException → 可重试网络类)。
|
|||
|
|
handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
guard let status = response?.statusCode else {
|
|||
|
|
handleRetryable(taskId: taskId, identifier: identifier, serverError: false, status: nil)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
switch status {
|
|||
|
|
case 200...299:
|
|||
|
|
bus.emitCompleted(taskId: taskId, etag: response?.value(forHTTPHeaderField: "ETag"))
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
case 403:
|
|||
|
|
bus.emitNeedsRenew(taskId: taskId)
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
case 400...499:
|
|||
|
|
bus.emitFailed(taskId: taskId, code: UploadBridge.failHttp4xx, status: status)
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
default:
|
|||
|
|
handleRetryable(taskId: taskId, identifier: identifier, serverError: true, status: status)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 可重试结局:未达尝试上限 → 指数退避后重建任务;达上限 → 终态事件
|
|||
|
|
/// (SERVER_ERROR / NETWORK;status 语义对齐 Kotlin——5xx 带 status,
|
|||
|
|
/// 网络类不带)。退避定时器经主队列 dispatchAfter——App 挂起期间暂停、
|
|||
|
|
/// 唤醒后补跑(系统已按需后台拉起,G3 真机校准点)。
|
|||
|
|
private func handleRetryable(
|
|||
|
|
taskId: String,
|
|||
|
|
identifier: Int,
|
|||
|
|
serverError: Bool,
|
|||
|
|
status: Int?
|
|||
|
|
) {
|
|||
|
|
taskRuntimeCleanup(taskId: taskId, identifier: identifier)
|
|||
|
|
guard let entry = store.entry(taskId: taskId) else { return }
|
|||
|
|
if entry.executions >= UploadPlugin.maxWorkAttempts {
|
|||
|
|
bus.emitFailed(
|
|||
|
|
taskId: taskId,
|
|||
|
|
code: serverError ? UploadBridge.failServerError : UploadBridge.failNetwork,
|
|||
|
|
status: status
|
|||
|
|
)
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var next = entry
|
|||
|
|
next.executions += 1
|
|||
|
|
store.upsert(taskId: taskId, entry: next)
|
|||
|
|
let delay =
|
|||
|
|
UploadPlugin.retryBackoffBaseSeconds * pow(2, Double(entry.executions - 1))
|
|||
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|||
|
|
self?.recreate(taskId: taskId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 重试重建(退避到期;期间被取消/重注册则 entry 已变——按 URL 等字段重建)。
|
|||
|
|
private func recreate(taskId: String) {
|
|||
|
|
guard let entry = store.entry(taskId: taskId) else { return }
|
|||
|
|
let spec = UploadRegisterSpec(
|
|||
|
|
taskId: taskId,
|
|||
|
|
url: entry.url,
|
|||
|
|
method: entry.method,
|
|||
|
|
headers: entry.headers,
|
|||
|
|
filePath: entry.filePath,
|
|||
|
|
uploadId: entry.uploadId,
|
|||
|
|
attempt: entry.attempt
|
|||
|
|
)
|
|||
|
|
do {
|
|||
|
|
try startTask(spec: spec, executions: entry.executions)
|
|||
|
|
} catch {
|
|||
|
|
bus.emitFailed(taskId: taskId, code: UploadBridge.failNetwork, status: nil)
|
|||
|
|
store.remove(taskId: taskId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// delegate 持有任务引用(重挂/取消用)。
|
|||
|
|
fileprivate func trackActiveTask(_ task: URLSessionTask) {
|
|||
|
|
guard let taskId = store.taskId(sessionTaskIdentifier: task.taskIdentifier) else { return }
|
|||
|
|
store.mutate(taskId: taskId) { entry in
|
|||
|
|
entry.sessionTaskIdentifier = task.taskIdentifier
|
|||
|
|
}
|
|||
|
|
lock.lock()
|
|||
|
|
activeTasks[task.taskIdentifier] = task
|
|||
|
|
lock.unlock()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── EventChannel 流(终态缓存重放见 UploadEventBus) ────────────────────
|
|||
|
|
|
|||
|
|
/// EventChannel `photolive/uploads/events`:onListen 接管总线 sink(事件
|
|||
|
|
/// 统一主线程投递,镜像 Kotlin mainHandler.post);onCancel 摘除。
|
|||
|
|
private final class UploadEventStreamHandler: NSObject, FlutterStreamHandler {
|
|||
|
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
|||
|
|
-> FlutterError?
|
|||
|
|
{
|
|||
|
|
UploadEventBus.shared.attach { event in
|
|||
|
|
DispatchQueue.main.async {
|
|||
|
|
events(event)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|||
|
|
UploadEventBus.shared.detach()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── URLSession delegate(系统回调 → 插件分类) ──────────────────────────
|
|||
|
|
|
|||
|
|
/// 后台会话 delegate(独立对象 + weak 回指,避免 session↔plugin 环)。
|
|||
|
|
/// 回调在系统串行队列;所有插件入口自带锁。
|
|||
|
|
private final class UploadSessionDelegate: NSObject, URLSessionDataDelegate {
|
|||
|
|
weak var plugin: UploadPlugin?
|
|||
|
|
|
|||
|
|
init(plugin: UploadPlugin) {
|
|||
|
|
self.plugin = plugin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func urlSession(
|
|||
|
|
_ session: URLSession,
|
|||
|
|
task: URLSessionTask,
|
|||
|
|
didSendBodyData bytesSent: Int64,
|
|||
|
|
totalBytesSent: Int64,
|
|||
|
|
totalBytesExpectedToSend: Int64
|
|||
|
|
) {
|
|||
|
|
guard let plugin = plugin,
|
|||
|
|
let taskId = plugin.taskId(forSessionTask: task.taskIdentifier)
|
|||
|
|
else { return }
|
|||
|
|
plugin.trackActiveTask(task)
|
|||
|
|
if plugin.shouldEmitProgress(
|
|||
|
|
taskId: taskId,
|
|||
|
|
sent: totalBytesSent,
|
|||
|
|
total: totalBytesExpectedToSend
|
|||
|
|
) {
|
|||
|
|
UploadEventBus.shared.emitProgress(
|
|||
|
|
taskId: taskId,
|
|||
|
|
sent: totalBytesSent,
|
|||
|
|
total: totalBytesExpectedToSend
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 上传任务本质是 data task:响应体(MinIO XML/空)读取后丢弃,防连接滞留。
|
|||
|
|
func urlSession(
|
|||
|
|
_ session: URLSession,
|
|||
|
|
dataTask: URLSessionDataTask,
|
|||
|
|
didReceive data: Data,
|
|||
|
|
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|||
|
|
) {
|
|||
|
|
completionHandler(.discard)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func urlSession(
|
|||
|
|
_ session: URLSession,
|
|||
|
|
task: URLSessionTask,
|
|||
|
|
didCompleteWithError error: Error?
|
|||
|
|
) {
|
|||
|
|
guard let plugin = plugin,
|
|||
|
|
let taskId = plugin.taskId(forSessionTask: task.taskIdentifier)
|
|||
|
|
else { return }
|
|||
|
|
plugin.handleTaskCompletion(
|
|||
|
|
taskId: taskId,
|
|||
|
|
identifier: task.taskIdentifier,
|
|||
|
|
response: task.response as? NSHTTPURLResponse,
|
|||
|
|
error: error
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
|||
|
|
plugin?.finishBackgroundEvents()
|
|||
|
|
}
|
|||
|
|
}
|