diff --git a/lib/core/file_ops.dart b/lib/core/file_ops.dart index 768d30f..41b930a 100644 --- a/lib/core/file_ops.dart +++ b/lib/core/file_ops.dart @@ -760,16 +760,24 @@ class FileOps { final total = await _totalSizeV(sources); var done = 0; final made = []; - Future copyAny(String src, String target) async { + Future copyAny(String src, String target, {bool inner = false}) async { _check(); final st = await vStat(src); if (st == null) throw FileSystemException(tr('원본이 없습니다'), src); if (st.isDir) { await vMkdirs(target); 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 { + // 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) { done += n; onProgress?.call(src, done, total); @@ -806,12 +814,35 @@ class FileOps { made.add(target); continue; } + final keptBefore = keptNewer.length; made.addAll(await _copyV([s], destDir, onProgress: onProgress)); - await vDelete(s); + final kept = keptNewer.sublist(keptBefore); + if (kept.isEmpty) { + await vDelete(s); + } else { + // 229: 건너뛴 (받는 쪽이 더 새) 원본은 남기고 옮긴 것만 지운다 + await _deleteExceptV(s, kept.toSet()); + } } return made; } + /// 229: [dir] (로컬 · WebDAV) 안에서 [keep] 의 파일만 남기고 지운다 (비게 된 폴더도) + Future _deleteExceptV(String dir, Set 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 _mirrorV(String srcDir, String dstDir, {bool delete = false, bool update = false, bool strict = false}) async { var n = 0; Future walk(String src, String dst) async { diff --git a/test/move_settled_test.dart b/test/move_settled_test.dart index ccae87a..80d635c 100644 --- a/test/move_settled_test.dart +++ b/test/move_settled_test.dart @@ -281,6 +281,27 @@ void main() { 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 { HttpOverrides.global = null; final outside = Directory(p.join(tmp.path, 'outside2'))..createSync();