WIP 229 폴더 충돌 (WebDAV): 합칠 때 안쪽의 받는 쪽이 더 새 파일 건너뜀 · 이동은 건너뛴 원본 남김 (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-12 08:05:59 +09:00
parent 62e8d52031
commit a4a5c4289a
2 changed files with 55 additions and 3 deletions

View File

@ -760,16 +760,24 @@ class FileOps {
final total = await _totalSizeV(sources); final total = await _totalSizeV(sources);
var done = 0; var done = 0;
final made = <String>[]; final made = <String>[];
Future<void> copyAny(String src, String target) async { Future<void> copyAny(String src, String target, {bool inner = false}) async {
_check(); _check();
final st = await vStat(src); final st = await vStat(src);
if (st == null) throw FileSystemException(tr('원본이 없습니다'), src); if (st == null) throw FileSystemException(tr('원본이 없습니다'), src);
if (st.isDir) { if (st.isDir) {
await vMkdirs(target); await vMkdirs(target);
for (final e in await vList(src)) { for (final e in await vList(src)) {
await copyAny(e.path, vJoin(target, vBasename(e.path))); await copyAny(e.path, vJoin(target, vBasename(e.path)), inner: true);
} }
} else { } else {
// 229: 합치는 폴더 안에서 받는 쪽이 더 새것이면 예전 것으로 덮지 않는다 (WebDAV 도 로컬과 같게)
if (inner && skipNewerInMerge) {
final t = await vStat(target);
if (t != null && !t.isDir && t.modified.isAfter(st.modified.add(const Duration(seconds: 2)))) {
keptNewer.add(src);
return;
}
}
await _copyFileV(src, target, st.size, st.modified, (n) { await _copyFileV(src, target, st.size, st.modified, (n) {
done += n; done += n;
onProgress?.call(src, done, total); onProgress?.call(src, done, total);
@ -806,12 +814,35 @@ class FileOps {
made.add(target); made.add(target);
continue; continue;
} }
final keptBefore = keptNewer.length;
made.addAll(await _copyV([s], destDir, onProgress: onProgress)); made.addAll(await _copyV([s], destDir, onProgress: onProgress));
final kept = keptNewer.sublist(keptBefore);
if (kept.isEmpty) {
await vDelete(s); await vDelete(s);
} else {
// 229: 건너뛴 (받는 쪽이 더 새) 원본은 남기고 옮긴 것만 지운다
await _deleteExceptV(s, kept.toSet());
}
} }
return made; return made;
} }
/// 229: [dir] (로컬 · WebDAV) 안에서 [keep] 의 파일만 남기고 지운다 (비게 된 폴더도)
Future<bool> _deleteExceptV(String dir, Set<String> keep) async {
var empty = true;
for (final e in await vList(dir)) {
if (e.isDir) {
if (!await _deleteExceptV(e.path, keep)) empty = false;
} else if (keep.contains(e.path)) {
empty = false;
} else {
await vDelete(e.path);
}
}
if (empty) await vDelete(dir);
return empty;
}
Future<int> _mirrorV(String srcDir, String dstDir, {bool delete = false, bool update = false, bool strict = false}) async { Future<int> _mirrorV(String srcDir, String dstDir, {bool delete = false, bool update = false, bool strict = false}) async {
var n = 0; var n = 0;
Future<void> walk(String src, String dst) async { Future<void> walk(String src, String dst) async {

View File

@ -281,6 +281,27 @@ void main() {
expect(File(p.join(src, 'a.txt')).existsSync(), isFalse); expect(File(p.join(src, 'a.txt')).existsSync(), isFalse);
}); });
test('229 폴더 충돌 (WebDAV 로): 폴더를 덮어쓰기로 합쳐도 안쪽의 받는 쪽이 더 새 파일은 건너뛰고 원본에 남긴다', () async {
HttpOverrides.global = null;
final d = p.join(tmp.path, 's3', 'D');
final oldSame = File(p.join(d, 'same.txt'))
..createSync(recursive: true)
..writeAsStringSync('old');
oldSame.setLastModifiedSync(DateTime(2026, 1, 1));
File(p.join(d, 'other.txt')).writeAsStringSync('o');
File(p.join(remote.path, 'D', 'same.txt'))
..createSync(recursive: true)
..writeAsStringSync('NEWER');
final job = TransferJob(sources: [d], dest: 'dav://nas/', move: true, conflict: NameConflict.overwrite, skipNewer: true);
await job.run();
expect(job.error, isNull);
expect(File(p.join(remote.path, 'D', 'same.txt')).readAsStringSync(), 'NEWER', reason: '더 새 파일 그대로');
expect(File(p.join(remote.path, 'D', 'other.txt')).readAsStringSync(), 'o');
expect(oldSame.existsSync(), isTrue, reason: '건너뛴 원본은 이동이어도 남김');
expect(File(p.join(d, 'other.txt')).existsSync(), isFalse, reason: '옮긴 것은 지움');
expect(job.leftInSource, 1);
});
test('214 d (WebDAV 로): 로컬 원본의 링크는 따라가지 않는다', () async { test('214 d (WebDAV 로): 로컬 원본의 링크는 따라가지 않는다', () async {
HttpOverrides.global = null; HttpOverrides.global = null;
final outside = Directory(p.join(tmp.path, 'outside2'))..createSync(); final outside = Directory(p.join(tmp.path, 'outside2'))..createSync();