From 9d1883311fb423179e9b0209ed6496b7c41dcf7e Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:15:30 +0900 Subject: [PATCH] =?UTF-8?q?modular=5Fsync=20F5:=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EA=B8=80=20=3D=20=EC=8B=A4=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=9D=B8=EC=88=98=20(runOptionsText=20?= =?UTF-8?q?=ED=95=98=EB=82=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/sync_preview.dart runOptionsText: 실행 인수 (rsyncArgs · robocopyRuns) 와 같은 규칙 - 원본 지우기 옵션은 빼고 이동일 때만 그 방법의 것, robocopy 는 앱이 붙이는 것 (이동의 /MOVE /IS /IT /IM · 링크 /XJ · 더 새것 건너뛰기 /XO) 까지 - 부르는 곳: 모니터링 "실제 실행" · 실행 전 미리 보기 (effectiveRunText) · 탐색기 복사 · 이동 확인 창 · 실시간 동기화 [최종 정리] 창 - 고친 어긋남 (15 검토 F5): 탐색기 확인 창이 robocopy 이동을 /MOVE 로만 보였다 · [최종 정리] 창이 모니터링에서 고친 옵션이 있어도 늘 -avHPOg 로 보였다 - 약속 시험: 보이는 글 = 실제 인수 (robocopy 4 · rsync 4 · 현재 방식) 사용자가 보게 될 동작: 탐색기의 robocopy 이동 확인 창에 실제 옵션이 모두 보임 · [최종 정리] 창에 실제로 실행할 옵션이 보임 확인: analyze 0 · 단위 1482 ×2 · 통합 6/6 ×2 · base10 214장 0px · lsync_p0_shot ×2 · rsync_page ×2 (rsync_page 1회는 시작 3초 만에 시험 앱이 끝남 - media_kit 디버그 기록 파일의 PID 겹침, 이 고침과 무관 · 따로 보고) Co-Authored-By: Claude Opus 5.5 --- lib/core/sync_preview.dart | 14 +++++++++- lib/ui/explorer/transfer_flow.dart | 5 ++-- lib/ui/monitor/live_sync_flow.dart | 17 +++++++++--- lib/ui/sync_preview_view.dart | 10 ++----- test/run_options_text_contract_test.dart | 35 ++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 test/run_options_text_contract_test.dart diff --git a/lib/core/sync_preview.dart b/lib/core/sync_preview.dart index b94ca8d..7c9e4ad 100644 --- a/lib/core/sync_preview.dart +++ b/lib/core/sync_preview.dart @@ -2,7 +2,7 @@ import 'dart:io' show FileSystemEntity; import 'file_ops.dart' show isAppTempName, isPartialFile, SourceUnreadableException; import 'rsync_filter.dart'; -import 'sync_tools.dart' show splitOptions, isSourceRemovingOption; +import 'sync_tools.dart' show splitOptions, isSourceRemovingOption, CopyMethod, robocopyExtraOptions; export 'sync_tools.dart' show isSourceRemovingOption; import 'vfs.dart'; @@ -102,6 +102,18 @@ String withoutSourceRemovingOptions(String options) => rawOptionTokens(options) .where((raw) => !splitOptions(raw).any(isSourceRemovingOption)) .join(' '); +/// modular_sync F5: 화면에 보일 실행 옵션 글 - 실행 인수 (rsyncArgs · robocopyRuns) 와 같은 규칙. 원본 지우기 옵션은 빼고 이동일 때만 +/// 그 방법의 것을 붙이고, robocopy 는 앱이 붙이는 것 (이동의 /MOVE /IS /IT /IM · 링크 /XJ · [skipNewer] 의 /XO) 까지. +/// 앱이 직접 (builtin) 이면 빈 글. 대역폭은 화면이 따로 보인다 +String runOptionsText(CopyMethod method, String options, {required bool move, bool skipNewer = false}) { + if (method == CopyMethod.builtin) return ''; + final opts = withoutSourceRemovingOptions(options); + final extra = method == CopyMethod.robocopy + ? robocopyExtraOptions(splitOptions(opts), move: move, skipNewer: skipNewer) + : [if (move) '--remove-source-files']; + return [opts, ...extra].where((x) => x.isNotEmpty).join(' '); +} + /// 옵션 글에 원본을 지우는 옵션이 들어 있는지 (환경 설정의 rsync 옵션 등) bool optionsRemoveSource(String options) => splitOptions(options).any(isSourceRemovingOption); diff --git a/lib/ui/explorer/transfer_flow.dart b/lib/ui/explorer/transfer_flow.dart index e0c94a9..f0dcf8b 100644 --- a/lib/ui/explorer/transfer_flow.dart +++ b/lib/ui/explorer/transfer_flow.dart @@ -11,7 +11,7 @@ import '../../app/transfer_plan.dart'; import '../../core/file_clipboard.dart'; import '../../core/file_ops.dart'; import '../../core/name_clash.dart'; -import '../../core/sync_preview.dart' show optionsRemoveSource, withoutSourceRemovingOptions; +import '../../core/sync_preview.dart' show optionsRemoveSource, runOptionsText; import '../../core/sync_tools.dart'; import '../../core/vfs.dart'; import '../../l10n/tr.dart'; @@ -197,7 +197,8 @@ class ExplorerTransfers extends ChangeNotifier { '\n${namesOf(sources)}' '${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [namesOf(skippedHere)])}'}' '\n\n${trf('방법: {0}', [tr(method.label)])}' - '${method == CopyMethod.builtin ? '' : ' · ${withoutSourceRemovingOptions(task.options)}${move ? (method == CopyMethod.rsync ? ' --remove-source-files' : ' /MOVE') : ''}'}' + // F5: 실제로 실행할 옵션 그대로 (robocopy 이동의 /IS /IT /IM · 링크 /XJ · 더 새것 건너뛰기 /XO 까지 - 예전에는 /MOVE 만) + '${method == CopyMethod.builtin ? '' : ' · ${runOptionsText(method, task.options, move: move, skipNewer: ctl.c.settings.copy.skipNewer)}'}' '${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'), // 178 보충: 환경 설정의 원본 지우기 옵션은 복사에서 뺀다 - 왜 빠졌는지 알림 if (!move && method != CopyMethod.builtin && optionsRemoveSource(task.options)) diff --git a/lib/ui/monitor/live_sync_flow.dart b/lib/ui/monitor/live_sync_flow.dart index fba35a3..a80d1f1 100644 --- a/lib/ui/monitor/live_sync_flow.dart +++ b/lib/ui/monitor/live_sync_flow.dart @@ -17,6 +17,8 @@ import '../../app/app_controller.dart'; import '../../app/copy_center.dart'; import '../../app/live_sync.dart'; import '../../app/settings.dart'; +import '../../core/sync_preview.dart' show runOptionsText; +import '../../core/sync_tools.dart' show CopyMethod; import '../../core/vfs.dart'; import '../../l10n/tr.dart'; import '../copy_sync_settings.dart' show editLiveSyncPairDialog, removeLiveSyncPair; @@ -212,6 +214,9 @@ Future showLivePairMenu(BuildContext context, AppController c, LiveSyncPai } } +/// [최종 정리] 를 처음 할 때의 rsync 옵션 +const finalizeRsyncOptions = '-avHPOg'; + /// 최종 정리: 원본의 것을 모두 대상으로 옮기고 (rsync -avHPOg --remove-source-files 원본/ 대상/) /// 빈 폴더를 지운다 (find 원본/ -type d -empty -delete). 원본 폴더를 남길지 · 지울지만 고른다. /// 원본이 비므로 이 실시간 동기화는 끈다 (켜 둔 채 "지우기 포함" 이면 대상까지 비워 버린다). @@ -224,6 +229,11 @@ Future finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai return false; } var prune = 'keep'; + // F5: 보이는 명령 = 실제 실행 - 처음이면 정해진 옵션, 모니터링에서 고쳐 실행한 적이 있으면 그 옵션 (예전에는 늘 -avHPOg 로 보였다) + final center = CopyCenter.of(c); + final key = center.fresh([x.source], x.target, move: true, contents: true, method: 'rsync').key; + final known = center.tasks.where((y) => y.key == key).firstOrNull; + final runOptions = known == null || known.lastRun.isEmpty ? finalizeRsyncOptions : known.options; final go = await showDialog( context: context, builder: (ctx) => StatefulBuilder( @@ -236,7 +246,7 @@ Future finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai Text(tr('원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.')), const SizedBox(height: 10), SelectableText( - 'rsync -avHPOg --remove-source-files ${x.source}/ ${x.target}/\n' + 'rsync ${runOptionsText(CopyMethod.rsync, runOptions, move: true)} ${x.source}/ ${x.target}/\n' 'find ${x.source}/ -type d -empty -delete', style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)), const SizedBox(height: 14), @@ -264,10 +274,9 @@ Future finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai messenger.showSnackBar(SnackBar(content: Text(tr('rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.')))); return false; } - final center = CopyCenter.of(c); var t = await center.remember([x.source], x.target, move: true, contents: true, method: 'rsync'); - // 처음이면 정해진 옵션 (모니터링에서 고친 것이 있으면 그것) - t = t.copyWith(prune: prune, options: t.lastRun.isEmpty ? '-avHPOg' : null); + // 처음이면 정해진 옵션 (모니터링에서 고친 것이 있으면 그것) - 창에 보인 것과 같은 규칙 + t = t.copyWith(prune: prune, options: t.lastRun.isEmpty ? finalizeRsyncOptions : null); await center.update(t); // 104: 고친 옵션에 지우기가 있으면 미리 보기를 거친다 if (!context.mounted || !await confirmDeletingRun(context, t) || !context.mounted) return false; diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index 6ab5841..afa3ca3 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -199,13 +199,9 @@ bool taskDeletes(CopyTask t) { /// 178 · 209: 실제로 실행할 방법 · 옵션 (원본 지우기 옵션은 빼고, 이동일 때만 그 방법의 원본 지우기를 붙인다). /// 238: robocopy 는 실제로 붙는 것까지 (이동의 /MOVE /IS /IT /IM, 링크 /XJ, [skipNewer] (229) 의 /XO - robocopyRuns 와 같게) String effectiveRunText(String method, String options, {required bool move, bool skipNewer = false}) { - final name = method == 'builtin' ? tr('현재 방식') : method; - if (method == 'builtin') return name; - final opts = withoutSourceRemovingOptions(options); - final extra = method == 'robocopy' - ? robocopyExtraOptions(splitOptions(opts), move: move, skipNewer: skipNewer) - : [if (move && method == 'rsync') '--remove-source-files']; - return [name, opts, ...extra].where((x) => x.isNotEmpty).join(' '); + final m = CopyMethod.values.where((x) => x.name == method).firstOrNull ?? CopyMethod.builtin; + if (m == CopyMethod.builtin) return tr('현재 방식'); + return [method, runOptionsText(m, options, move: move, skipNewer: skipNewer)].where((x) => x.isNotEmpty).join(' '); } /// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다. diff --git a/test/run_options_text_contract_test.dart b/test/run_options_text_contract_test.dart new file mode 100644 index 0000000..34042b8 --- /dev/null +++ b/test/run_options_text_contract_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/core/files/path_rules.dart' show appTempRsyncExcludes; +import 'package:jj_mkvmaker/core/sync_preview.dart' show runOptionsText; +import 'package:jj_mkvmaker/core/sync_tools.dart'; + +/// modular_sync F5 약속: 화면에 보이는 실행 옵션 글 = 실제 실행 인수 (rsyncArgs · robocopyRuns 가 만드는 것) - 예전에는 +/// 탐색기 확인 창이 robocopy 이동을 /MOVE 로만 보였고, [최종 정리] 는 고친 옵션이 있어도 늘 -avHPOg 로 보였다 +void main() { + const src = r'C:\src\Show', dst = r'D:\dst'; + + test('robocopy: 복사 · 이동 · 더 새것 건너뛰기 · 링크 옵션 - 실행 인수에서 경로를 뺀 것과 같음', () { + for (final (opts, move, skipNewer) in [ + ('/E /COPY:DAT /R:2 /W:2', false, false), + ('/E /COPY:DAT /R:2 /W:2', true, false), + ('/E /R:1 /MOVE', true, true), + ('/E /SJ', false, true), + ]) { + final run = robocopyRuns(options: opts, folders: [src], files: const [], dest: dst, move: move, skipNewer: skipNewer).single; + expect(runOptionsText(CopyMethod.robocopy, opts, move: move, skipNewer: skipNewer), run.sublist(2).join(' '), + reason: '$opts · 이동 $move · 건너뛰기 $skipNewer'); + } + }); + + test('rsync: 복사는 원본 지우기 옵션을 빼고 · 이동만 --remove-source-files - 실행 인수 (경로 · 앱 임시 이름 빼기) 와 같음', () { + for (final (opts, move) in [('-avHPOg', false), ('-avHPOg', true), ('-av --remove-source-files --delete', false), ('-a --bwlimit=100', true)]) { + final args = rsyncArgs(options: opts, sources: [src], dest: dst, move: move, contents: true); + final shown = args.sublist(0, args.length - 2).where((a) => !appTempRsyncExcludes.contains(a)).join(' '); + expect(runOptionsText(CopyMethod.rsync, opts, move: move), shown, reason: '$opts · 이동 $move'); + } + }); + + test('앱이 직접 (현재 방식) 은 빈 글', () { + expect(runOptionsText(CopyMethod.builtin, '-avHPOg', move: true), ''); + }); +}