Skip to content

Commit 4e3bdf6

Browse files
committed
fix: webdav sync increment download
1 parent d998782 commit 4e3bdf6

File tree

6 files changed

+116
-86
lines changed

6 files changed

+116
-86
lines changed

lib/config/sync_config_page.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../model/sync_config.dart';
1010
import 'package:http/http.dart' as http;
1111
import 'dart:convert';
1212
import '../generated/l10n/app_localizations.dart';
13+
import 'package:restart_app/restart_app.dart';
1314

1415
class SyncConfigService {
1516
static Future<Map?> loadSyncConfig() async {
@@ -127,6 +128,23 @@ class _SyncConfigPageState extends State<SyncConfigPage> {
127128
ScaffoldMessenger.of(
128129
context,
129130
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.dataWorkDirectorySetSuccess)));
131+
// 新增弹窗提示
132+
showDialog(
133+
context: context,
134+
builder: (context) => AlertDialog(
135+
title: Text('提示'),
136+
content: Text('数据存储目录已更改,需重启程序以应用更改。'),
137+
actions: [
138+
TextButton(
139+
onPressed: () {
140+
Navigator.of(context).pop();
141+
Restart.restartApp();
142+
},
143+
child: Text('确定'),
144+
),
145+
],
146+
),
147+
);
130148
}
131149
}
132150
}

lib/main_page.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import 'config/diary_mode_config_service.dart';
1010
import 'config/theme_service.dart';
1111
import 'util/sync_service.dart';
1212
import 'model/enums.dart';
13-
import 'package:url_launcher/url_launcher.dart';
14-
import 'widgets/sync_progress_dialog.dart';
1513

