From 52af291105d43a1c3df651152a83f48cc03b3721 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Mon, 6 Jul 2026 23:59:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=91=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=EF=BC=8C=E4=BF=9D=E7=95=99=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=BA=94=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/sync/webdav_sync_page.dart | 4 - lib/services/sync/backup_service.dart | 2 - lib/services/sync/webdav_service.dart | 271 ++++++++++++++++---------- 3 files changed, 173 insertions(+), 104 deletions(-) diff --git a/lib/pages/sync/webdav_sync_page.dart b/lib/pages/sync/webdav_sync_page.dart index b2d3b48..bbe1a2f 100644 --- a/lib/pages/sync/webdav_sync_page.dart +++ b/lib/pages/sync/webdav_sync_page.dart @@ -55,10 +55,6 @@ class _WebDAVSyncPageState extends State { }); _loadRemoteInfo(); } - - final autoSyncEnabled = await WebDAVService.instance.isAutoSyncEnabled(); - final autoSyncInterval = await WebDAVService.instance.getAutoSyncInterval(); - debugPrint('WebDAV auto sync: enabled=$autoSyncEnabled, interval=$autoSyncInterval'); } Future _loadRemoteInfo() async { diff --git a/lib/services/sync/backup_service.dart b/lib/services/sync/backup_service.dart index 881c6b5..c109db3 100644 --- a/lib/services/sync/backup_service.dart +++ b/lib/services/sync/backup_service.dart @@ -629,8 +629,6 @@ class BackupService { 'avatarPath', 'webdav_config', 'webdav_last_sync', - 'webdav_auto_sync', - 'webdav_auto_sync_interval', }; for (final entry in data.entries) { final key = entry.key; diff --git a/lib/services/sync/webdav_service.dart b/lib/services/sync/webdav_service.dart index 3c38a0c..70e540d 100644 --- a/lib/services/sync/webdav_service.dart +++ b/lib/services/sync/webdav_service.dart @@ -47,19 +47,14 @@ class WebDAVService { static const String _configKey = 'webdav_config'; static const String _lastSyncKey = 'webdav_last_sync'; - static const String _autoSyncKey = 'webdav_auto_sync'; - static const String _autoSyncIntervalKey = 'webdav_auto_sync_interval'; - - // 默认自动同步间隔(分钟) - static const int _defaultAutoSyncInterval = 5; + static const String _backupPrefix = 'mooknote_backup_'; + static const int _maxBackupCount = 5; // HTTP 请求超时 - static const Duration _httpTimeout = Duration(seconds: 30); + static const Duration _httpTimeout = Duration(seconds: 120); + static const Duration _shortTimeout = Duration(seconds: 30); Map? _cachedConfig; - Timer? _autoSyncTimer; - bool _isAutoSyncEnabled = false; - int _autoSyncIntervalMinutes = _defaultAutoSyncInterval; bool _isSyncing = false; /// 获取配置 @@ -106,10 +101,7 @@ class WebDAVService { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_configKey); await prefs.remove(_lastSyncKey); - await prefs.remove(_autoSyncKey); - await prefs.remove(_autoSyncIntervalKey); _cachedConfig = null; - stopAutoSync(); } /// 测试连接 @@ -218,7 +210,7 @@ class WebDAVService { final path = config['path']!; final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; - final zipUrl = '$baseUrl$path/mooknote_backup.zip'; + final dirUrl = '$baseUrl$path'; final client = http.Client(); int uploadedFiles = 0; @@ -234,16 +226,28 @@ class WebDAVService { return SyncResult(success: false, message: exportResult.errorMessage ?? '创建备份失败'); } + final fileName = _generateBackupFileName(); + final zipUrl = '$dirUrl/$fileName'; final success = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!); if (success) { uploadedFiles = 1; uploadedImages = exportResult.imageCount; - debugPrint('[WebDAV] 备份上传成功 (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})'); + debugPrint('[WebDAV] 备份上传成功: $fileName (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})'); + // 清理旧备份 + await _cleanupOldBackups(client, dirUrl, username, password); } else { return SyncResult(success: false, message: '上传备份文件失败'); } } else if (direction == SyncDirection.download) { + // 找到最新的备份文件 + final backups = await _listRemoteBackups(client, dirUrl, username, password); + if (backups.isEmpty) { + return SyncResult(success: false, message: '服务器上没有备份文件,请先从其他设备上传'); + } + final latestFile = backups.last; + final zipUrl = '$dirUrl/$latestFile'; + final tempDir = await getTemporaryDirectory(); final tempZip = File(p.join(tempDir.path, 'mooknote_download.zip')); final success = await _downloadFile(client, zipUrl, username, password, tempZip); @@ -257,17 +261,24 @@ class WebDAVService { downloadedFiles = 1; downloadedImages = importResult.stats?['图片'] ?? 0; needReload = true; - debugPrint('[WebDAV] 备份恢复成功: ${importResult.statsText}'); + debugPrint('[WebDAV] 备份恢复成功 ($latestFile): ${importResult.statsText}'); } else { return SyncResult(success: false, message: importResult.errorMessage ?? '恢复备份失败'); } } else { - return SyncResult(success: false, message: '服务器上没有备份文件,请先从其他设备上传'); + return SyncResult(success: false, message: '下载备份文件失败'); } } else if (direction == SyncDirection.bidirectional) { - // 获取远程备份的修改时间 - final remoteModTime = await _getRemoteFileModifiedTime(client, zipUrl, username, password); + // 获取远程最新备份的修改时间 + final backups = await _listRemoteBackups(client, dirUrl, username, password); + DateTime? remoteModTime; + String? latestRemoteFile; + if (backups.isNotEmpty) { + latestRemoteFile = backups.last; + final latestUrl = '$dirUrl/$latestRemoteFile'; + remoteModTime = await _getRemoteFileModifiedTime(client, latestUrl, username, password); + } // 获取上次同步时间 final syncPrefs = await SharedPreferences.getInstance(); @@ -277,12 +288,12 @@ class WebDAVService { final bool remoteIsNewer = remoteModTime != null && (lastSyncTime == null || remoteModTime.isAfter(lastSyncTime)); - if (remoteIsNewer) { + if (remoteIsNewer && latestRemoteFile != null) { // 远程更新,下载并恢复 final tempDir = await getTemporaryDirectory(); final tempZip = File(p.join(tempDir.path, 'mooknote_bidir.zip')); - final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip); + final downloadSuccess = await _downloadFile(client, '$dirUrl/$latestRemoteFile', username, password, tempZip); if (downloadSuccess && await tempZip.exists()) { final bytes = await tempZip.readAsBytes(); final importResult = await BackupService.instance.restoreFromZipBytes(bytes); @@ -292,7 +303,7 @@ class WebDAVService { downloadedFiles = 1; downloadedImages = importResult.stats?['图片'] ?? 0; needReload = true; - debugPrint('[WebDAV] 远程备份较新,已恢复: ${importResult.statsText}'); + debugPrint('[WebDAV] 远程备份较新,已恢复 ($latestRemoteFile): ${importResult.statsText}'); } } else { try { await tempZip.delete(); } catch (_) {} @@ -304,11 +315,14 @@ class WebDAVService { // 上传本地备份(无论是否下载,确保远程有最新数据) final exportResult = await BackupService.instance.exportDataForAutoBackup(); if (exportResult.success && exportResult.zipBytes != null) { - final uploadSuccess = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!); + final fileName = _generateBackupFileName(); + final uploadSuccess = await _uploadBytes(client, '$dirUrl/$fileName', username, password, exportResult.zipBytes!); if (uploadSuccess) { uploadedFiles = 1; uploadedImages = exportResult.imageCount; - debugPrint('[WebDAV] 本地备份已上传 (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})'); + debugPrint('[WebDAV] 本地备份已上传: $fileName (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})'); + // 清理旧备份 + await _cleanupOldBackups(client, dirUrl, username, password); } } } @@ -341,74 +355,6 @@ class WebDAVService { } } - /// 获取自动同步间隔(分钟) - Future getAutoSyncInterval() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt(_autoSyncIntervalKey) ?? _defaultAutoSyncInterval; - } - - /// 设置自动同步间隔(分钟) - Future setAutoSyncInterval(int minutes) async { - if (minutes < 1) minutes = 1; - if (minutes > 60) minutes = 60; - - _autoSyncIntervalMinutes = minutes; - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(_autoSyncIntervalKey, minutes); - - if (_isAutoSyncEnabled) { - await startAutoSync(); - } - } - - /// 启动自动同步 - Future startAutoSync() async { - await stopAutoSync(); - - final prefs = await SharedPreferences.getInstance(); - _isAutoSyncEnabled = true; - _autoSyncIntervalMinutes = await getAutoSyncInterval(); - await prefs.setBool(_autoSyncKey, true); - - // 立即执行一次同步 - debugPrint('[WebDAV] 自动同步已启动,间隔 $_autoSyncIntervalMinutes 分钟'); - await syncData(direction: SyncDirection.bidirectional); - - // 设置定时器 - _autoSyncTimer = Timer.periodic( - Duration(minutes: _autoSyncIntervalMinutes), - (timer) async { - if (_isAutoSyncEnabled) { - try { - await syncData(direction: SyncDirection.bidirectional); - } catch (e) { - debugPrint('[WebDAV] 自动同步异常: $e'); - } - } - }, - ); - } - - /// 停止自动同步 - Future stopAutoSync() async { - _autoSyncTimer?.cancel(); - _autoSyncTimer = null; - _isAutoSyncEnabled = false; - - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_autoSyncKey, false); - } - - /// 检查自动同步状态 - Future isAutoSyncEnabled() async { - if (_autoSyncTimer != null) { - return _isAutoSyncEnabled; - } - - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_autoSyncKey) ?? false; - } - /// 获取上次同步时间 Future getLastSyncTime() async { final prefs = await SharedPreferences.getInstance(); @@ -435,6 +381,7 @@ class WebDAVService { if (response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 307 || response.statusCode == 308) { final location = response.headers['location']; + await response.stream.drain(); if (location != null) { request = http.Request('PUT', Uri.parse(location)); request.headers['Authorization'] = _basicAuth(username, password); @@ -470,6 +417,7 @@ class WebDAVService { if (response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 307 || response.statusCode == 308) { final location = response.headers['location']; + await response.stream.drain(); if (location != null) { request = http.Request('GET', Uri.parse(location)); request.headers['Authorization'] = _basicAuth(username, password); @@ -493,7 +441,7 @@ class WebDAVService { } } - /// 获取远程备份文件信息(修改时间和大小) + /// 获取远程最新备份文件信息(修改时间和大小) Future?> getRemoteBackupInfo() async { final config = await getConfig(); if (config == null) return null; @@ -504,23 +452,31 @@ class WebDAVService { final path = config['path']!; final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; - final zipUrl = '$baseUrl$path/mooknote_backup.zip'; + final dirUrl = '$baseUrl$path'; final client = http.Client(); try { + // 列出备份文件,找到最新的 + final backups = await _listRemoteBackups(client, dirUrl, username, password); + if (backups.isEmpty) return null; + + final latestFile = backups.last; + final zipUrl = '$dirUrl/$latestFile'; + var request = http.Request('HEAD', Uri.parse(zipUrl)); request.headers['Authorization'] = _basicAuth(username, password); - var response = await client.send(request).timeout(_httpTimeout); + var response = await client.send(request).timeout(_shortTimeout); // 处理重定向 if (response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 307 || response.statusCode == 308) { final location = response.headers['location']; + await response.stream.drain(); if (location != null) { request = http.Request('HEAD', Uri.parse(location)); request.headers['Authorization'] = _basicAuth(username, password); - response = await client.send(request).timeout(_httpTimeout); + response = await client.send(request).timeout(_shortTimeout); } } @@ -554,23 +510,24 @@ class WebDAVService { var request = http.Request('HEAD', Uri.parse(url)); request.headers['Authorization'] = _basicAuth(username, password); - var response = await client.send(request).timeout(_httpTimeout); + var response = await client.send(request).timeout(_shortTimeout); // 处理重定向 if (response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 307 || response.statusCode == 308) { final location = response.headers['location']; + await response.stream.drain(); if (location != null) { request = http.Request('HEAD', Uri.parse(location)); request.headers['Authorization'] = _basicAuth(username, password); - response = await client.send(request).timeout(_httpTimeout); + response = await client.send(request).timeout(_shortTimeout); } } if (response.statusCode == 200) { final lastModified = response.headers['last-modified']; if (lastModified != null) { - return HttpDate.parse(lastModified); + return HttpDate.parse(lastModified).toLocal(); } } return null; @@ -580,6 +537,124 @@ class WebDAVService { } } + /// 生成带毫秒时间戳的备份文件名 + String _generateBackupFileName() { + final ts = DateTime.now().millisecondsSinceEpoch; + return '$_backupPrefix$ts.zip'; + } + + /// 获取远程目录中的备份文件列表(按时间戳升序) + Future> _listRemoteBackups( + http.Client client, + String dirUrl, + String username, + String password, + ) async { + try { + var request = http.Request('PROPFIND', Uri.parse(dirUrl)); + request.headers['Authorization'] = _basicAuth(username, password); + request.headers['Depth'] = '1'; + + var response = await client.send(request).timeout(_shortTimeout); + + // 处理重定向 + if (response.statusCode == 301 || response.statusCode == 302 || + response.statusCode == 307 || response.statusCode == 308) { + final location = response.headers['location']; + await response.stream.drain(); + if (location != null) { + dirUrl = location; + request = http.Request('PROPFIND', Uri.parse(dirUrl)); + request.headers['Authorization'] = _basicAuth(username, password); + request.headers['Depth'] = '1'; + response = await client.send(request).timeout(_shortTimeout); + } + } + + if (response.statusCode != 207) { + debugPrint('[WebDAV] PROPFIND 返回 ${response.statusCode},无法列出文件'); + return []; + } + + final body = await response.stream.bytesToString(); + + // 解析 XML 提取 href 中的文件名 + // 兼容不同命名空间: , , + final hrefRegExp = RegExp(r'<(?:\w+:)?href[^>]*>([^<]+)', caseSensitive: false); + final matches = hrefRegExp.allMatches(body); + final backupFiles = []; + for (final match in matches) { + var href = match.group(1) ?? ''; + // URL decode + href = Uri.decodeFull(href); + // 提取文件名部分 + final fileName = href.split('/').where((s) => s.isNotEmpty).lastOrNull; + if (fileName != null && fileName.startsWith(_backupPrefix) && fileName.endsWith('.zip')) { + backupFiles.add(fileName); + } + } + // 按文件名中的时间戳升序排列 + backupFiles.sort(); + debugPrint('[WebDAV] 找到 ${backupFiles.length} 个备份文件: $backupFiles'); + return backupFiles; + } catch (e) { + debugPrint('[WebDAV] 列出备份文件失败: $e'); + return []; + } + } + + /// 删除远程文件 + Future _deleteRemoteFile( + http.Client client, + String fileUrl, + String username, + String password, + ) async { + try { + var request = http.Request('DELETE', Uri.parse(fileUrl)); + request.headers['Authorization'] = _basicAuth(username, password); + + var response = await client.send(request).timeout(_shortTimeout); + + // 处理重定向 + if (response.statusCode == 301 || response.statusCode == 302 || + response.statusCode == 307 || response.statusCode == 308) { + final location = response.headers['location']; + await response.stream.drain(); + if (location != null) { + request = http.Request('DELETE', Uri.parse(location)); + request.headers['Authorization'] = _basicAuth(username, password); + response = await client.send(request).timeout(_shortTimeout); + } + } + + debugPrint('[WebDAV] DELETE $fileUrl -> ${response.statusCode}'); + return response.statusCode == 200 || response.statusCode == 204 || response.statusCode == 404; + } catch (e) { + debugPrint('[WebDAV] 删除远程文件失败: $e'); + return false; + } + } + + /// 清理旧备份,保留最新的 _maxBackupCount 个 + Future _cleanupOldBackups( + http.Client client, + String dirUrl, + String username, + String password, + ) async { + final backups = await _listRemoteBackups(client, dirUrl, username, password); + debugPrint('[WebDAV] 清理检查: 共 ${backups.length} 个备份,保留 $_maxBackupCount 个'); + if (backups.length <= _maxBackupCount) return; + + final toDelete = backups.sublist(0, backups.length - _maxBackupCount); + for (final fileName in toDelete) { + final fileUrl = '$dirUrl/$fileName'; + final deleted = await _deleteRemoteFile(client, fileUrl, username, password); + debugPrint('[WebDAV] 删除旧备份 $fileName: ${deleted ? '成功' : '失败'}'); + } + } + /// Basic Auth 编码 String _basicAuth(String username, String password) { final credentials = base64Encode(utf8.encode('$username:$password'));