modular_sync F5: 화면에 보이는 실행 옵션 글 = 실제 실행 인수 (runOptionsText 하나)

- 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 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-24 10:15:30 +09:00
parent 88f0701d23
commit 9d1883311f
5 changed files with 67 additions and 14 deletions

View File

@ -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);

View File

@ -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))

View File

@ -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<void> 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<bool> 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<bool>(
context: context,
builder: (ctx) => StatefulBuilder(
@ -236,7 +246,7 @@ Future<bool> 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<bool> 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;

View File

@ -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 화면과 같은 미리 보기를 거친다.

View File

@ -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), '');
});
}