James 1 روز پیش
والد
کامیت
e710c86c76
5فایلهای تغییر یافته به همراه539 افزوده شده و 48 حذف شده
  1. 25 47
      lib/utils/fileUtils.dart
  2. 248 0
      lib/utils/httpDownloader.dart
  3. 165 0
      lib/utils/httpUploader.dart
  4. 97 1
      pubspec.lock
  5. 4 0
      pubspec.yaml

+ 25 - 47
lib/utils/fileUtils.dart

@@ -10,33 +10,40 @@ import 'logger.dart';
 
 
 
-String fileOrDirectoryName(FileSystemEntity directory) {
-  return lastNameOfPath(directory.path);
+bool isDirectory(String filePath) {
+  return FileSystemEntity.isDirectorySync(filePath);
 }
 
-String lastNameOfPath(path) {
-  final paths = path.split(Platform.pathSeparator);
-  return paths.last;
+Future<bool> isDirectoryExists(String filePath) {
+  return Directory(filePath).exists();
 }
 
-bool isDirectory(path) {
-  return FileSystemEntity.isDirectorySync(path);
+bool isFile(String filePath) {
+  return FileSystemEntity.isFileSync(filePath);
 }
 
-Future<bool> isDirectoryExists(path) {
-  return Directory(path).exists();
+String getDirectoryName(String filePath) {
+  return path.dirname(filePath);
 }
 
-Future<bool> isFileExists(path) {
-  return File(path).exists();
+Future<bool> isFileExists(String filePath) {
+  return File(filePath).exists();
 }
 
-bool isFile(path) {
-  return FileSystemEntity.isFileSync(path);
+String getFileName(String filePath) {
+  return path.basename(filePath);
 }
 
-Future<int> fileSizeOf(path) async {
-  final file = File(path);
+String getFileNameWithoutExtension(String filePath) {
+  return path.basenameWithoutExtension(filePath);
+}
+
+String getFileExtension(String filePath, {int level = 1}) {
+  return path.extension(filePath, level);
+}
+
+Future<int> fileSizeOf(filePath) async {
+  final file = File(filePath);
   final size = await file.length();
   return size;
 }
@@ -45,7 +52,7 @@ String formatFileSize(int bytes) {
   if (bytes <= 0) return "0 B";
   const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
   var i = (log(bytes) / log(1024)).floor();
-  return ((bytes / pow(1024, i)).toStringAsFixed(2)) + ' ' + suffixes[i];
+  return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${suffixes[i]}';
 }
 
 // MD5
@@ -123,38 +130,9 @@ clearDirectory(Directory directory) async {
   }
 }
 
-deleteDirectory(String path) async {
-  final directory = Directory(path);
+deleteDirectory(String dirPath) async {
+  final directory = Directory(dirPath);
   await directory.delete(recursive: true);
 }
 
 
-
-Future<String?> getSaveLocation({required String suggestedName, String? iOSAppDirectory}) async {
-  if (Platform.isIOS) {
-    final docDir = await getApplicationDocumentsDirectory();
-    String path = docDir.path;
-    if (iOSAppDirectory != null) {
-      path = '${docDir.path}/$iOSAppDirectory';
-    }
-    Directory dir = Directory(path);
-    // 检查文件夹是否已经存在
-    bool exists = await dir.exists();
-    if (!exists) {
-      // 如果文件夹不存在,则创建它
-      await dir.create(recursive: true); // 设置 recursive 为 true 可以创建多级目录
-    }
-    final finalPath = File('${dir.path}/$suggestedName').path;
-    // Log.d('iOS get file path: $finalPath');
-    return finalPath;
-
-  } else {
-    final file_selector.FileSaveLocation? selectedFile = await file_selector.getSaveLocation(
-      suggestedName: suggestedName,
-    );
-    if (selectedFile != null) {
-      return selectedFile.path;
-    }
-  }
-  return null;
-}

+ 248 - 0
lib/utils/httpDownloader.dart

@@ -0,0 +1,248 @@
+
+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; // 回退到扩展名推断
+    }
+  }
+
+}
+

+ 165 - 0
lib/utils/httpUploader.dart

@@ -0,0 +1,165 @@
+
+import 'package:dio/dio.dart';
+import 'package:file_picker/file_picker.dart';
+import 'fileUtils.dart';
+
+
+const _show_debug_info = true;
+
+
+
+/// 上传状态枚举
+enum UploadStatus {
+  uploading,
+  completed,
+  failed,
+}
+
+/// 上传结果
+class UploadResult {
+  final UploadStatus status;
+  final String? filePath;
+  final double? progress;
+  final int? errorCode;
+  final String? errorReason;
+
+  bool get isCompleted => status == UploadStatus.completed;
+  bool get isUploading => status == UploadStatus.uploading;
+  bool get isFailed => status == UploadStatus.failed;
+
+
+  /// ----- error code -----
+  static const int errorCodeError = -1;
+  static const int errorCodeUserCancel = 1;
+  static const int errorCodeFileIsNotExist = -2;
+
+
+  /// ----- constructor -----
+  UploadResult({
+    required this.status,
+    this.filePath,
+    this.progress,
+    this.errorCode,
+    this.errorReason,
+  });
+
+  factory UploadResult.uploading(double progress) {
+    if (progress < 0) { progress = 0; }
+    else if (progress > 1) { progress = 1; }
+    return UploadResult(
+      status: UploadStatus.uploading,
+      progress: progress,
+    );
+  }
+
+  factory UploadResult.failed({
+    int errorCode = errorCodeError,
+    String? errorReason,
+  }) {
+    return UploadResult(
+      status: UploadStatus.failed,
+      errorCode: errorCode,
+      errorReason: errorReason,
+    );
+  }
+
+  factory UploadResult.completed({
+    required String filePath,
+  }) {
+    return UploadResult(
+      status: UploadStatus.completed,
+      filePath: filePath,
+      progress: 1.0,
+    );
+  }
+}
+
+
+class HttpDownloader {
+
+  //上传
+  static Future<UploadResult> upload({
+    required String receiverUrl,
+    String? filePath,
+    void Function(UploadResult)? onProgress,
+  }) async {
+
+    // 选择文件
+    String selectedFilePath;
+    if (filePath != null) {
+      if (await isFileExists(filePath)) {
+        selectedFilePath = filePath;
+      } else {
+        final result = UploadResult.failed(
+          errorCode: UploadResult.errorCodeFileIsNotExist,
+          errorReason: 'file is not exist',
+        );
+        if (onProgress != null) {
+          onProgress(result);
+        }
+        return result;
+      }
+    } else {
+      FilePickerResult? result = await FilePicker.platform.pickFiles();
+      if (result == null) {
+        final result = UploadResult.failed(
+          errorCode: UploadResult.errorCodeUserCancel,
+          errorReason: 'Selection canceled by user',
+        );
+        if (onProgress != null) {
+          onProgress(result);
+        }
+        return result;
+      }
+      PlatformFile file = result.files.first;
+      selectedFilePath = file.path!;
+    }
+
+    FormData formData = FormData.fromMap({
+      "file": await MultipartFile.fromFile(selectedFilePath),
+    });
+
+    final dio = Dio();
+    try {
+
+      if (_show_debug_info) {
+        print('┌────────────────────────────────────────────────────────────────────────────────────────');
+        print('│ ⬆️ start upload file: $selectedFilePath');
+        print('└────────────────────────────────────────────────────────────────────────────────────────');
+      }
+
+      final response = await dio.post(
+        receiverUrl,
+        data: formData,
+        onSendProgress: (sent, total) {
+          if (_show_debug_info) {
+            print('upload: $sent / $total');
+          }
+          if (onProgress != null) {
+            onProgress(UploadResult.uploading(sent / (total * 100)));
+          }
+        },
+      );
+
+      if (_show_debug_info) {
+        print('$filePath 上传完成: $response.data');
+      }
+      final result = UploadResult.completed(filePath: selectedFilePath);
+      if (onProgress != null) {
+        onProgress(result);
+      }
+      return result;
+
+    } catch (e) {
+      print('上传失败: $e');
+      final result = UploadResult.failed(
+        errorReason: e.toString(),
+      );
+      if (onProgress != null) {
+        onProgress(result);
+      }
+      return result;
+    }
+  }
+}
+

+ 97 - 1
pubspec.lock

@@ -241,6 +241,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.6"
+  dbus:
+    dependency: transitive
+    description:
+      name: dbus
+      sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.11"
   device_info_plus:
     dependency: "direct main"
     description:
@@ -257,6 +265,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.3"
+  dio:
+    dependency: "direct main"
+    description:
+      name: dio
+      sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.9.0"
+  dio_web_adapter:
+    dependency: transitive
+    description:
+      name: dio_web_adapter
+      sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
   encrypt:
     dependency: "direct main"
     description:
@@ -281,6 +305,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.1"
+  file_picker:
+    dependency: "direct main"
+    description:
+      name: file_picker
+      sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde
+      url: "https://pub.dev"
+    source: hosted
+    version: "10.3.8"
   file_selector:
     dependency: "direct main"
     description:
@@ -612,7 +644,7 @@ packages:
     source: hosted
     version: "1.16.0"
   mime:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: mime
       sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
@@ -707,6 +739,62 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.0"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
+      url: "https://pub.dev"
+    source: hosted
+    version: "12.0.1"
+  permission_handler_android:
+    dependency: transitive
+    description:
+      name: permission_handler_android
+      sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "13.0.1"
+  permission_handler_apple:
+    dependency: transitive
+    description:
+      name: permission_handler_apple
+      sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.4.7"
+  permission_handler_html:
+    dependency: transitive
+    description:
+      name: permission_handler_html
+      sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.3+5"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.0"
+  permission_handler_windows:
+    dependency: transitive
+    description:
+      name: permission_handler_windows
+      sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.1"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
   platform:
     dependency: transitive
     description:
@@ -1064,6 +1152,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.6.1"
   yaml:
     dependency: transitive
     description:

+ 4 - 0
pubspec.yaml

@@ -30,6 +30,10 @@ dependencies:
   local_auth: ^3.0.0
   device_info_plus: ^10.1.2
   uuid: ^4.5.2
+  permission_handler: ^12.0.1
+  dio: ^5.9.0
+  mime: ^2.0.0
+  file_picker: ^10.3.8
 
 dev_dependencies:
   build_runner: ^2.4.8