robocopy 도 '받는 쪽이 더 새것이면 건너뛰기' 를 따름 (/XO) (229)
- 데이터 (버그, 관리자 지적): robocopy 는 기본으로 예전 원본 (Older) 으로 대상의 새 파일을 덮어썼다 - 설정이 켜져 있으면 /XO - 건너뛴 원본은 남기고 알림, 미리 보기 · 명령 줄도 같게. 설정 설명 글에 robocopy 를 넣음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
d8ce0eb302
commit
90246717ec
|
|
@ -2034,7 +2034,7 @@
|
|||
"허용했으면 다시 확인": "Check again after allowing",
|
||||
"헝가리어": "Hungarian",
|
||||
"현재 방식": "Built-in",
|
||||
"현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "When syncing folder contents with the built-in method or WebDAV (same as rsync -u). Skipped files stay in the source even when moving",
|
||||
"현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "When copying or moving with the built-in method, robocopy or WebDAV (same as rsync -u / robocopy /XO). Skipped files stay in the source even when moving",
|
||||
"현재 유지": "Keep as is",
|
||||
"현재 페이지": "Current page",
|
||||
"현재 페이지 추가": "Add current page",
|
||||
|
|
|
|||
|
|
@ -2034,7 +2034,7 @@
|
|||
"허용했으면 다시 확인": "許可したらもう一度確認",
|
||||
"헝가리어": "ハンガリー語",
|
||||
"현재 방식": "現在の方式",
|
||||
"현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "現在の方式 · WebDAV でフォルダーの中身を合わせるとき (rsync の -u と同じ)。スキップしたファイルは移動でも元の場所に残します",
|
||||
"현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "現在の方式 · robocopy · WebDAV でコピー · 移動するとき (rsync の -u、robocopy の /XO と同じ)。スキップしたファイルは移動でも元の場所に残します",
|
||||
"현재 유지": "現状維持",
|
||||
"현재 페이지": "現在のページ",
|
||||
"현재 페이지 추가": "現在のページを追加",
|
||||
|
|
|
|||
|
|
@ -2034,7 +2034,7 @@
|
|||
"허용했으면 다시 확인": "允许后重新检查",
|
||||
"헝가리어": "匈牙利语",
|
||||
"현재 방식": "当前方式",
|
||||
"현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "使用当前方式 · WebDAV 同步文件夹内容时(与 rsync 的 -u 相同)。跳过的文件即使是移动也会保留在源中",
|
||||
"현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "使用当前方式 · robocopy · WebDAV 复制 · 移动时(与 rsync 的 -u、robocopy 的 /XO 相同)。跳过的文件即使是移动也会保留在源中",
|
||||
"현재 유지": "保持现状",
|
||||
"현재 페이지": "当前页面",
|
||||
"현재 페이지 추가": "添加当前页面",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ class TransferJob extends ChangeNotifier {
|
|||
/// 48: 같은 이름이 있을 때 (앱이 직접 복사 · 이동할 때만 - robocopy · rsync 는 그 프로그램의 규칙)
|
||||
final NameConflict conflict;
|
||||
|
||||
/// 229: 앱이 직접 맞출 때 받는 쪽이 더 새것이면 건너뛴다 (환경 설정 - 옵션 칸이 없는 현재 방식 · WebDAV 도). 건너뛴 원본은 이동이라도 남긴다
|
||||
/// 229: 앱이 직접 맞출 때 · robocopy (/XO) 에서 받는 쪽이 더 새것이면 건너뛴다 (환경 설정 - 옵션 칸이 없는 현재 방식 · WebDAV 도).
|
||||
/// 건너뛴 원본은 이동이라도 남긴다. rsync 는 옵션 칸의 -u 를 따른다
|
||||
final bool skipNewer;
|
||||
|
||||
/// 같은 이름이라 건너뛴 항목 (원본 경로)
|
||||
|
|
@ -440,6 +441,7 @@ class TransferJob extends ChangeNotifier {
|
|||
bandwidthKBps: bandwidthKBps,
|
||||
move: move,
|
||||
contents: contents,
|
||||
skipNewer: skipNewer,
|
||||
);
|
||||
_tick();
|
||||
for (final args in runs) {
|
||||
|
|
|
|||
|
|
@ -107,12 +107,15 @@ List<List<String>> robocopyRuns({
|
|||
int bandwidthKBps = 0,
|
||||
bool move = false,
|
||||
bool contents = false,
|
||||
bool skipNewer = false,
|
||||
}) {
|
||||
// 178: 원본을 지우는 옵션 (/MOV · /MOVE) 은 [move] 로만
|
||||
final opts = [for (final o in splitOptions(options)) if (!isSourceRemovingOption(o)) o];
|
||||
final extra = [
|
||||
if (bandwidthKBps > 0 && !opts.any((o) => o.toUpperCase().startsWith('/IPG'))) '/IPG:${robocopyIpg(bandwidthKBps)}',
|
||||
if (move) ...robocopyMoveOptions(opts),
|
||||
// 229: 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" - robocopy 는 기본으로 예전 원본 (Older) 도 덮어쓴다
|
||||
if (skipNewer && !opts.any((o) => o.toUpperCase() == '/XO')) '/XO',
|
||||
];
|
||||
final runs = <List<String>>[
|
||||
for (final f in folders) [f, contents ? dest : p.join(dest, p.basename(f)), ...opts, ...extra],
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
value: s.copySkipNewer,
|
||||
onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v),
|
||||
title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')),
|
||||
subtitle: Text(tr('현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')),
|
||||
subtitle: Text(tr('현재 방식 · robocopy · WebDAV 로 복사 · 이동할 때 (rsync 의 -u, robocopy 의 /XO 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('속도 제한 (KB/s)')),
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
content: Text(trf('칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?', [
|
||||
'${_method == 'builtin' ? tr('현재 방식') : _method} ${_options.text.trim()}',
|
||||
// 178: 실제로 쓰는 옵션 (원본 지우기는 이동일 때만)
|
||||
effectiveRunText(t.method, t.options, move: t.move),
|
||||
effectiveRunText(t.method, t.options, move: t.move, skipNewer: widget.c.settings.copySkipNewer),
|
||||
])),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),
|
||||
|
|
@ -552,7 +552,7 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
width: 460,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(
|
||||
'${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move)])}',
|
||||
'${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move, skipNewer: s.copySkipNewer)])}',
|
||||
style: TextStyle(fontSize: 12, color: t.move ? Colors.orangeAccent : JjColors.textDim),
|
||||
),
|
||||
if (!t.move && optionsRemoveSource(_options.text))
|
||||
|
|
|
|||
|
|
@ -183,8 +183,8 @@ bool taskDeletes(CopyTask t) {
|
|||
/// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다.
|
||||
/// 지울 목록이 나온 뒤에만 [실행] 이 켜진다. 지우기가 없으면 묻지 않고 true
|
||||
/// 178 · 209: 실제로 실행할 방법 · 옵션 (원본 지우기 옵션은 빼고, 이동일 때만 그 방법의 원본 지우기를 붙인다).
|
||||
/// 238: robocopy 이동은 실제로 붙는 /MOVE /IS /IT /IM 까지 (robocopyRuns 와 같게)
|
||||
String effectiveRunText(String method, String options, {required bool move}) {
|
||||
/// 238: robocopy 이동은 실제로 붙는 /MOVE /IS /IT /IM 까지 (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;
|
||||
final opts = withoutSourceRemovingOptions(options);
|
||||
|
|
@ -195,7 +195,8 @@ String effectiveRunText(String method, String options, {required bool move}) {
|
|||
: method == 'rsync'
|
||||
? ' --remove-source-files'
|
||||
: '';
|
||||
return '$name $opts$extra'.trim();
|
||||
final xo = method == 'robocopy' && skipNewer && !splitOptions(opts).any((o) => o.toUpperCase() == '/XO') ? ' /XO' : '';
|
||||
return '$name $opts$extra$xo'.trim();
|
||||
}
|
||||
|
||||
/// [includeMove] (210): 이동 (원본 파일 지우기) 도 미리 보기 · 확인을 거친다 - 모니터링의 [실행] 처럼 따로 묻는 창이 없을 때.
|
||||
|
|
@ -207,7 +208,11 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
List<PreviewItem>? preview;
|
||||
Object? error;
|
||||
final direct = t.method == 'builtin' || [...t.sources, t.dest].any(isDav);
|
||||
final update = optionsUpdate(t.options) || (direct && skipNewer);
|
||||
// robocopy 는 /XO (옵션 칸 또는 환경 설정) 가 -u 와 같다
|
||||
final robo = t.method == 'robocopy' && !direct;
|
||||
final update = robo
|
||||
? skipNewer || splitOptions(t.options).any((o) => o.toUpperCase() == '/XO')
|
||||
: optionsUpdate(t.options) || (direct && skipNewer);
|
||||
String dstOf(String src) => t.contents ? t.dest : vJoin(t.dest, vBasename(src));
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
|
|
@ -227,7 +232,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
],
|
||||
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
|
||||
Text(effectiveRunText(t.method, t.options, move: t.move), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
|
||||
if (t.move)
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ void main() {
|
|||
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
|
||||
final a = p.join(tmp.path, 'ca'), b = p.join(tmp.path, 'cb');
|
||||
Directory(a).createSync(recursive: true);
|
||||
Directory(b).createSync(recursive: true);
|
||||
|
|
|
|||
|
|
@ -128,16 +128,17 @@ void main() {
|
|||
expect(job.leftInSource, 0);
|
||||
});
|
||||
|
||||
test('235: 이동 뒤 원본에 남은 것은 카드 기록에도 (남은 수 · 옮긴 수)', () async {
|
||||
// robocopy 가 건너뛰게: 받는 쪽이 더 새것 (/XO)
|
||||
test('235 · 229: robocopy 이동도 받는 쪽이 더 새것이면 건너뜀 (환경 설정 → /XO) · 남은 것은 카드 기록에도 (남은 수 · 옮긴 수)', () async {
|
||||
// 옵션 칸에는 /XO 가 없다 - 환경 설정 (skipNewer) 만으로 robocopy 가 건너뛴다
|
||||
final old = File(p.join(src, 'old.txt'))..writeAsStringSync('o');
|
||||
old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2)));
|
||||
File(p.join(dst, 'old.txt')).writeAsStringSync('new');
|
||||
final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, method: Platform.isWindows ? CopyMethod.robocopy : CopyMethod.builtin,
|
||||
options: Platform.isWindows ? '/E /XO' : '', skipNewer: true);
|
||||
options: Platform.isWindows ? '/E /COPY:DAT /DCOPY:T /R:2 /W:2' : '', skipNewer: true);
|
||||
await job.run();
|
||||
expect(job.error, isNull);
|
||||
expect(old.existsSync(), isTrue);
|
||||
expect(File(p.join(dst, 'old.txt')).readAsStringSync(), 'new', reason: '대상의 더 새 파일 그대로');
|
||||
expect(old.existsSync(), isTrue, reason: '건너뛴 원본은 남김');
|
||||
expect(job.leftInSource, 1);
|
||||
expect([job.doneFiles, job.allFiles], [job.allFiles - 1, job.allFiles], reason: '옮긴 수는 남은 것을 뺀다 (robocopy 는 다 했다고 셈)');
|
||||
final t = CopyTask(id: '1', sources: [src], dest: dst, move: true).copyWith(lastResult: 'done', lastFiles: job.doneFiles, lastLeft: job.leftInSource);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ void main() {
|
|||
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[2].sublist(0, 3), [r'C:\c', r'E:\dst', '3.mkv']);
|
||||
expect(robocopyIpg(0), 0);
|
||||
// 229: 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" → /XO (옵션 칸에 있으면 두 번 넣지 않음)
|
||||
expect(robocopyRuns(options: '/E', folders: [r'C:\a'], files: const [], dest: r'E:\d', skipNewer: true).single.last, '/XO');
|
||||
expect(robocopyRuns(options: '/E /xo', folders: [r'C:\a'], files: const [], dest: r'E:\d', skipNewer: true).single.where((o) => o.toUpperCase() == '/XO').length, 1);
|
||||
});
|
||||
|
||||
test('rsync 출력 읽기: 파일 줄만 (폴더 · 진행 · 요약 줄 뺌)', () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue