/// 上传传输抽象与双实现(P3 计划 Task 7;Task 8 扩展 iOS 选择分支)。 /// /// 引擎的传输步骤(presign PUT)经 [Transport] 接口委托: /// - **[DartHttpTransport]**(Task 6 逻辑原样抽出):裸 dio 流式 PUT + /// 进度计数流 + 取消令牌——桌面 / 调试路径(dart-define /// `FORCE_DART_TRANSPORT=true` 强制启用); /// - **[NativeTransport]**(Task 7 新增,Android;Task 8 起含 iOS): /// MethodChannel `photolive/uploads` 注册原生后台上传——Android = /// WorkManager 后台工作(expedited + dataSync 前台服务);iOS = /// URLSession background(identifier com.photolive.photo_live.uploads, /// 编译留 G3)。两平台协议逐字一致(Kotlin `UploadProtocol.kt` / /// Swift `UploadBridge.swift`),EventChannel `photolive/uploads/events` /// 回流 progress/completed/failed/needsRenew 事件——App 切后台/锁屏后 /// 续传由系统调度承担。 /// /// 失败语义(两实现归一为 [TransportException] 族,引擎分类见 /// upload_engine.dart `_classifyFailure`): /// | 情形 | 异常 | /// |---|---| /// | 网络不可达/超时/取消令牌触发前的连接层失败 | `TransportException(network)` | /// | HTTP 403(presign 凭证过期) | [TransportRenewRequiredException](引擎 renew 后重发) | /// | 其余 HTTP 非 2xx | `TransportException(http, statusCode)` | /// | 源文件缺失(原生 worker 发现) | `TransportException(sourceMissing)` | /// | 平台通道调用失败 | `TransportException(channel)` | /// | 取消令牌触发 | `TransportException(cancelled)`(引擎静默收敛) | library; import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart'; /// presign PUT 传输接口(引擎只委托文件 PUT;init/complete/renew 仍在 /// Dart 引擎内,保持服务端交互语义单源)。 abstract interface class Transport { /// 单次文件 PUT。 /// /// - 成功返回 [TransportResult](ETag 供诊断;完成核验以服务端为准); /// - 失败抛 [TransportException] 族(见库级注释分类表); /// - [cancelToken] 取消时抛 `TransportException(cancelled)`。 Future send({ required String taskId, required String url, required String method, required Map headers, required String filePath, Map? metadata, void Function(int sent, int total)? onProgress, CancelToken? cancelToken, }); /// 释放底层资源(引擎 dispose 调用;注入方自持传输时自行管理)。 void dispose(); } /// PUT 成功结果。 class TransportResult { const TransportResult({this.etag}); /// 服务端返回的 ETag 响应头(可空;仅诊断用途)。 final String? etag; } /// 传输失败码(原生侧事件码与本侧异常码的归一层)。 abstract final class TransportErrorCodes { /// 网络不可达/超时/连接层失败。 static const String network = 'NETWORK'; /// HTTP 层失败(statusCode 保留在异常上)。 static const String http = 'HTTP'; /// 取消令牌触发(非失败分类;引擎静默收敛)。 static const String cancelled = 'CANCELLED'; /// 源文件缺失(受管副本被清理,不可恢复)。 static const String sourceMissing = 'SOURCE_MISSING'; /// 平台通道调用失败(桥接层异常)。 static const String channel = 'CHANNEL'; } /// 传输失败(两实现统一的异常类型)。 class TransportException implements Exception { const TransportException(this.code, {this.statusCode, this.cause}); /// 失败分类([TransportErrorCodes])。 final String code; /// HTTP 状态码(http 类失败携带)。 final int? statusCode; /// 底层异常(诊断)。 final Object? cause; @override String toString() => 'TransportException($code${statusCode != null ? ', status=$statusCode' : ''}' '${cause != null ? ', cause=$cause' : ''})'; } /// HTTP 403(presign 凭证过期):引擎 renewUpload 换新凭证后重发。 class TransportRenewRequiredException extends TransportException { const TransportRenewRequiredException({super.cause}) : super(TransportErrorCodes.http, statusCode: 403); } // ── Dart(dio)实现 ────────────────────────────────────────────────────── /// Task 6 的裸 dio 直传实现(iOS / 桌面 / FORCE_DART_TRANSPORT 调试)。 /// /// 严格携带 credential.headers(+ Content-Length——流式发送 dio 无法自动 /// 推断,显式声明);进度经计数流上报(不依赖适配器对 onSendProgress 的 /// 实现)。 class DartHttpTransport implements Transport { DartHttpTransport({Dio? dio}) : _dio = dio, _ownedDio = dio == null; final Dio? _dio; final bool _ownedDio; /// 自建实例惰性创建(注入 dio 时不建)。 Dio? _created; Dio get _client => _dio ?? (_created ??= Dio()); @override Future send({ required String taskId, required String url, required String method, required Map headers, required String filePath, Map? metadata, void Function(int sent, int total)? onProgress, CancelToken? cancelToken, }) async { final file = File(filePath); final length = await file.length(); var sent = 0; final progressStream = file.openRead().map((List chunk) { sent += chunk.length; onProgress?.call(sent, length); return chunk; }); try { final resp = await _client.put( url, data: progressStream, cancelToken: cancelToken, options: Options( method: method, headers: { ...headers, Headers.contentLengthHeader: length, }, // 响应体非 JSON(MinIO XML/空),防 transformer 解析报错。 responseType: ResponseType.plain, receiveDataWhenStatusError: true, ), ); final etag = resp.headers.value(HttpHeaders.etagHeader); return TransportResult(etag: etag); } on DioException catch (e) { throw _mapDioException(e); } } /// dio 异常 → 传输异常归一(403 → renew;连接层 → network;取消 → /// cancelled;其余 badResponse → http)。 static TransportException _mapDioException(DioException e) { if (e.type == DioExceptionType.cancel) { return const TransportException(TransportErrorCodes.cancelled); } final status = e.response?.statusCode; if (status != null) { if (status == 403) return const TransportRenewRequiredException(); return TransportException(TransportErrorCodes.http, statusCode: status, cause: e); } if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.transformTimeout || e.type == DioExceptionType.connectionError || e.type == DioExceptionType.badCertificate || e.type == DioExceptionType.unknown) { return TransportException(TransportErrorCodes.network, cause: e); } // badResponse 无响应必经上方分支,防御性兜底为网络类。 return TransportException(TransportErrorCodes.network, cause: e); } @override void dispose() { if (_ownedDio) { _client.close(); } } } // ── 原生(Android WorkManager / iOS URLSession background)实现 ───────── /// 平台通道名(与 Android `UploadPlugin.kt` / iOS `UploadPlugin.swift` /// 一致;原生常量见 UploadProtocol.kt 与 UploadBridge.swift——Task 8 起 /// iOS 为同一协议的 Swift 镜像,无协议分歧)。 abstract final class UploadBridgeChannels { /// 命令通道:registerTask / cancelTask / queryTask / ackEvents。 static const String methods = 'photolive/uploads'; /// 事件流:progress / completed / failed / needsRenew(带 seq, /// 终态事件未 ack 前原生侧缓存重放——生命周期安全)。 static const String events = 'photolive/uploads/events'; } /// 平台通道绑定薄层(默认真实通道;测试注入替身)。 class NativeUploadChannel { const NativeUploadChannel(); static const MethodChannel _methods = MethodChannel( UploadBridgeChannels.methods, ); static const EventChannel _events = EventChannel( UploadBridgeChannels.events, ); Future invoke(String method, Map args) => _methods.invokeMethod(method, args); Stream get events => _events.receiveBroadcastStream(); } /// 原生事件等待句柄。 class _NativeWaiter { _NativeWaiter(this.onProgress); final void Function(int sent, int total)? onProgress; final Completer completer = Completer(); void complete(TransportResult result) { if (!completer.isCompleted) completer.complete(result); } void completeError(Object error) { if (!completer.isCompleted) completer.completeError(error); } } /// Android/iOS 原生传输:registerTask 入队原生后台上传(Android = /// WorkManager;iOS = URLSession background),事件流回流终态/进度。 /// /// 生命周期安全:事件带单调 seq;终态事件在 Dart ack 前由原生侧缓存, /// 重新 listen 时重放——进程重启/切后台恢复后不丢完成信号(iOS 侧缓存 /// 另落盘,系统后台拉起投递的终态在进程重启后同样重放)。进度事件不 /// 重放(瞬态)。 class NativeTransport implements Transport { NativeTransport({NativeUploadChannel? channel}) : _channel = channel ?? const NativeUploadChannel(); final NativeUploadChannel _channel; StreamSubscription? _eventsSub; final Map _waiters = {}; void _ensureSubscribed() { if (_eventsSub != null) return; _eventsSub = _channel.events.listen( _onEvent, onError: (Object _) { // 事件流出错:丢弃订阅,下次 send 重建(注册重试语义兜底)。 _eventsSub?.cancel(); _eventsSub = null; }, onDone: () => _eventsSub = null, ); } void _onEvent(dynamic raw) { if (raw is! Map) return; final event = Map.from(raw); final taskId = event['taskId'] as String?; final waiter = taskId == null ? null : _waiters[taskId]; switch (event['event']) { case 'progress': final sent = (event['sent'] as num?)?.toInt(); final total = (event['total'] as num?)?.toInt(); if (waiter != null && sent != null && total != null) { waiter.onProgress?.call(sent, total); } case 'completed': waiter?.complete( TransportResult(etag: event['etag'] as String?), ); _ackTerminal(event); case 'needsRenew': waiter?.completeError(const TransportRenewRequiredException()); _ackTerminal(event); case 'failed': waiter?.completeError(_mapFailedEvent(event)); _ackTerminal(event); default: break; // 未知事件:前向兼容忽略。 } } /// 原生失败码 → 传输异常。 static TransportException _mapFailedEvent(Map event) { final status = (event['status'] as num?)?.toInt(); switch (event['code']) { case 'NETWORK': return const TransportException(TransportErrorCodes.network); case 'SOURCE_MISSING': return const TransportException(TransportErrorCodes.sourceMissing); case 'HTTP_4XX': case 'SERVER_ERROR': return TransportException( TransportErrorCodes.http, statusCode: status, ); default: return TransportException( event['code'] as String? ?? TransportErrorCodes.channel, statusCode: status, ); } } void _ackTerminal(Map event) { final seq = (event['seq'] as num?)?.toInt(); if (seq == null) return; unawaited( _channel .invoke('ackEvents', {'seq': seq}) .catchError((Object _) => null), ); } @override Future send({ required String taskId, required String url, required String method, required Map headers, required String filePath, Map? metadata, void Function(int sent, int total)? onProgress, CancelToken? cancelToken, }) async { _ensureSubscribed(); final waiter = _NativeWaiter(onProgress); _waiters[taskId] = waiter; // 取消令牌联动:cancelTask(原生尽力停止 worker)+ 等待句柄立即失败。 cancelToken?.whenCancel.then((_) { // 已终结则 no-op(isCompleted 守护)。 waiter.completeError( const TransportException(TransportErrorCodes.cancelled), ); unawaited( _channel .invoke('cancelTask', {'taskId': taskId}) .catchError((Object _) => null), ); }); try { await _channel.invoke('registerTask', { 'taskId': taskId, 'url': url, 'method': method, 'headers': headers, 'filePath': filePath, 'metadata': metadata ?? const {}, }); } on PlatformException catch (e) { // 注册失败:摘除等待句柄后归一为通道异常。 if (_waiters[taskId] == waiter) _waiters.remove(taskId); throw TransportException(TransportErrorCodes.channel, cause: e); } try { return await waiter.completer.future; } finally { if (_waiters[taskId] == waiter) _waiters.remove(taskId); } } /// 查询原生侧工作状态(WorkInfo 概览;Task 9/11 校准用)。 Future?> queryTask(String taskId) async { try { final result = await _channel .invoke('queryTask', {'taskId': taskId}); return result == null ? null : Map.from(result as Map); } on PlatformException { return null; } } @override void dispose() { _eventsSub?.cancel(); _eventsSub = null; } } // ── 传输选择 ───────────────────────────────────────────────────────────── /// dart-define FORCE_DART_TRANSPORT=true:Android/iOS 也强制 Dart dio /// 路径(桥接层问题排查用)。 const bool kForceDartTransport = bool.fromEnvironment('FORCE_DART_TRANSPORT'); /// 传输选择纯函数(可测):Android 或 iOS 且未强制 Dart → 原生。 /// /// Task 8 起含 iOS:协议与 Android 逐字一致(同一 [NativeTransport] /// 经同一通道名驱动 Swift 侧 UploadPlugin——iOS 编译/真机验证留 G3, /// 代码路径在此之前即纳入原生选择)。 bool preferNativeTransport({ required bool isAndroid, required bool isIOS, bool forceDartTransport = kForceDartTransport, }) => (isAndroid || isIOS) && !forceDartTransport; /// 默认传输解析:Android/iOS → [NativeTransport](WorkManager / /// URLSession background 后台续传);其余平台 / 强制覆盖 → /// [DartHttpTransport]。 Transport defaultUploadTransport() { if (preferNativeTransport( isAndroid: !kIsWeb && Platform.isAndroid, isIOS: !kIsWeb && Platform.isIOS, )) { return NativeTransport(); } return DartHttpTransport(); }