modular_sync #3 OptionText: "rsync 옵션이 있나" 를 popt 규칙 하나로 (rsyncHasOption) - 거르기 값의 u 를 -u 로 오인하던 것 (데이터)
- rsync_options.dart rsyncHasOption(words, short, long): 짧은 묶음은 한 글자씩, 값 자리 (값을 받는 글자 B e f M T @ 의 뒤 ·
값을 받는 긴 옵션의 다음 낱말) 는 옵션으로 보지 않음. rsyncShortWithArg 는 거르기 해석 (rsync_filter) 과 함께 씀
- "-u 가 있나" 세 곳 (transfer_job._update · withUpdateOption · optionsUpdate) 이 이것을 부름. 예전에는 "-로 시작하는 낱말에 u 가 있으면"
이라 -f'- build/' · -f "- run/" 를 -u 로 읽었다: 미리 보기는 "받는 쪽이 더 새것은 건너뜀", 실제 rsync 는 덮어씀 · 앱이 맞출 때도 건너뜀
- 고치기 전 판 (512d290) 에서 같은 시험이 틀리게 나옴을 먼저 확인: 판단 [true, true, true] (기대 [true, false, false]) ·
실제 rsync 는 -avf'- build/' 에서 build.txt 를 보내는데 앱은 -u 로 봄 (c3_before.log). 고친 판에서는 2회 통과
- 약속 시험: 판단 표 + 실제 rsync (받는 쪽이 더 새 파일, 옵션 5가지) - 앱의 -u 판단 = rsync 가 보내는지
사용자가 보게 될 동작: rsync 옵션의 거르기 값에 u 가 들어 있어도 "받는 쪽이 더 새것은 건너뜀" 으로 잘못 보거나 앱이 맞출 때 건너뛰지 않음 (실제 rsync 와 같게)
확인: analyze 0 · 단위 1486 ×2 · 통합 6/6 ×2 · base10 214장 0px · rsync_page ×2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
512d2904d2
commit
bdea662daa
|
|
@ -252,7 +252,7 @@ class TransferJob extends ChangeNotifier {
|
|||
bool get viaWebDav => isDav(dest) || sources.any(isDav);
|
||||
|
||||
/// rsync -u (--update) 를 옵션에 넣었는지 (앱이 맞출 때도 같은 뜻으로: 대상이 더 새것이면 건너뜀)
|
||||
bool get _update => splitOptions(options).any((o) => o == '--update' || (o.startsWith('-') && !o.startsWith('--') && o.contains('u')));
|
||||
bool get _update => rsyncHasOption(splitOptions(options), short: 'u', long: '--update');
|
||||
|
||||
/// 100: WebDAV 를 앱이 맞출 때도 rsync 의 지우기 (--delete 계열) 를 따른다 (확인 창에서 지울 목록을 본 뒤에만 실행됨)
|
||||
bool get _delete => method == CopyMethod.rsync && splitOptions(options).any(isRsyncDeleteOption);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:convert' show utf8;
|
||||
import 'dart:io' show File, Platform;
|
||||
|
||||
import 'sync_tools.dart' show splitOptions;
|
||||
import 'sync_tools.dart' show splitOptions, rsyncShortWithArg;
|
||||
|
||||
/// 177: rsync 옵션의 거르기 규칙 (--exclude · --include · --exclude-from · --include-from · --filter "- …" / "+ …")
|
||||
/// 을 실행 전 비교 (미리 보기 · 지울 목록) 에도 똑같이 적용한다. 예전에는 미리 보기가 이 규칙을 보지 않아,
|
||||
|
|
@ -137,7 +137,7 @@ class RsyncFilter {
|
|||
// 예전에는 -f 가 맨 앞일 때만 읽고, 묶음 안의 C · F 를 패턴 글자까지 뒤져 잘못 알렸다
|
||||
for (var j = 1; j < o.length; j++) {
|
||||
final ch = o[j];
|
||||
if ('BefMT@'.contains(ch)) {
|
||||
if (rsyncShortWithArg.contains(ch)) {
|
||||
var arg = o.substring(j + 1);
|
||||
if (arg.isEmpty && i + 1 < t.length) arg = t[++i];
|
||||
if (ch == 'f') filterRule(arg);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ String toCygwinPath(String path, {bool windows = true}) {
|
|||
/// rsync 옵션에 -u (--update: 받는 쪽이 더 새 파일은 건너뜀) 를 더한다. 이미 있으면 그대로 (-avu 처럼 묶인 것도).
|
||||
/// 양쪽 (⇄) 으로 함께 맞출 때 서로의 새 파일을 옛 파일로 덮어쓰지 않게.
|
||||
String withUpdateOption(String options) {
|
||||
final has = splitOptions(options).any((o) => o == '--update' || (o.startsWith('-') && !o.startsWith('--') && o.contains('u')));
|
||||
final has = rsyncHasOption(splitOptions(options), short: 'u', long: '--update');
|
||||
return has ? options : '${options.trim()} -u'.trim();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,45 @@ const _curlyQuote = {'\u2018': "'", '\u2019': "'", '\u201A': "'", '\u201C': '"',
|
|||
/// 옵션의 값 자리 (--exclude –x.txt 의 –x.txt) · 낱말 가운데의 대시는 그대로 (파일 이름). --filter · -f 값의 규칙 머리 (“– *.tmp”) 는 - 로.
|
||||
List<String> splitOptions(String s) => _parseOptions(s).words;
|
||||
|
||||
/// 짧은 옵션 묶음에서 값을 받는 rsync 글자 (popt): 그 글자 뒤의 글 (비었으면 다음 낱말) 이 값 - -avf"- *.log" · -rf "- x"
|
||||
const rsyncShortWithArg = 'BefMT@';
|
||||
|
||||
/// = 없이 다음 낱말을 값으로 받는 rsync 긴 옵션 (--exclude x · --filter "- y")
|
||||
const _rsyncLongWithArg = {
|
||||
'address', 'backup-dir', 'block-size', 'bwlimit', 'checksum-choice', 'checksum-seed', 'chmod', 'chown', 'compare-dest',
|
||||
'compress-choice', 'compress-level', 'contimeout', 'copy-as', 'copy-dest', 'debug', 'early-input', 'exclude', 'exclude-from',
|
||||
'files-from', 'filter', 'groupmap', 'iconv', 'include', 'include-from', 'info', 'link-dest', 'log-file', 'log-file-format',
|
||||
'max-alloc', 'max-delete', 'max-size', 'min-size', 'modify-window', 'only-write-batch', 'out-format', 'outbuf', 'partial-dir',
|
||||
'password-file', 'port', 'protocol', 'read-batch', 'remote-option', 'rsh', 'rsync-path', 'skip-compress', 'sockopts',
|
||||
'stop-after', 'stop-at', 'suffix', 'temp-dir', 'timeout', 'usermap', 'write-batch',
|
||||
};
|
||||
|
||||
/// modular_sync #3 OptionText: 옵션 낱말들 ([splitOptions]) 에 rsync 옵션 [short] (한 글자, 짧은 묶음 안에서도) 또는 [long] (--이름) 이 있는지.
|
||||
/// popt 처럼 읽는다 - 짧은 묶음은 한 글자씩, 값 자리 (값을 받는 글자 뒤 · 값을 받는 옵션의 다음 낱말) 는 옵션으로 보지 않는다.
|
||||
/// 예전에는 "-로 시작하는 낱말에 그 글자가 있으면" 으로 봐서 -f'- build/' 의 u 를 -u 로 읽었다 (미리 보기 ≠ 실제)
|
||||
bool rsyncHasOption(List<String> words, {String? short, String? long}) {
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
final o = words[i];
|
||||
if (o.startsWith('--')) {
|
||||
final eq = o.indexOf('=');
|
||||
final name = eq < 0 ? o : o.substring(0, eq);
|
||||
if (long != null && name == long) return true;
|
||||
if (eq < 0 && _rsyncLongWithArg.contains(name.substring(2))) i++;
|
||||
continue;
|
||||
}
|
||||
if (o.length < 2 || !o.startsWith('-')) continue;
|
||||
for (var j = 1; j < o.length; j++) {
|
||||
final ch = o[j];
|
||||
if (short != null && ch == short) return true;
|
||||
if (rsyncShortWithArg.contains(ch)) {
|
||||
if (j == o.length - 1) i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// rsync 3.x 의 긴 옵션 이름 (앱에 든 rsync 3.5.1 의 --help) - 대시 하나로 붙여 넣은 –delete 를 --delete 로 읽을지 정한다
|
||||
const _rsyncLong = {
|
||||
'acls', 'address', 'append', 'append-verify', 'archive', 'atimes', 'backup', 'backup-dir', 'block-size', 'blocking-io',
|
||||
|
|
|
|||
|
|
@ -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, CopyMethod, robocopyExtraOptions;
|
||||
import 'sync_tools.dart' show splitOptions, isSourceRemovingOption, CopyMethod, robocopyExtraOptions, rsyncHasOption;
|
||||
|
||||
export 'sync_tools.dart' show isSourceRemovingOption;
|
||||
import 'vfs.dart';
|
||||
|
|
@ -119,7 +119,7 @@ bool optionsRemoveSource(String options) => splitOptions(options).any(isSourceRe
|
|||
|
||||
/// rsync 옵션에 -u (받는 쪽이 더 새 파일은 건너뜀) 가 있는지
|
||||
bool optionsUpdate(String options) =>
|
||||
splitOptions(options).any((o) => o == '--update' || (o.startsWith('-') && !o.startsWith('--') && o.contains('u')));
|
||||
rsyncHasOption(splitOptions(options), short: 'u', long: '--update');
|
||||
|
||||
/// [src] 안의 것 → [dst] 안으로 맞출 때 바뀌는 것. 원본을 읽지 못한 폴더에서는 "지워짐" 을 세지 않는다.
|
||||
/// [limit] 개를 넘으면 멈춘다 (화면에는 "… 이상").
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart' show optionsUpdate;
|
||||
import 'package:jj_mkvmaker/core/sync_tools.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// modular_sync #3 OptionText 약속: 옵션 글에 rsync 옵션이 있는지는 popt 규칙 하나 (rsyncHasOption) - 값 자리는 옵션으로 보지 않는다.
|
||||
/// 예전 -u 확인 세 곳은 "-로 시작하는 낱말에 u 가 있으면" 이라 -f'- build/' 의 u 를 -u 로 읽었다 (미리 보기는 "더 새것 건너뜀", 실제는 덮어씀)
|
||||
void main() {
|
||||
test('판단 표: 짧은 묶음 · 값을 받는 글자 뒤 · 값을 받는 옵션의 다음 낱말 · 긴 옵션 · = 값', () {
|
||||
bool u(String opts) => rsyncHasOption(splitOptions(opts), short: 'u', long: '--update');
|
||||
expect([u('-avu'), u('-uav'), u('-a --update'), u('-a -u')], [true, true, true, true]);
|
||||
expect([u('-avHPOg'), u("-avf'- build/'"), u('-av -f "- run/"'), u('--exclude -u.txt -a'), u('--filter "- u*"'), u('--exclude=-u')],
|
||||
[false, false, false, false, false, false]);
|
||||
expect(u("-avf'- x' -u"), isTrue, reason: '값 뒤의 진짜 -u');
|
||||
expect([optionsUpdate('-avu'), optionsUpdate("-avf'- build/'")], [true, false], reason: '미리 보기가 쓰는 판단도 같은 하나');
|
||||
expect(withUpdateOption("-avf'- build/'"), "-avf'- build/' -u", reason: '⇄ 에서 -u 를 더할 때도 값의 u 를 -u 로 보지 않음');
|
||||
});
|
||||
|
||||
test('실제 rsync: 받는 쪽이 더 새 파일 - 옵션 글마다 앱의 -u 판단과 rsync 가 보내는지가 맞음', () async {
|
||||
final rsync = p.join(Directory.current.path, 'third_party', 'rsync', 'windows', 'rsync.exe');
|
||||
if (!Platform.isWindows || !File(rsync).existsSync()) return markTestSkipped('rsync 없음');
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_opttext_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final now = DateTime.now();
|
||||
File(p.join(tmp.path, 's', 'build.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('old')
|
||||
..setLastModifiedSync(now.subtract(const Duration(hours: 1)));
|
||||
File(p.join(tmp.path, 'd', 'build.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('NEW!')
|
||||
..setLastModifiedSync(now);
|
||||
// 거르기 값에 u 가 든 경우 (- build/ · - run/) 가 예전에 -u 로 오인되던 것 (build.txt 는 거르지 않음)
|
||||
for (final opts in ['-av', '-avu', "-avf'- build/'", "-avuf'- build/'", '-av -f "- run/"']) {
|
||||
final args = rsyncArgs(options: opts, sources: [p.join(tmp.path, 's')], dest: p.join(tmp.path, 'd'), contents: true);
|
||||
final r = await Process.run(rsync, ['--dry-run', '--out-format=%n', ...args]);
|
||||
expect(r.exitCode, 0, reason: '$opts: ${r.stderr}');
|
||||
final sends = '${r.stdout}'.split('\n').any((l) => l.trim() == 'build.txt');
|
||||
expect(sends, !optionsUpdate(opts), reason: '$opts: rsync 가 보내면 -u 아님 (${r.stdout})');
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue