httpDownloader.dart 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:dio/dio.dart';
  4. import 'package:mime/mime.dart';
  5. import 'package:path/path.dart' as path;
  6. import 'package:path_provider/path_provider.dart';
  7. import 'package:permission_handler/permission_handler.dart';
  8. const _show_debug_info = true;
  9. /// 下载状态枚举
  10. enum DownloadStatus {
  11. downloading,
  12. completed,
  13. failed,
  14. }
  15. /// 下载结果
  16. class DownloadResult {
  17. final DownloadStatus status;
  18. final String? filePath;
  19. final double? progress;
  20. final int? errorCode;
  21. final String? errorReason;
  22. bool get isCompleted => status == DownloadStatus.completed;
  23. bool get isDownloading => status == DownloadStatus.downloading;
  24. bool get isFailed => status == DownloadStatus.failed;
  25. /// ----- error code -----
  26. static const int errorCodeError = -1;
  27. static const int errorCodePermissionDenied = -2;
  28. /// ----- constructor -----
  29. DownloadResult({
  30. required this.status,
  31. this.filePath,
  32. this.progress,
  33. this.errorCode,
  34. this.errorReason,
  35. });
  36. factory DownloadResult.downloading(double progress) {
  37. if (progress < 0) { progress = 0; }
  38. else if (progress > 1) { progress = 1; }
  39. return DownloadResult(
  40. status: DownloadStatus.downloading,
  41. progress: progress,
  42. );
  43. }
  44. factory DownloadResult.failed({
  45. int errorCode = errorCodeError,
  46. String? errorReason,
  47. }) {
  48. return DownloadResult(
  49. status: DownloadStatus.failed,
  50. errorCode: errorCode,
  51. errorReason: errorReason,
  52. );
  53. }
  54. factory DownloadResult.completed({
  55. required String filePath,
  56. }) {
  57. return DownloadResult(
  58. status: DownloadStatus.completed,
  59. filePath: filePath,
  60. progress: 1.0,
  61. );
  62. }
  63. }
  64. class HttpDownloader {
  65. //下载
  66. static Future<DownloadResult> download({
  67. required String fileUrl,
  68. String? fileName,
  69. void Function(DownloadResult)? onProgress,
  70. }) async {
  71. // 请求存储权限
  72. PermissionStatus status = await Permission.storage.request();
  73. if (!status.isGranted) {
  74. final result = DownloadResult.failed(
  75. errorCode: DownloadResult.errorCodePermissionDenied,
  76. errorReason: 'no storage permission',
  77. );
  78. if (onProgress != null) {
  79. onProgress(result);
  80. }
  81. return result;
  82. }
  83. // 获取下载目录
  84. final saveDir = await getDownloadDirectory();
  85. // 处理保存文件名
  86. fileName ??= await processFileName(fileUrl, fileName);
  87. final savePath = '$saveDir/$fileName';
  88. try {
  89. final dio = Dio();
  90. // 发送开始下载状态
  91. if (_show_debug_info) {
  92. print('┌────────────────────────────────────────────────────────────────────────────────────────');
  93. print('│ ⬇️ start download: $fileUrl');
  94. print('│ save path: $fileName');
  95. print('└────────────────────────────────────────────────────────────────────────────────────────');
  96. }
  97. if (onProgress != null) {
  98. onProgress(DownloadResult.downloading(0.0));
  99. }
  100. await dio.download(
  101. fileUrl,
  102. savePath,
  103. onReceiveProgress: (received, total) {
  104. if (_show_debug_info) {
  105. print('$fileName receive: $received/$total');
  106. }
  107. if (total != -1) {
  108. if (onProgress != null) {
  109. onProgress(DownloadResult.downloading(received / total));
  110. }
  111. }
  112. },
  113. );
  114. // 发送完成状态
  115. if (_show_debug_info) {
  116. print('文件下载完成: $savePath');
  117. }
  118. final result = DownloadResult.completed(filePath: savePath);
  119. if (onProgress != null) {
  120. onProgress(result);
  121. }
  122. return result;
  123. } catch (e) {
  124. print('下载失败: $e');
  125. // 发送失败状态
  126. final result = DownloadResult.failed(
  127. errorReason: e.toString(),
  128. );
  129. if (onProgress != null) {
  130. onProgress(result);
  131. }
  132. return result;
  133. }
  134. }
  135. // 文件存储路径
  136. static Future<String> getDownloadDirectory() async {
  137. Directory directory;
  138. if (Platform.isAndroid) {
  139. // Android: 外部存储目录(对应 RNFS.ExternalDirectoryPath)
  140. directory = await getExternalStorageDirectory() ??
  141. await getApplicationDocumentsDirectory();
  142. } else if (Platform.isIOS) {
  143. // iOS: Library 目录
  144. directory = await getLibraryDirectory();
  145. } else {
  146. // 其他平台使用文档目录
  147. directory = await getApplicationDocumentsDirectory();
  148. }
  149. // 创建 dl_cache 子目录
  150. final cacheDir = Directory(path.join(directory.path, 'cache'));
  151. if (!await cacheDir.exists()) {
  152. await cacheDir.create(recursive: true);
  153. }
  154. return cacheDir.path;
  155. }
  156. //验证文件名
  157. static String getValidFileName(String name) {
  158. // 替换无效字符
  159. return name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
  160. }
  161. // 获取文件名
  162. static Future<String> processFileName(String url, String? customName) async {
  163. String fileName;
  164. if (customName != null) {
  165. fileName = getValidFileName(customName);
  166. } else {
  167. // 尝试从URL获取
  168. final uri = Uri.parse(url);
  169. fileName = uri.pathSegments.lastOrNull ?? _randomFileName();
  170. // 如果没有扩展名,尝试添加
  171. if (!fileName.contains('.')) {
  172. final mimeType = await getMimeTypeAccurate(url); // 需要网络请求获取Content-Type
  173. if (mimeType != null) {
  174. final ext = mimeType.split('/').last;
  175. fileName = '$fileName.$ext';
  176. }
  177. }
  178. }
  179. return fileName;
  180. }
  181. static String _randomFileName() {
  182. const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  183. final random = Random.secure();
  184. final result = StringBuffer();
  185. for (int i = 0; i < 16; i++) {
  186. final index = random.nextInt(charset.length);
  187. result.write(charset[index]);
  188. }
  189. return '${result.toString()}${DateTime.now().millisecondsSinceEpoch}';
  190. }
  191. // 或者结合网络请求获取更准确的结果
  192. static Future<String?> getMimeTypeAccurate(String url) async {
  193. // 先尝试从扩展名获取
  194. final fromExtension = lookupMimeType(url);
  195. // 如果无法确定或想验证,可以发送HEAD请求
  196. try {
  197. final response = await Dio().head(url);
  198. final fromHeader = response.headers
  199. .value('content-type')
  200. ?.split(';')
  201. .first;
  202. return fromHeader ?? fromExtension;
  203. } catch (e) {
  204. return fromExtension; // 回退到扩展名推断
  205. }
  206. }
  207. }