1614
class MainTabPage extends StatefulWidget {
1715
const MainTabPage({super.key});

lib/util/sync_service.dart

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'dart:convert';
1010
import '../generated/l10n/app_localizations.dart';
1111
import 'package:url_launcher/url_launcher.dart';
1212
import '../widgets/sync_progress_dialog.dart';
13+
import 'package:path/path.dart' as path;
1314

1415
class SyncService {
1516
static Future<bool> isSyncConfigured() async {
@@ -86,7 +87,7 @@ class SyncService {
8687
}).toList();
8788
localDirsList.sort((a, b) => a.split('/').length.compareTo(b.split('/').length));
8889
for (final relativePath in localDirsList) {
89-
final remotePath = (syncConfig.webdavRemoteDir.endsWith('/') ? syncConfig.webdavRemoteDir : syncConfig.webdavRemoteDir + '/') + relativePath.replaceFirst('/', '');
90+
final remotePath = (syncConfig.webdavRemoteDir.endsWith('/') ? syncConfig.webdavRemoteDir : '${syncConfig.webdavRemoteDir}/') + relativePath.replaceFirst('/', '');
9091
final decodedRemotePath = Uri.encodeFull(remotePath);
9192
await WebdavUtil.createDirectory(
9293
webdavUrl: syncConfig.webdavUrl,
@@ -123,17 +124,21 @@ class SyncService {
123124

124125
// 增量下载 WebDAV 内容到本地
125126
print('开始从 WebDAV 增量下载到本地目录');
126-
final downloadSuccess = await WebdavUtil.downloadDirectoryIncremental(
127+
// 下载远程 data/diary 目录到本地 workDir/data/diary
128+
final localDiaryDir = path.join(workDir ?? '', 'data/diary');
129+
// remoteDir 强制拼接
130+
final remoteDiaryDir = syncConfig.webdavRemoteDir.endsWith('/')
131+
? syncConfig.webdavRemoteDir + 'data/diary'
132+
: syncConfig.webdavRemoteDir + '/data/diary';
133+
final downloadSuccess = await WebdavUtil.downloadDirectory(
127134
webdavUrl: syncConfig.webdavUrl,
128135
username: syncConfig.webdavUsername,
129136
password: syncConfig.webdavPassword,
130-
remoteDir: syncConfig.webdavRemoteDir.isEmpty ? '/' : syncConfig.webdavRemoteDir,
131-
localDir: workDir ?? '', // 保证类型安全,workDir一定为String
137+
remoteDir: remoteDiaryDir,
138+
localDir: localDiaryDir,
132139
onProgress: (current, total, filePath) {
133-
// 只统计文件进度,忽略所有文件夹日志
134-
if (onProgress != null && !filePath.startsWith('[目录]')) {
135-
// total 应始终为 remoteTotalFiles
136-
onProgress(current, remoteTotalFiles, filePath);
140+
if (onProgress != null) {
141+
onProgress(current, total, filePath);
137142
}
138143
},
139144
);

lib/util/webdav_util.dart

Lines changed: 76 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ class WebdavUtil {
9595
}
9696
}
9797

98+
// 公共方法:拼接 WebDAV 文件 URL,自动去重斜杠和 /dav 前缀
99+
static String buildWebdavFileUrl(String webdavUrl, String remotePath) {
100+
// 去掉 remotePath 开头所有 / 和 dav/
101+
String path = remotePath;
102+
while (path.startsWith('/')) path = path.substring(1);
103+
while (path.startsWith('dav/')) path = path.substring(4);
104+
// 保证 webdavUrl 结尾只有一个斜杠
105+
String base = webdavUrl.endsWith('/') ? webdavUrl : webdavUrl + '/';
106+
return base + path;
107+
}
108+
98109
static Future<bool> downloadFile({
99110
required String webdavUrl,
100111
required String username,
@@ -103,7 +114,9 @@ class WebdavUtil {
103114
required String localPath,
104115
}) async {
105116
try {
106-
final uri = Uri.parse('$webdavUrl$remotePath');
117+
// 使用公共方法拼接 URL
118+
final url = buildWebdavFileUrl(webdavUrl, remotePath);
119+
final uri = Uri.parse(url);
107120
final auth = base64Encode(utf8.encode('$username:$password'));
108121
print('=== WebDAV 文件下载请求调试信息 ===');
109122
print('请求方法: GET');
@@ -186,20 +199,13 @@ class WebdavUtil {
186199
final uri = Uri.parse('$webdavUrl$remotePath');
187200
final auth = base64Encode(utf8.encode('$username:$password'));
188201
final bytes = await file.readAsBytes();
189-
print('=== WebDAV 文件上传请求调试信息 ===');
190-
print('请求方法: PUT');
191-
print('请求 URL: $uri');
192202
print('上传文件: ${file.path} -> $remotePath');
193203
final response = await http.put(uri, headers: {'Authorization': 'Basic $auth'}, body: bytes);
194-
print('响应状态码: ${response.statusCode}');
195204
if (response.statusCode == 201 || response.statusCode == 200 || response.statusCode == 204) {
196-
print('上传成功: $remotePath');
197-
print('=== WebDAV 文件上传请求调试信息结束 ===');
198205
return true;
199206
} else {
200207
print('上传失败: $remotePath, 状态码: ${response.statusCode}');
201208
print('响应内容: ${response.body}');
202-
print('=== WebDAV 文件上传请求调试信息结束 ===');
203209
return false;
204210
}
205211
} catch (e, stackTrace) {
@@ -275,7 +281,7 @@ class WebdavUtil {
275281
// 依次创建远程目录(父目录优先)
276282
for (final relativePath in localDirsList) {
277283
if (relativePath.isEmpty) continue;
278-
final remotePath = (remoteDir.endsWith('/') ? remoteDir : remoteDir + '/') + relativePath.replaceFirst('/', '');
284+
final remotePath = (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/') + relativePath.replaceFirst('/', '');
279285
print('[增量目录同步] 创建远程目录: $remotePath');
280286
final created = await createDirectory(
281287
webdavUrl: webdavUrl,
@@ -294,7 +300,7 @@ class WebdavUtil {
294300
List<File> filesToUpload = [];
295301
for (final file in allEntities.whereType<File>()) {
296302
final relativePath = file.path.substring(localDir.length).replaceAll('\\', '/');
297-
final remotePath = (remoteDir.endsWith('/') ? remoteDir : remoteDir + '/') + relativePath.replaceFirst('/', '');
303+
final remotePath = (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/') + relativePath.replaceFirst('/', '');
298304
// 获取本地文件修改时间
299305
final localModified = await file.lastModified();
300306
// 获取远程文件修改时间
@@ -321,7 +327,7 @@ class WebdavUtil {
321327
current++;
322328
if (onProgress != null) onProgress(current, total, entity.path);
323329
final relativePath = entity.path.substring(localDir.length).replaceAll('\\', '/');
324-
final remotePath = (remoteDir.endsWith('/') ? remoteDir : remoteDir + '/') + relativePath.replaceFirst('/', '');
330+
final remotePath = (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/') + relativePath.replaceFirst('/', '');
325331
final uploadSuccess = await uploadFile(
326332
webdavUrl: webdavUrl,
327333
username: username,
@@ -352,35 +358,42 @@ class WebdavUtil {
352358
void Function(int current, int total, String filePath)? onProgress,
353359
}) async {
354360
try {
355-
final uri = Uri.parse(webdavUrl + (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/'));
361+
print('=== WebDAV 增量下载目录调试信息 ===');
362+
print('请求方法: PROPFIND');
363+
print('远程目录: $remoteDir');
364+
print('本地目录: $localDir');
365+
final uriStr = webdavUrl + (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/');
366+
print('实际请求 URL: $uriStr');
367+
final uri = Uri.parse(uriStr);
356368
final auth = base64Encode(utf8.encode('$username:$password'));
357369

358370
final request = http.Request('PROPFIND', uri)
359-
..headers.addAll({'Authorization': 'Basic $auth', 'Content-Type': 'application/xml', 'Depth': '1'})
360-
..body = '''<?xml version="1.0" encoding="utf-8" ?>
361-
<D:propfind xmlns:D="DAV:">
362-
<D:prop>
363-
<D:displayname/>
364-
<D:getcontentlength/>
365-
<D:getcontenttype/>
366-
<D:resourcetype/>
367-
<D:getlastmodified/>
368-
</D:prop>
369-
</D:propfind>''';
371+
..headers.addAll({'Authorization': 'Basic $auth', 'Content-Type': 'application/xml', 'Depth': 'infinity'})
372+
..body = '''<?xml version="1.0" encoding="utf-8" ?>\n<D:propfind xmlns:D="DAV:">\n <D:prop>\n <D:displayname/>\n <D:getcontentlength/>\n <D:getcontenttype/>\n <D:resourcetype/>\n <D:getlastmodified/>\n </D:prop>\n</D:propfind>''';
370373

374+
print('发送 PROPFIND 请求...');
371375
final response = await http.Client().send(request);
376+
print('响应状态码: ${response.statusCode}');
372377
if (response.statusCode != 207) {
373378
print('WebDAV PROPFIND 请求失败: ${response.statusCode}');
374379
return false;
375380
}
376381

377382
final responseBody = await response.stream.bytesToString();
383+
print('响应体长度: ${responseBody.length} 字符');
384+
print('响应体内容(前500字符): ${responseBody.substring(0, responseBody.length > 500 ? 500 : responseBody.length)}');
385+
print('=== WebDAV PROPFIND 请求调试信息结束 ===');
378386
final files = _parseWebdavResponseWithModified(responseBody, remoteDir);
387+
print('webdav解析到 ${files.length} 个文件/目录:');
388+
for (final file in files) {
389+
print(' ${file['isFile'] ? '文件' : '目录'}: ${file['name']} (路径: ${file['path']})');
390+
}
379391

380392
// 确保本地目录存在
381393
final localDirectory = Directory(localDir);
382394
if (!await localDirectory.exists()) {
383395
await localDirectory.create(recursive: true);
396+
print('本地目录不存在,已创建: $localDir');
384397
}
385398

386399
// 筛选需要下载的文件
@@ -391,19 +404,17 @@ class WebdavUtil {
391404
final localFile = File(localPath);
392405

393406
if (!await localFile.exists()) {
394-
// 本地文件不存在,需要下载
395407
filesToDownload.add(file);
396-
print('需要下载: ${file['name']} (本地文件不存在)');
408+
print('[增量下载] 需要下载: ${file['name']} (本地文件不存在)');
397409
} else {
398-
// 比较修改时间
399410
final localModified = await localFile.lastModified();
400411
final remoteModified = file['lastModified'] as DateTime?;
401412

402413
if (remoteModified != null && remoteModified.isAfter(localModified)) {
403414
filesToDownload.add(file);
404-
print('需要下载: ${file['name']} (远程文件更新)');
415+
print('[增量下载] 需要下载: ${file['name']} (远程文件更新, 本地: $localModified, 远程: $remoteModified)');
405416
} else {
406-
print('跳过下载: ${file['name']} (本地文件是最新的)');
417+
print('[增量下载] 跳过下载: ${file['name']} (本地文件是最新的, 本地: $localModified, 远程: $remoteModified)');
407418
}
408419
}
409420
}
@@ -415,6 +426,7 @@ class WebdavUtil {
415426

416427
for (final file in filesToDownload) {
417428
current++;
429+
print('[增量下载] 开始下载: ${file['name']} (路径: ${file['path']})');
418430
if (onProgress != null) onProgress(current, total, file['path']);
419431
final success = await downloadFile(
420432
webdavUrl: webdavUrl,
@@ -424,8 +436,10 @@ class WebdavUtil {
424436
localPath: '$localDir/${file['name']}',
425437
);
426438
if (!success) {
427-
print('下载文件失败: ${file['path']}');
439+
print('[增量下载] 下载文件失败: ${file['path']}');
428440
return false;
441+
} else {
442+
print('[增量下载] 下载文件成功: ${file['path']}');
429443
}
430444
}
431445

@@ -446,7 +460,7 @@ class WebdavUtil {
446460
try {
447461
final uri = Uri.parse('$webdavUrl$remotePath');
448462
final auth = base64Encode(utf8.encode('$username:$password'));
449-
final response = await http.Request('MKCOL', uri)
463+
final response = http.Request('MKCOL', uri)
450464
..headers.addAll({'Authorization': 'Basic $auth'});
451465
final streamedResponse = await http.Client().send(response);
452466
// MKCOL: 201 Created 或 405 Method Not Allowed(已存在)视为成功
@@ -484,7 +498,7 @@ class WebdavUtil {
484498
// 去除域名和 remoteDir 前缀
485499
final uri = Uri.parse(href);
486500
return uri.path;
487-
}).where((path) => path != '/' && path != remoteDir && path != (remoteDir.endsWith('/') ? remoteDir : remoteDir + '/')).toList();
501+
}).where((path) => path != '/' && path != remoteDir && path != (remoteDir.endsWith('/') ? remoteDir : '$remoteDir/')).toList();
488502
return dirs;
489503
}
490504

@@ -501,37 +515,34 @@ class WebdavUtil {
501515
return response.statusCode == 204 || response.statusCode == 200 || response.statusCode == 404;
502516
}
503517

504-
// 解析 WebDAV PROPFIND 响应,包含修改时间
518+
// 解析 WebDAV PROPFIND 响应,包含修改时间(支持大小写标签)
505519
static List<Map<String, dynamic>> _parseWebdavResponseWithModified(String xmlResponse, String baseDir) {
520+
// 兼容 d: 和 D: 标签
521+
xmlResponse = xmlResponse.replaceAll('<d:', '<D:').replaceAll('</d:', '</D:');
506522
final files = <Map<String, dynamic>>[];
507523
try {
508-
final lines = xmlResponse.split('\n');
509-
String? currentPath;
510-
bool isCollection = false;
511-
DateTime? lastModified;
512-
513-
for (final line in lines) {
514-
final trimmed = line.trim();
515-
516-
if (trimmed.contains('<D:href>') && trimmed.contains('</D:href>')) {
517-
final start = trimmed.indexOf('<D:href>') + 8;
518-
final end = trimmed.indexOf('</D:href>');
519-
currentPath = trimmed.substring(start, end);
520-
if (currentPath.startsWith('/')) {
521-
currentPath = currentPath.substring(1);
522-
}
523-
isCollection = false;
524-
lastModified = null;
525-
}
526-
527-
if (trimmed.contains('<D:collection/>')) {
528-
isCollection = true;
529-
}
530-
531-
if (trimmed.contains('<D:getlastmodified>') && trimmed.contains('</D:getlastmodified>')) {
532-
final start = trimmed.indexOf('<D:getlastmodified>') + 20;
533-
final end = trimmed.indexOf('</D:getlastmodified>');
534-
final dateStr = trimmed.substring(start, end);
524+
// 提取所有 <D:response>...</D:response>
525+
final responseMatches = RegExp(r'<D:response>([\s\S]*?)</D:response>').allMatches(xmlResponse);
526+
for (final match in responseMatches) {
527+
final response = match.group(1)!;
528+
// href
529+
final hrefMatch = RegExp(r'<D:href>(.*?)</D:href>').firstMatch(response);
530+
if (hrefMatch == null) continue;
531+
var currentPath = hrefMatch.group(1)!;
532+
if (currentPath.startsWith('/')) currentPath = currentPath.substring(1);
533+
// 只过滤掉 baseDir 自身
534+
final baseDirPath = baseDir.replaceAll(RegExp(r'^/+|/+$'), '');
535+
if (currentPath == baseDirPath || currentPath == '$baseDirPath/') continue;
536+
// 判断是否目录
537+
final isCollection = response.contains('<D:collection/>');
538+
// 文件名
539+
final fileName = currentPath.split('/').last;
540+
if (fileName.isEmpty) continue;
541+
// 修改时间
542+
DateTime? lastModified;
543+
final lastModifiedMatch = RegExp(r'<D:getlastmodified>([^<]+)</D:getlastmodified>').firstMatch(response);
544+
if (lastModifiedMatch != null) {
545+
final dateStr = lastModifiedMatch.group(1)!;
535546
try {
536547
lastModified = DateTime.parse(dateStr);
537548
} catch (e) {
@@ -542,23 +553,12 @@ class WebdavUtil {
542553
}
543554
}
544555
}
545-
546-
if (trimmed.contains('</D:response>') && currentPath != null) {
547-
if (currentPath != baseDir.replaceAll('/', '') &&
548-
currentPath != baseDir.replaceFirst('/', '') &&
549-
currentPath.isNotEmpty) {
550-
final fileName = currentPath.split('/').last;
551-
if (fileName.isNotEmpty) {
552-
files.add({
553-
'name': fileName,
554-
'path': '/$currentPath',
555-
'isFile': !isCollection,
556-
'lastModified': lastModified,
557-
});
558-
}
559-
}
560-
currentPath = null;
561-
}
556+
files.add({
557+
'name': fileName,
558+
'path': '/' + currentPath,
559+
'isFile': !isCollection,
560+
'lastModified': lastModified,
561+
});
562562
}
563563
} catch (e) {
564564
print('解析 WebDAV 响应异常: $e');

pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,14 @@ packages:
445445
url: "https://pub.dev"
446446
source: hosted
447447
version: "6.0.2"
448+
restart_app:
449+
dependency: "direct main"
450+
description:
451+
name: restart_app
452+
sha256: "00d5ec3e9de871cedbe552fc41e615b042b5ec654385e090e0983f6d02f655ed"
453+
url: "https://pub.dev"
454+
source: hosted
455+
version: "1.3.2"
448456
shared_preferences:
449457
dependency: "direct main"
450458
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies:
4545
flutter_dotenv: ^5.2.1
4646
permission_handler: ^11.3.1
4747
flutter_svg: ^2.0.10
48+
restart_app: ^1.3.2
4849

4950
dev_dependencies:
5051
flutter_test:

0 commit comments

Comments
 (0)