This commit is contained in:
DelLevin-Home
2026-07-07 00:12:50 +08:00
parent 52af291105
commit 2fa777d303
3 changed files with 47 additions and 34 deletions

View File

@@ -80,7 +80,7 @@ class AutoBackupService {
// 导出数据 // 导出数据
final result = await BackupService.instance.exportDataForAutoBackup(); final result = await BackupService.instance.exportDataForAutoBackup();
if (!result.success) { if (!result.success || result.zipPath == null) {
debugPrint('AutoBackup: 导出失败 - ${result.errorMessage}'); debugPrint('AutoBackup: 导出失败 - ${result.errorMessage}');
return; return;
} }
@@ -89,8 +89,9 @@ class AutoBackupService {
final fileName = 'auto_backup_${_formatDateTime(DateTime.now())}.zip'; final fileName = 'auto_backup_${_formatDateTime(DateTime.now())}.zip';
final backupFile = File(path.join(backupDir.path, fileName)); final backupFile = File(path.join(backupDir.path, fileName));
// 写入备份文件 // 移动临时 zip 到备份目录
await backupFile.writeAsBytes(result.zipBytes!); final tempFile = File(result.zipPath!);
await tempFile.rename(backupFile.path);
debugPrint('AutoBackup: 备份成功 - ${backupFile.path}'); debugPrint('AutoBackup: 备份成功 - ${backupFile.path}');

View File

@@ -158,13 +158,9 @@ class BackupService {
encoder.close(); encoder.close();
// 读取最终 zip 文件 // 返回 zip 文件路径,不再读入内存
final zipFile = File(tempZipPath);
final zipBytes = await zipFile.readAsBytes();
await zipFile.delete();
return _ExportData( return _ExportData(
zipBytes: Uint8List.fromList(zipBytes), zipPath: tempZipPath,
movieCount: movies.length, movieCount: movies.length,
bookCount: books.length, bookCount: books.length,
noteCount: notes.length, noteCount: notes.length,
@@ -184,31 +180,36 @@ class BackupService {
Future<ExportResult> exportDataWithImages() async { Future<ExportResult> exportDataWithImages() async {
try { try {
final data = await _buildExportData(); final data = await _buildExportData();
final zipFile = File(data.zipPath!);
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final fileName = 'mooknote_backup_${_formatDateTime(DateTime.now())}.zip'; final fileName = 'mooknote_backup_${_formatDateTime(DateTime.now())}.zip';
final tempFilePath = path.join(tempDir.path, fileName); final tempFilePath = path.join(tempDir.path, fileName);
await File(tempFilePath).writeAsBytes(data.zipBytes);
String? finalPath; String? finalPath;
try { try {
// FilePicker.saveFile 需要 bytes这里必须读入内存
final zipBytes = await zipFile.readAsBytes();
final outputPath = await FilePicker.platform.saveFile( final outputPath = await FilePicker.platform.saveFile(
dialogTitle: '保存备份文件', dialogTitle: '保存备份文件',
fileName: fileName, fileName: fileName,
type: FileType.custom, type: FileType.custom,
allowedExtensions: ['zip'], allowedExtensions: ['zip'],
bytes: data.zipBytes, bytes: zipBytes,
); );
if (outputPath == null) { if (outputPath == null) {
await zipFile.delete();
return ExportResult.cancelled(); return ExportResult.cancelled();
} }
finalPath = outputPath; finalPath = outputPath;
if (finalPath != tempFilePath) {
await File(finalPath).writeAsBytes(data.zipBytes);
}
} catch (e) { } catch (e) {
// FilePicker 不可用时,复制到临时目录
await zipFile.copy(tempFilePath);
finalPath = tempFilePath; finalPath = tempFilePath;
} }
// 清理原始临时 zip
try { await zipFile.delete(); } catch (_) {}
return ExportResult.success( return ExportResult.success(
filePath: finalPath, filePath: finalPath,
movieCount: data.movieCount, movieCount: data.movieCount,
@@ -229,12 +230,12 @@ class BackupService {
// ─── 自动备份导出 ───────────────────────────────────── // ─── 自动备份导出 ─────────────────────────────────────
/// 导出数据用于自动备份(返回字节数据 /// 导出数据用于自动备份(返回临时 zip 文件路径,调用方负责删除
Future<AutoBackupExportResult> exportDataForAutoBackup() async { Future<AutoBackupExportResult> exportDataForAutoBackup() async {
try { try {
final data = await _buildExportData(); final data = await _buildExportData();
return AutoBackupExportResult.success( return AutoBackupExportResult.success(
zipBytes: data.zipBytes, zipPath: data.zipPath!,
movieCount: data.movieCount, movieCount: data.movieCount,
bookCount: data.bookCount, bookCount: data.bookCount,
noteCount: data.noteCount, noteCount: data.noteCount,
@@ -776,7 +777,7 @@ class BackupService {
/// 导出中间数据 /// 导出中间数据
class _ExportData { class _ExportData {
final Uint8List zipBytes; final String? zipPath; // 临时 zip 文件路径,调用方负责删除
final int movieCount; final int movieCount;
final int bookCount; final int bookCount;
final int noteCount; final int noteCount;
@@ -784,7 +785,7 @@ class _ExportData {
final int epubCount; final int epubCount;
_ExportData({ _ExportData({
required this.zipBytes, this.zipPath,
required this.movieCount, required this.movieCount,
required this.bookCount, required this.bookCount,
required this.noteCount, required this.noteCount,
@@ -798,7 +799,7 @@ class _ExportData {
class AutoBackupExportResult { class AutoBackupExportResult {
final bool success; final bool success;
final String? errorMessage; final String? errorMessage;
final Uint8List? zipBytes; final String? zipPath; // 临时 zip 文件路径,调用方负责删除
final int movieCount; final int movieCount;
final int bookCount; final int bookCount;
final int noteCount; final int noteCount;
@@ -808,7 +809,7 @@ class AutoBackupExportResult {
AutoBackupExportResult._({ AutoBackupExportResult._({
required this.success, required this.success,
this.errorMessage, this.errorMessage,
this.zipBytes, this.zipPath,
this.movieCount = 0, this.movieCount = 0,
this.bookCount = 0, this.bookCount = 0,
this.noteCount = 0, this.noteCount = 0,
@@ -817,7 +818,7 @@ class AutoBackupExportResult {
}); });
factory AutoBackupExportResult.success({ factory AutoBackupExportResult.success({
required Uint8List zipBytes, required String zipPath,
required int movieCount, required int movieCount,
required int bookCount, required int bookCount,
required int noteCount, required int noteCount,
@@ -825,7 +826,7 @@ class AutoBackupExportResult {
int epubCount = 0, int epubCount = 0,
}) { }) {
return AutoBackupExportResult._( return AutoBackupExportResult._(
success: true, zipBytes: zipBytes, success: true, zipPath: zipPath,
movieCount: movieCount, bookCount: bookCount, movieCount: movieCount, bookCount: bookCount,
noteCount: noteCount, imageCount: imageCount, noteCount: noteCount, imageCount: imageCount,
epubCount: epubCount, epubCount: epubCount,

View File

@@ -222,13 +222,15 @@ class WebDAVService {
try { try {
if (direction == SyncDirection.upload) { if (direction == SyncDirection.upload) {
final exportResult = await BackupService.instance.exportDataForAutoBackup(); final exportResult = await BackupService.instance.exportDataForAutoBackup();
if (!exportResult.success || exportResult.zipBytes == null) { if (!exportResult.success || exportResult.zipPath == null) {
return SyncResult(success: false, message: exportResult.errorMessage ?? '创建备份失败'); return SyncResult(success: false, message: exportResult.errorMessage ?? '创建备份失败');
} }
final fileName = _generateBackupFileName(); final fileName = _generateBackupFileName();
final zipUrl = '$dirUrl/$fileName'; final zipUrl = '$dirUrl/$fileName';
final success = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!); final success = await _uploadFile(client, zipUrl, username, password, exportResult.zipPath!);
// 清理临时 zip 文件
try { await File(exportResult.zipPath!).delete(); } catch (_) {}
if (success) { if (success) {
uploadedFiles = 1; uploadedFiles = 1;
uploadedImages = exportResult.imageCount; uploadedImages = exportResult.imageCount;
@@ -314,9 +316,11 @@ class WebDAVService {
// 上传本地备份(无论是否下载,确保远程有最新数据) // 上传本地备份(无论是否下载,确保远程有最新数据)
final exportResult = await BackupService.instance.exportDataForAutoBackup(); final exportResult = await BackupService.instance.exportDataForAutoBackup();
if (exportResult.success && exportResult.zipBytes != null) { if (exportResult.success && exportResult.zipPath != null) {
final fileName = _generateBackupFileName(); final fileName = _generateBackupFileName();
final uploadSuccess = await _uploadBytes(client, '$dirUrl/$fileName', username, password, exportResult.zipBytes!); final uploadSuccess = await _uploadFile(client, '$dirUrl/$fileName', username, password, exportResult.zipPath!);
// 清理临时 zip 文件
try { await File(exportResult.zipPath!).delete(); } catch (_) {}
if (uploadSuccess) { if (uploadSuccess) {
uploadedFiles = 1; uploadedFiles = 1;
uploadedImages = exportResult.imageCount; uploadedImages = exportResult.imageCount;
@@ -361,15 +365,22 @@ class WebDAVService {
return prefs.getString(_lastSyncKey); return prefs.getString(_lastSyncKey);
} }
/// 上传字节数据到 WebDAV /// 上传文件到 WebDAV(从文件路径读取,避免备份服务端重复占用内存)
Future<bool> _uploadBytes( Future<bool> _uploadFile(
http.Client client, http.Client client,
String url, String url,
String username, String username,
String password, String password,
Uint8List bytes, String filePath,
) async { ) async {
try { try {
final file = File(filePath);
if (!await file.exists()) {
debugPrint('[WebDAV] _uploadFile: 文件不存在 $filePath');
return false;
}
final bytes = await file.readAsBytes();
var request = http.Request('PUT', Uri.parse(url)); var request = http.Request('PUT', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password); request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Content-Type'] = 'application/zip'; request.headers['Content-Type'] = 'application/zip';
@@ -394,7 +405,7 @@ class WebDAVService {
debugPrint('[WebDAV] PUT $url -> ${response.statusCode}'); debugPrint('[WebDAV] PUT $url -> ${response.statusCode}');
return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204; return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204;
} catch (e) { } catch (e) {
debugPrint('[WebDAV] _uploadBytes error: $e'); debugPrint('[WebDAV] _uploadFile error: $e');
return false; return false;
} }
} }