import 'dart:io'; import 'dart:math'; import 'package:dio/dio.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; const _show_debug_info = true; /// 下载状态枚举 enum DownloadStatus { downloading, completed, failed, } /// 下载结果 class DownloadResult { final DownloadStatus status; final String? filePath; final double? progress; final int? errorCode; final String? errorReason; bool get isCompleted => status == DownloadStatus.completed; bool get isDownloading => status == DownloadStatus.downloading; bool get isFailed => status == DownloadStatus.failed; /// ----- error code ----- static const int errorCodeError = -1; static const int errorCodePermissionDenied = -2; /// ----- constructor ----- DownloadResult({ required this.status, this.filePath, this.progress, this.errorCode, this.errorReason, }); factory DownloadResult.downloading(double progress) { if (progress < 0) { progress = 0; } else if (progress > 1) { progress = 1; } return DownloadResult( status: DownloadStatus.downloading, progress: progress, ); } factory DownloadResult.failed({ int errorCode = errorCodeError, String? errorReason, }) { return DownloadResult( status: DownloadStatus.failed, errorCode: errorCode, errorReason: errorReason, ); } factory DownloadResult.completed({ required String filePath, }) { return DownloadResult( status: DownloadStatus.completed, filePath: filePath, progress: 1.0, ); } } class HttpDownloader { //下载 static Future download({ required String fileUrl, String? fileName, void Function(DownloadResult)? onProgress, }) async { // 请求存储权限 PermissionStatus status = await Permission.storage.request(); if (!status.isGranted) { final result = DownloadResult.failed( errorCode: DownloadResult.errorCodePermissionDenied, errorReason: 'no storage permission', ); if (onProgress != null) { onProgress(result); } return result; } // 获取下载目录 final saveDir = await getDownloadDirectory(); // 处理保存文件名 fileName ??= await processFileName(fileUrl, fileName); final savePath = '$saveDir/$fileName'; try { final dio = Dio(); // 发送开始下载状态 if (_show_debug_info) { print('┌────────────────────────────────────────────────────────────────────────────────────────'); print('│ ⬇️ start download: $fileUrl'); print('│ save path: $fileName'); print('└────────────────────────────────────────────────────────────────────────────────────────'); } if (onProgress != null) { onProgress(DownloadResult.downloading(0.0)); } await dio.download( fileUrl, savePath, onReceiveProgress: (received, total) { if (_show_debug_info) { print('$fileName receive: $received/$total'); } if (total != -1) { if (onProgress != null) { onProgress(DownloadResult.downloading(received / total)); } } }, ); // 发送完成状态 if (_show_debug_info) { print('文件下载完成: $savePath'); } final result = DownloadResult.completed(filePath: savePath); if (onProgress != null) { onProgress(result); } return result; } catch (e) { print('下载失败: $e'); // 发送失败状态 final result = DownloadResult.failed( errorReason: e.toString(), ); if (onProgress != null) { onProgress(result); } return result; } } // 文件存储路径 static Future getDownloadDirectory() async { Directory directory; if (Platform.isAndroid) { // Android: 外部存储目录(对应 RNFS.ExternalDirectoryPath) directory = await getExternalStorageDirectory() ?? await getApplicationDocumentsDirectory(); } else if (Platform.isIOS) { // iOS: Library 目录 directory = await getLibraryDirectory(); } else { // 其他平台使用文档目录 directory = await getApplicationDocumentsDirectory(); } // 创建 dl_cache 子目录 final cacheDir = Directory(path.join(directory.path, 'cache')); if (!await cacheDir.exists()) { await cacheDir.create(recursive: true); } return cacheDir.path; } //验证文件名 static String getValidFileName(String name) { // 替换无效字符 return name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); } // 获取文件名 static Future processFileName(String url, String? customName) async { String fileName; if (customName != null) { fileName = getValidFileName(customName); } else { // 尝试从URL获取 final uri = Uri.parse(url); fileName = uri.pathSegments.lastOrNull ?? _randomFileName(); // 如果没有扩展名,尝试添加 if (!fileName.contains('.')) { final mimeType = await getMimeTypeAccurate(url); // 需要网络请求获取Content-Type if (mimeType != null) { final ext = mimeType.split('/').last; fileName = '$fileName.$ext'; } } } return fileName; } static String _randomFileName() { const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; final random = Random.secure(); final result = StringBuffer(); for (int i = 0; i < 16; i++) { final index = random.nextInt(charset.length); result.write(charset[index]); } return '${result.toString()}${DateTime.now().millisecondsSinceEpoch}'; } // 或者结合网络请求获取更准确的结果 static Future getMimeTypeAccurate(String url) async { // 先尝试从扩展名获取 final fromExtension = lookupMimeType(url); // 如果无法确定或想验证,可以发送HEAD请求 try { final response = await Dio().head(url); final fromHeader = response.headers .value('content-type') ?.split(';') .first; return fromHeader ?? fromExtension; } catch (e) { return fromExtension; // 回退到扩展名推断 } } }