import 'dart:typed_data'; import 'package:dio/dio.dart'; /// 预签名直传失败。 /// /// - [statusCode] 为 null 表示网络层失败(断连/超时),可原凭据重试; /// - 403 为存储侧拒绝签名(凭据过期/被改动),须重新 createUpload 换新凭据; /// - 其余状态码按可重试处理(重试走完整重传)。 class MediaDirectUploadException implements Exception { const MediaDirectUploadException({this.statusCode, required this.message}); final int? statusCode; final String message; /// 存储侧拒绝了凭据(签名过期/不符),重试前必须换新凭据。 bool get isCredentialRejected => statusCode == 403; @override String toString() => 'MediaDirectUploadException($statusCode): $message'; } /// 预签名 PUT 直传抽象(两步上传第二步的传输层)。 /// /// 不走业务信封、不带 Bearer 鉴权——鉴权就是 URL 里的签名本身; /// [headers] 即 createUpload 返回的 requiredHeaders,必须原样携带 /// (Content-Type 已签进签名,改动即 403)。 abstract class MediaDirectUploadClient { Future put({ required String url, required Map headers, required Uint8List bytes, void Function(int sent, int total)? onProgress, }); } /// 基于裸 Dio 的实现:独立实例,无 AuthInterceptor(预签名 URL 携带 /// Authorization 头会破坏 SigV4 校验),sendTimeout 按 10 MiB 弱网上限放宽。 class DioMediaDirectUploadClient implements MediaDirectUploadClient { DioMediaDirectUploadClient({Dio? dio}) : _dio = dio ?? Dio( BaseOptions( connectTimeout: const Duration(seconds: 5), sendTimeout: const Duration(seconds: 120), receiveTimeout: const Duration(seconds: 30), validateStatus: (_) => true, ), ); final Dio _dio; @override Future put({ required String url, required Map headers, required Uint8List bytes, void Function(int sent, int total)? onProgress, }) async { final Response response; try { response = await _dio.put( url, data: Stream.value(bytes), options: Options( headers: {...headers, Headers.contentLengthHeader: bytes.length}, ), onSendProgress: onProgress, ); } on DioException catch (error) { throw MediaDirectUploadException(message: '直传网络失败:${error.type.name}'); } final status = response.statusCode ?? 0; if (status < 200 || status >= 300) { throw MediaDirectUploadException( statusCode: status, message: '存储侧拒绝直传:HTTP $status', ); } } }