startDownload method
开始或恢复下载
断点续传策略:
- 文件名包含 version+buildVersion,版本变化自动重新下载
- 检测本地文件大小,若超出预期则删除重来
- 服务端不支持 Range(返回200而非206)时,自动清理本地文件从头写入
Implementation
Future<void> startDownload() async {
_assertAndroid();
if (_updateInfo?.latestVersion == null) {
_emit(UpgraderEventType.downloadError, '更新信息为空,无法开始下载');
statusNotifier.value = DownloadStatus.error;
return;
}
if (statusNotifier.value == DownloadStatus.downloading) {
_emit(UpgraderEventType.log, '下载已在进行中,跳过重复请求');
return;
}
statusNotifier.value = DownloadStatus.downloading;
_cancelToken = CancelToken();
final latest = _updateInfo!.latestVersion!;
try {
final tempDir = await getTemporaryDirectory();
// 文件名包含 version + buildVersion,避免版本切换时复用旧文件
_savePath = '${tempDir.path}/app-v${latest.version}-${latest.buildVersion}.apk';
_emit(UpgraderEventType.downloadStart, '准备下载', {
'savePath': _savePath,
'downloadUrl': latest.downloadUrl,
'apkSize': latest.apkSize,
'version': latest.version,
'buildVersion': latest.buildVersion,
});
// 清理其他版本的残留文件
await _cleanOldApkFiles(tempDir, _savePath!);
final file = File(_savePath!);
int existingLength = 0;
if (await file.exists()) {
existingLength = await file.length();
_emit(UpgraderEventType.downloadResume, '检测到本地已有文件', {
'existingBytes': existingLength,
'expectedBytes': latest.apkSize,
});
// 本地文件大小超出或等于预期大小
if (latest.apkSize > 0 && existingLength >= latest.apkSize) {
if (existingLength == latest.apkSize) {
_emit(UpgraderEventType.log, '文件大小匹配,直接进入校验');
await _onDownloadCompleted();
return;
}
// 大小超出,说明文件损坏,删除重来
_emit(UpgraderEventType.downloadConflict, '本地文件大小异常,删除重下', {
'existingBytes': existingLength,
'expectedBytes': latest.apkSize,
});
await file.delete();
existingLength = 0;
}
}
// 执行下载
final useRange = existingLength > 0;
// 构建请求头:OSS 签名 + 断点续传 Range
final Map<String, String> downloadHeaders = {};
if (_ossConfig != null && !_ossConfig!.isPublicRead) {
downloadHeaders.addAll(
OssSigner.generateHeaders(
config: _ossConfig!,
downloadUrl: latest.downloadUrl,
),
);
}
if (useRange) {
downloadHeaders['Range'] = 'bytes=$existingLength-';
}
await _dio.download(
latest.downloadUrl,
_savePath,
cancelToken: _cancelToken,
// 有续传偏移时用 append 模式,否则用 write(覆盖)
fileAccessMode: useRange ? FileAccessMode.append : FileAccessMode.write,
onReceiveProgress: (received, total) {
final currentTotal = existingLength + received;
final totalSize = latest.apkSize > 0 ? latest.apkSize : (total + existingLength);
if (totalSize > 0) {
progressNotifier.value = (currentTotal / totalSize).clamp(0.0, 1.0);
}
_emit(UpgraderEventType.downloadProgress, '下载进度', {
'received': received,
'total': total,
'currentTotal': currentTotal,
'totalSize': totalSize,
'progress': totalSize > 0
? (currentTotal / totalSize).clamp(0.0, 1.0)
: null,
});
},
options: Options(
headers: downloadHeaders.isEmpty ? null : downloadHeaders,
validateStatus:
useRange ? (status) => status == 200 || status == 206 : null,
),
deleteOnError: false,
);
// 下载请求完成后,检查文件一致性
// 如果我们请求了 Range 但服务端返回了完整文件(200),
// dio.download 在 append 模式下会把完整文件追加到已有内容后面
// 这里通过大小检测来兜底
if (latest.apkSize > 0) {
final actualSize = await file.length();
if (actualSize != latest.apkSize) {
_emit(UpgraderEventType.downloadConflict, '文件大小不匹配,重新下载', {
'actualBytes': actualSize,
'expectedBytes': latest.apkSize,
});
await file.delete();
// 递归重试一次(此时本地文件已删除,不会再走续传)
statusNotifier.value = DownloadStatus.none;
await startDownload();
return;
}
}
await _onDownloadCompleted();
} on DioException catch (e) {
if (CancelToken.isCancel(e)) {
statusNotifier.value = DownloadStatus.paused;
_emit(UpgraderEventType.downloadPaused, '下载已暂停', {
'savePath': _savePath,
'progress': progressNotifier.value,
});
} else if (e.response?.statusCode == 416) {
// 416 Range Not Satisfiable:本地文件 offset 无效
_emit(UpgraderEventType.downloadRetry, '收到 416,清理本地文件后重试', {
'statusCode': 416,
});
if (_savePath != null) {
final file = File(_savePath!);
if (await file.exists()) await file.delete();
}
statusNotifier.value = DownloadStatus.none;
await startDownload(); // 重试
} else {
statusNotifier.value = DownloadStatus.error;
_emit(UpgraderEventType.downloadError, '下载出错', {
'error': e.toString(),
'message': e.message,
'statusCode': e.response?.statusCode,
});
_errorHandler?.call(e);
}
} catch (e) {
statusNotifier.value = DownloadStatus.error;
_emit(UpgraderEventType.downloadError, '下载时发生未知错误', {
'error': e.toString(),
});
_errorHandler?.call(e);
}
}