| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 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<DownloadResult> 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<String> 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<String> 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<String?> 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; // 回退到扩展名推断
- }
- }
- }
|