generated from dellevin/template
111
This commit is contained in:
@@ -79,19 +79,20 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成备份文件名
|
// 生成备份文件名
|
||||||
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}');
|
||||||
|
|
||||||
// 清理旧备份,只保留最新的5个
|
// 清理旧备份,只保留最新的5个
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user