You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
4.5 KiB
153 lines
4.5 KiB
import 'dart:io';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:dio/io.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:logger/web.dart';
|
|
|
|
/// 文件下载管理器
|
|
/// 负责处理固件包的下载、文件管理等功能
|
|
class DownloadManager {
|
|
late final Dio _dio;
|
|
|
|
/// 构造函数
|
|
/// 初始化Dio实例并配置SSL证书处理
|
|
DownloadManager() {
|
|
_dio = Dio(BaseOptions(
|
|
connectTimeout: const Duration(seconds: 30), // 增加连接超时时间
|
|
receiveTimeout: Duration.zero,
|
|
sendTimeout: const Duration(seconds: 30), // 增加发送超时时间
|
|
headers: {'Accept-Encoding': 'gzip'},
|
|
));
|
|
|
|
// 配置SSL证书验证
|
|
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
|
final client = HttpClient();
|
|
|
|
// 处理SSL证书验证
|
|
client.badCertificateCallback =
|
|
(X509Certificate cert, String host, int port) {
|
|
// 在生产环境中,应该进行适当的证书验证
|
|
// 这里为了解决证书过期问题,暂时跳过验证
|
|
// 建议:在生产环境中实现更安全的证书验证逻辑
|
|
Logger().w('SSL证书验证跳过: $host:$port');
|
|
return true;
|
|
};
|
|
|
|
return client;
|
|
};
|
|
}
|
|
|
|
/// 下载固件包
|
|
/// [downloadUrl] 下载地址
|
|
/// [onProgress] 下载进度回调
|
|
/// [onStatusUpdate] 状态更新回调
|
|
/// 返回下载文件的本地路径
|
|
/// 抛出异常如果下载失败
|
|
Future<String> downloadFirmware({
|
|
required String downloadUrl,
|
|
required Function(double) onProgress,
|
|
required Function(String) onStatusUpdate,
|
|
}) async {
|
|
onStatusUpdate('准备下载固件包...');
|
|
|
|
final fileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
|
|
const directoryName = "ota_upgrades";
|
|
|
|
// 获取下载目录
|
|
onStatusUpdate('正在获取下载目录...');
|
|
final Directory? downloadsDir = await getApplicationSupportDirectory();
|
|
if (downloadsDir == null) {
|
|
throw FileSystemException('无法访问下载目录');
|
|
}
|
|
|
|
final targetDir = Directory('${downloadsDir.path}/Download/$directoryName');
|
|
if (!await targetDir.exists()) {
|
|
onStatusUpdate('创建下载目录...');
|
|
await targetDir.create(recursive: true);
|
|
}
|
|
|
|
final savePath = '${targetDir.path}/$fileName';
|
|
|
|
// 检查是否已存在同名文件
|
|
onStatusUpdate('检查本地文件...');
|
|
if (await _checkExistingFile(targetDir, fileName)) {
|
|
onStatusUpdate('文件已存在,准备升级...');
|
|
return savePath;
|
|
}
|
|
|
|
// 清理旧文件
|
|
onStatusUpdate('清理旧文件...');
|
|
await _cleanOldFiles(targetDir);
|
|
|
|
// 开始下载
|
|
onStatusUpdate('开始下载固件包...');
|
|
Logger().d('文件保存路径: $savePath');
|
|
|
|
final options = Options(
|
|
receiveTimeout: Duration.zero,
|
|
followRedirects: true,
|
|
maxRedirects: 5,
|
|
);
|
|
|
|
await _dio.download(
|
|
downloadUrl,
|
|
savePath,
|
|
options: options,
|
|
onReceiveProgress: (received, total) {
|
|
if (total != -1) {
|
|
final progress = received / total;
|
|
onProgress(progress);
|
|
onStatusUpdate('正在下载固件包... ${(progress * 100).toStringAsFixed(1)}%');
|
|
}
|
|
},
|
|
);
|
|
|
|
// 验证文件是否下载成功
|
|
final file = File(savePath);
|
|
if (!await file.exists()) {
|
|
throw FileSystemException('文件下载失败,请检查写入权限');
|
|
}
|
|
|
|
onStatusUpdate('下载完成,准备升级...');
|
|
return savePath;
|
|
}
|
|
|
|
/// 检查是否存在同名文件
|
|
/// [targetDir] 目标目录
|
|
/// [fileName] 文件名
|
|
/// 返回true表示文件已存在
|
|
Future<bool> _checkExistingFile(Directory targetDir, String fileName) async {
|
|
try {
|
|
final files = await targetDir.list().toList();
|
|
for (var entity in files) {
|
|
if (await FileSystemEntity.isFile(entity.path)) {
|
|
final name = basename(entity.path);
|
|
if (name == fileName) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
Logger().d('检查本地文件失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 清理旧文件
|
|
/// [targetDir] 目标目录
|
|
/// 抛出异常如果清理失败
|
|
Future<void> _cleanOldFiles(Directory targetDir) async {
|
|
try {
|
|
final files = await targetDir.list().toList();
|
|
for (var entity in files) {
|
|
if (await FileSystemEntity.isFile(entity.path)) {
|
|
await entity.delete();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
throw Exception('删除旧文件失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|