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
parent
88f0701d23
commit
9d1883311f
|
|
@ -2,7 +2,7 @@ import 'dart:io' show FileSystemEntity;
|
||||||
|
|
||||||
import 'file_ops.dart' show isAppTempName, isPartialFile, SourceUnreadableException;
|
import 'file_ops.dart' show isAppTempName, isPartialFile, SourceUnreadableException;
|
||||||
import 'rsync_filter.dart';
|
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;
|
export 'sync_tools.dart' show isSourceRemovingOption;
|
||||||
import 'vfs.dart';
|
import 'vfs.dart';
|
||||||
|
|
@ -102,6 +102,18 @@ String withoutSourceRemovingOptions(String options) => rawOptionTokens(options)
|
||||||
.where((raw) => !splitOptions(raw).any(isSourceRemovingOption))
|
.where((raw) => !splitOptions(raw).any(isSourceRemovingOption))
|
||||||
.join(' ');
|
.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 옵션 등)
|
/// 옵션 글에 원본을 지우는 옵션이 들어 있는지 (환경 설정의 rsync 옵션 등)
|
||||||
bool optionsRemoveSource(String options) => splitOptions(options).any(isSourceRemovingOption);
|
bool optionsRemoveSource(String options) => splitOptions(options).any(isSourceRemovingOption);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import '../../app/transfer_plan.dart';
|
||||||
import '../../core/file_clipboard.dart';
|
import '../../core/file_clipboard.dart';
|
||||||
import '../../core/file_ops.dart';
|
import '../../core/file_ops.dart';
|
||||||
import '../../core/name_clash.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/sync_tools.dart';
|
||||||
import '../../core/vfs.dart';
|
import '../../core/vfs.dart';
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
|
|
@ -197,7 +197,8 @@ class ExplorerTransfers extends ChangeNotifier {
|
||||||
'\n${namesOf(sources)}'
|
'\n${namesOf(sources)}'
|
||||||
'${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [namesOf(skippedHere)])}'}'
|
'${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [namesOf(skippedHere)])}'}'
|
||||||
'\n\n${trf('방법: {0}', [tr(method.label)])}'
|
'\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])}' : ''}'),
|
'${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'),
|
||||||
// 178 보충: 환경 설정의 원본 지우기 옵션은 복사에서 뺀다 - 왜 빠졌는지 알림
|
// 178 보충: 환경 설정의 원본 지우기 옵션은 복사에서 뺀다 - 왜 빠졌는지 알림
|
||||||
if (!move && method != CopyMethod.builtin && optionsRemoveSource(task.options))
|
if (!move && method != CopyMethod.builtin && optionsRemoveSource(task.options))
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import '../../app/app_controller.dart';
|
||||||
import '../../app/copy_center.dart';
|
import '../../app/copy_center.dart';
|
||||||
import '../../app/live_sync.dart';
|
import '../../app/live_sync.dart';
|
||||||
import '../../app/settings.dart';
|
import '../../app/settings.dart';
|
||||||
|
import '../../core/sync_preview.dart' show runOptionsText;
|
||||||
|
import '../../core/sync_tools.dart' show CopyMethod;
|
||||||
import '../../core/vfs.dart';
|
import '../../core/vfs.dart';
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
import '../copy_sync_settings.dart' show editLiveSyncPairDialog, removeLiveSyncPair;
|
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 원본/ 대상/)
|
/// 최종 정리: 원본의 것을 모두 대상으로 옮기고 (rsync -avHPOg --remove-source-files 원본/ 대상/)
|
||||||
/// 빈 폴더를 지운다 (find 원본/ -type d -empty -delete). 원본 폴더를 남길지 · 지울지만 고른다.
|
/// 빈 폴더를 지운다 (find 원본/ -type d -empty -delete). 원본 폴더를 남길지 · 지울지만 고른다.
|
||||||
/// 원본이 비므로 이 실시간 동기화는 끈다 (켜 둔 채 "지우기 포함" 이면 대상까지 비워 버린다).
|
/// 원본이 비므로 이 실시간 동기화는 끈다 (켜 둔 채 "지우기 포함" 이면 대상까지 비워 버린다).
|
||||||
|
|
@ -224,6 +229,11 @@ Future<bool> finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var prune = 'keep';
|
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>(
|
final go = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => StatefulBuilder(
|
builder: (ctx) => StatefulBuilder(
|
||||||
|
|
@ -236,7 +246,7 @@ Future<bool> finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai
|
||||||
Text(tr('원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.')),
|
Text(tr('원본의 파일을 모두 대상으로 옮기고 (원본에서 지움), 원본에 남은 빈 폴더를 지웁니다.')),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
SelectableText(
|
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',
|
'find ${x.source}/ -type d -empty -delete',
|
||||||
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
|
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
@ -264,10 +274,9 @@ Future<bool> finalizeLiveSync(BuildContext context, AppController c, LiveSyncPai
|
||||||
messenger.showSnackBar(SnackBar(content: Text(tr('rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.'))));
|
messenger.showSnackBar(SnackBar(content: Text(tr('rsync 실행 파일을 찾을 수 없습니다. 환경 설정 > Rsync 에서 확인하세요.'))));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final center = CopyCenter.of(c);
|
|
||||||
var t = await center.remember([x.source], x.target, move: true, contents: true, method: 'rsync');
|
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);
|
await center.update(t);
|
||||||
// 104: 고친 옵션에 지우기가 있으면 미리 보기를 거친다
|
// 104: 고친 옵션에 지우기가 있으면 미리 보기를 거친다
|
||||||
if (!context.mounted || !await confirmDeletingRun(context, t) || !context.mounted) return false;
|
if (!context.mounted || !await confirmDeletingRun(context, t) || !context.mounted) return false;
|
||||||
|
|
|
||||||
|
|
@ -199,13 +199,9 @@ bool taskDeletes(CopyTask t) {
|
||||||
/// 178 · 209: 실제로 실행할 방법 · 옵션 (원본 지우기 옵션은 빼고, 이동일 때만 그 방법의 원본 지우기를 붙인다).
|
/// 178 · 209: 실제로 실행할 방법 · 옵션 (원본 지우기 옵션은 빼고, 이동일 때만 그 방법의 원본 지우기를 붙인다).
|
||||||
/// 238: robocopy 는 실제로 붙는 것까지 (이동의 /MOVE /IS /IT /IM, 링크 /XJ, [skipNewer] (229) 의 /XO - robocopyRuns 와 같게)
|
/// 238: robocopy 는 실제로 붙는 것까지 (이동의 /MOVE /IS /IT /IM, 링크 /XJ, [skipNewer] (229) 의 /XO - robocopyRuns 와 같게)
|
||||||
String effectiveRunText(String method, String options, {required bool move, bool skipNewer = false}) {
|
String effectiveRunText(String method, String options, {required bool move, bool skipNewer = false}) {
|
||||||
final name = method == 'builtin' ? tr('현재 방식') : method;
|
final m = CopyMethod.values.where((x) => x.name == method).firstOrNull ?? CopyMethod.builtin;
|
||||||
if (method == 'builtin') return name;
|
if (m == CopyMethod.builtin) return tr('현재 방식');
|
||||||
final opts = withoutSourceRemovingOptions(options);
|
return [method, runOptionsText(m, options, move: move, skipNewer: skipNewer)].where((x) => x.isNotEmpty).join(' ');
|
||||||
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(' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다.
|
/// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다.
|
||||||
|
|
|
||||||
|
|
@ -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), '');
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue