diff --git a/lib/core/sync_tools.dart b/lib/core/sync_tools.dart index a01966d..f05c14a 100644 --- a/lib/core/sync_tools.dart +++ b/lib/core/sync_tools.dart @@ -134,10 +134,12 @@ List> robocopyRuns({ /// 이동에 붙이는 robocopy 옵션: /MOVE + 같은 파일도 포함. /// 217: 대상에 같은 파일이 있으면 robocopy 는 건너뛰고 원본에 남긴다 (종료 코드 0 - "옮겼습니다" 인데 남음, Windows 에서 확인). /// 같음 (/IS) 뿐 아니라 속성만 다름 (Tweaked, /IT) · 만든 시각만 다름 (Modified, /IM - 다른 프로그램으로 복사한 같은 파일) 도 넣어 원본에서 지운다. -/// 그래도 남는 것은 이동 뒤 다시 세어 알린다 (216) +/// 그래도 남는 것은 이동 뒤 다시 세어 알린다 (216). +/// 데이터: 원본 안의 링크 · junction 은 빼고 (/XJ) - robocopy 는 기본으로 junction 을 따라가 /MOVE 가 +/// 링크가 가리키는 원본 바깥 폴더의 파일까지 옮기고 지웠다 (Windows 에서 확인). 앱이 직접 할 때 (214) 와 같게 링크는 남긴다 List robocopyMoveOptions(List opts) => [ '/MOVE', - for (final o in const ['/IS', '/IT', '/IM']) + for (final o in const ['/IS', '/IT', '/IM', '/XJ']) if (!opts.any((x) => x.toUpperCase() == o)) o, ]; diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index 67cbb26..53ff0ee 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -186,7 +186,7 @@ bool taskDeletes(CopyTask t) { /// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다. /// 지울 목록이 나온 뒤에만 [실행] 이 켜진다. 지우기가 없으면 묻지 않고 true /// 178 · 209: 실제로 실행할 방법 · 옵션 (원본 지우기 옵션은 빼고, 이동일 때만 그 방법의 원본 지우기를 붙인다). -/// 238: robocopy 이동은 실제로 붙는 /MOVE /IS /IT /IM 까지 (robocopyRuns 와 같게). [skipNewer] (229): robocopy 의 /XO +/// 238: robocopy 이동은 실제로 붙는 /MOVE /IS /IT /IM /XJ 까지 (robocopyRuns 와 같게). [skipNewer] (229): robocopy 의 /XO String effectiveRunText(String method, String options, {required bool move, bool skipNewer = false}) { final name = method == 'builtin' ? tr('현재 방식') : method; if (method == 'builtin') return name; @@ -246,7 +246,9 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM const SizedBox(height: 8), SyncPreviewView( compute: () async => [ - for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update, skipLinks: direct), + for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update, + // robocopy 이동도 링크를 빼고 (/XJ) 남긴다 + skipLinks: direct || (robo && t.move)), ], onStart: () => set(() { preview = null; diff --git a/test/dismiss_test.dart b/test/dismiss_test.dart index 5e24f01..659c49c 100644 --- a/test/dismiss_test.dart +++ b/test/dismiss_test.dart @@ -288,8 +288,8 @@ void main() { testWidgets('209: 모니터링 카드는 실제로 실행할 것을 이동 / 복사와 함께 보인다 (복사 카드 칸의 원본 지우기 옵션은 쓰지 않는다고 알림)', (tester) async { expect(effectiveRunText('rsync', '-avPog', move: true), 'rsync -avPog --remove-source-files'); expect(effectiveRunText('rsync', '-avPog --remove-source-files', move: false), 'rsync -avPog'); - expect(effectiveRunText('robocopy', '/E /MOV', move: true), 'robocopy /E /MOVE /IS /IT /IM'); // 238: 실제로 붙는 것까지 - expect(effectiveRunText('robocopy', '/E', move: true, skipNewer: true), 'robocopy /E /MOVE /IS /IT /IM /XO'); // 229 + expect(effectiveRunText('robocopy', '/E /MOV', move: true), 'robocopy /E /MOVE /IS /IT /IM /XJ'); // 238: 실제로 붙는 것까지 + expect(effectiveRunText('robocopy', '/E', move: true, skipNewer: true), 'robocopy /E /MOVE /IS /IT /IM /XJ /XO'); // 229 final a = p.join(tmp.path, 'ca'), b = p.join(tmp.path, 'cb'); Directory(a).createSync(recursive: true); Directory(b).createSync(recursive: true); diff --git a/test/move_settled_test.dart b/test/move_settled_test.dart index 712aa69..63efc97 100644 --- a/test/move_settled_test.dart +++ b/test/move_settled_test.dart @@ -128,6 +128,21 @@ void main() { expect(job.leftInSource, 0); }); + test('robocopy 이동 (데이터): 원본 안의 junction 을 따라가 원본 바깥 폴더의 파일을 옮기고 지우지 않는다 (/XJ)', () async { + if (!Platform.isWindows) return; + final outside = Directory(p.join(p.dirname(src), 'outside_${p.basename(src)}'))..createSync(); + addTearDown(() => outside.existsSync() ? outside.deleteSync(recursive: true) : null); + final keep = File(p.join(outside.path, 'keep.txt'))..writeAsStringSync('k'); + Link(p.join(src, 'linked')).createSync(outside.path); // junction + final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, method: CopyMethod.robocopy, options: '/E /COPY:DAT /DCOPY:T /R:2 /W:2'); + await job.run(); + expect(job.error, isNull); + expect(keep.existsSync(), isTrue, reason: '링크가 가리키는 바깥 파일은 그대로'); + expect(File(p.join(dst, 'linked', 'keep.txt')).existsSync(), isFalse, reason: '링크 안의 것은 옮기지 않음'); + expect(Link(p.join(src, 'linked')).existsSync(), isTrue, reason: '링크는 원본에 남김'); + expect(File(p.join(src, 'a.txt')).existsSync(), isFalse, reason: '나머지는 옮김'); + }); + test('235 · 229: robocopy 이동도 받는 쪽이 더 새것이면 건너뜀 (환경 설정 → /XO) · 남은 것은 카드 기록에도 (남은 수 · 옮긴 수)', () async { // 옵션 칸에는 /XO 가 없다 - 환경 설정 (skipNewer) 만으로 robocopy 가 건너뛴다 final old = File(p.join(src, 'old.txt'))..writeAsStringSync('o'); diff --git a/test/sync_tools_test.dart b/test/sync_tools_test.dart index e02bd31..52f0b2e 100644 --- a/test/sync_tools_test.dart +++ b/test/sync_tools_test.dart @@ -61,8 +61,8 @@ void main() { bandwidthKBps: 640, move: true, ); - expect(runs[0], [r'C:\a\movies', p.join(r'E:\dst', 'movies'), '/E', '/R:2', '/IPG:100', '/MOVE', '/IS', '/IT', '/IM']); - expect(runs[1], [r'C:\b', r'E:\dst', '1.mkv', '2.mkv', '/R:2', '/IPG:100', '/MOV', '/IS', '/IT', '/IM']); // 파일만: /E 빼고 /MOV. 217: 이동은 같은 파일도 (/IS · /IT · /IM) + expect(runs[0], [r'C:\a\movies', p.join(r'E:\dst', 'movies'), '/E', '/R:2', '/IPG:100', '/MOVE', '/IS', '/IT', '/IM', '/XJ']); + expect(runs[1], [r'C:\b', r'E:\dst', '1.mkv', '2.mkv', '/R:2', '/IPG:100', '/MOV', '/IS', '/IT', '/IM', '/XJ']); // 파일만: /E 빼고 /MOV. 217: 이동은 같은 파일도 (/IS · /IT · /IM), 링크는 빼고 (/XJ) expect(runs[2].sublist(0, 3), [r'C:\c', r'E:\dst', '3.mkv']); expect(robocopyIpg(0), 0); // 229: 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" → /XO (옵션 칸에 있으면 두 번 넣지 않음)