From 634fcbc920738c063c543107ea1d3b85bbc73a60 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:03:29 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=8C=8C=EC=9D=BC=20(--ex?= =?UTF-8?q?clude-from=20=C2=B7=20--include-from=20=C2=B7=20--files-from=20?= =?UTF-8?q?=C2=B7=20".=20=ED=8C=8C=EC=9D=BC"=20=C2=B7=20merge)=20=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=EB=8C=80=20=EA=B2=BD=EB=A1=9C=EB=8A=94=20=EB=A7=89?= =?UTF-8?q?=EC=9D=8C=20-=20"=EB=AA=A9=EB=A1=9D=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=80=20=EC=A0=84=EC=B2=B4=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=81=EC=96=B4=20=EC=A3=BC=EC=84=B8=EC=9A=94"=20(=EA=B2=80?= =?UTF-8?q?=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 판단 (10/20, 7160577 에 이어): rsync 는 자기 작업 폴더 (Windows 는 응답 파일 폴더) 기준으로, 미리 보기는 앱의 작업 폴더 기준으로 상대 경로를 읽어 서로 다른 목록 파일을 볼 수 있다. unreadableOptions 가 막는다 (옵션 칸 · 확인 창 · 모니터링 빨갛게, 실행 · 실시간 동기화 막음). - 대상: --exclude-from · --include-from · --files-from (= 와 다음 낱말), --filter / -f (붙여 · 다음 낱말 · 짧은 묶음 -avf) 의 ". 파일" · "merge[,고치개] 파일". 전체 경로 = C:\ · C:/ · \서버 · / (/cygdrive/… · Android). - 빼는 것: ": 이름" · dir-merge · -F (폴더마다 읽는 규칙 파일 이름 - 원래 상대), --exclude 의 값 (패턴). 시험: rsync_msys_args_test (막는 것 10 · 막지 않는 것 11). dismiss_test · live_sync_scene_test 의 merge 규칙을 전체 경로로. 거르기 · 맞추기 · 실시간 · 복사 시험 25개 파일 382개 통과. Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 1 + assets/l10n/ja.json | 1 + assets/l10n/zh-Hans.json | 1 + lib/core/sync_tools.dart | 43 ++++++++++++++++++++++++++++++++++ test/dismiss_test.dart | 4 ++-- test/live_sync_scene_test.dart | 2 +- test/rsync_msys_args_test.dart | 32 +++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 3 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d60b5f4..d54fe9a 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1238,6 +1238,7 @@ "목록": "list", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "Hides ad areas in lists · next to · on top of videos", "목록 보기": "View list", + "목록 파일은 전체 경로로 적어 주세요: {0}": "Please write the full path of the list file: {0}", "목록에 JJ_MKVMaker 가 없으면 [다른 앱 허용] 으로 JJ_MKVMaker.exe 를 고르고 \"개인\" 에 체크합니다. 바꾼 뒤 공유를 끄고 다시 켜세요.": "If JJ_MKVMaker is not in the list, use [Allow another app] to pick JJ_MKVMaker.exe and check \"Private\". Turn sharing off and on again afterwards.", "목록에서 동영상을 누르면 여기에 자세히 보입니다": "Tap a video in the list to see its details here", "목록에서 빼기": "Remove from list", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index b9a086d..da1155d 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1238,6 +1238,7 @@ "목록": "一覧", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "リスト・動画の横・動画の上に出る広告エリアを隠します", "목록 보기": "リストを見る", + "목록 파일은 전체 경로로 적어 주세요: {0}": "リストファイルはフルパスで書いてください: {0}", "목록에 JJ_MKVMaker 가 없으면 [다른 앱 허용] 으로 JJ_MKVMaker.exe 를 고르고 \"개인\" 에 체크합니다. 바꾼 뒤 공유를 끄고 다시 켜세요.": "一覧に JJ_MKVMaker がなければ [別のアプリの許可] で JJ_MKVMaker.exe を選び「プライベート」にチェックします。変更後は共有をオフにして再びオンにしてください。", "목록에서 동영상을 누르면 여기에 자세히 보입니다": "リストの動画をタップすると、ここに詳細が表示されます", "목록에서 빼기": "リストから外す", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 7c6be10..a42f8f3 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1238,6 +1238,7 @@ "목록": "列表", "목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다": "隐藏列表 · 视频旁 · 视频上方出现的广告区域", "목록 보기": "查看列表", + "목록 파일은 전체 경로로 적어 주세요: {0}": "请填写列表文件的完整路径: {0}", "목록에 JJ_MKVMaker 가 없으면 [다른 앱 허용] 으로 JJ_MKVMaker.exe 를 고르고 \"개인\" 에 체크합니다. 바꾼 뒤 공유를 끄고 다시 켜세요.": "如果列表中没有 JJ_MKVMaker,请用 [允许其他应用] 选择 JJ_MKVMaker.exe 并勾选“专用”。更改后请关闭共享再重新开启。", "목록에서 동영상을 누르면 여기에 자세히 보입니다": "点按列表中的视频即可在此查看详情", "목록에서 빼기": "从列表中移除", diff --git a/lib/core/sync_tools.dart b/lib/core/sync_tools.dart index 3782ece..22ef2dc 100644 --- a/lib/core/sync_tools.dart +++ b/lib/core/sync_tools.dart @@ -235,9 +235,52 @@ List unreadableOptions(String options, {bool? windows}) { } // 값이 필요한 옵션이 끝에 값 없이 (--exclude) - rsync 가 원본 경로를 그 값으로 먹는다 if (value) bad.add(trf('{0} 의 값이 없습니다', [last])); + // 관리자 판단 (10/20): 목록 파일의 상대 경로 - rsync 는 자기 작업 폴더 (Windows 는 응답 파일 폴더) 기준으로, 미리 보기는 앱의 + // 작업 폴더 기준으로 읽어 서로 다른 파일을 볼 수 있다. 전체 경로만 받는다 + for (final f in listFileValues(parsed.words)) { + if (!_absolutePath(f)) bad.add(trf('목록 파일은 전체 경로로 적어 주세요: {0}', [f])); + } return bad; } +/// 파일을 받는 옵션의 값 (--exclude-from · --include-from · --files-from · --filter / -f 의 ". 파일" · "merge 파일"). +/// ": 이름" · dir-merge 는 폴더마다 읽는 파일 이름이라 (원래 상대) 빼고 +List listFileValues(List words) { + const fileOpts = {'--exclude-from', '--include-from', '--files-from'}; + final out = []; + String? mergeFile(String rule) => RegExp(r'^(?:\.|merge)(?:,[^\s]*)?\s+(.+)$').firstMatch(rule.trim())?.group(1); + for (var i = 0; i < words.length; i++) { + final w = words[i]; + final next = i + 1 < words.length ? words[i + 1] : null; + final eq = w.indexOf('='); + if (w.startsWith('--') && eq > 0 && fileOpts.contains(w.substring(0, eq))) { + out.add(w.substring(eq + 1)); + } else if (fileOpts.contains(w) && next != null) { + out.add(next); + i++; + } else if (w.startsWith('--filter=')) { + final f = mergeFile(w.substring(9)); + if (f != null) out.add(f); + } else if (w == '--filter' && next != null) { + final f = mergeFile(next); + if (f != null) out.add(f); + i++; + } else if (!w.startsWith('--') && w.length > 1 && w.startsWith('-')) { + // 짧은 옵션 묶음의 f: 붙은 값 (-f". 목록") 또는 다음 낱말 (-avf ". 목록") + final k = w.indexOf(RegExp('[BefMT@]'), 1); + if (k < 0 || w[k] != 'f') continue; + final rule = k == w.length - 1 ? next : w.substring(k + 1); + if (k == w.length - 1) i++; + final f = rule == null ? null : mergeFile(rule); + if (f != null) out.add(f); + } + } + return out; +} + +/// 전체 경로 (C:\ · C:/ · \\서버 · / 로 시작 - /cygdrive/c/… 와 Android 경로 포함) +bool _absolutePath(String f) => RegExp(r'^(?:[A-Za-z]:[\\/]|[\\/])').hasMatch(f); + /// Windows 경로 → MSYS2 rsync 가 읽는 경로 (C:\a\b → /cygdrive/c/a/b, \\server\share\x → //server/share/x). /// MSYS2 rsync 는 "C:" 를 원격 컴퓨터 이름으로 읽으므로 꼭 바꿔야 한다. String toCygwinPath(String path, {bool windows = true}) { diff --git a/test/dismiss_test.dart b/test/dismiss_test.dart index 7704d52..e5400e3 100644 --- a/test/dismiss_test.dart +++ b/test/dismiss_test.dart @@ -229,11 +229,11 @@ void main() { ..writeAsStringSync('o'); bool? r; // 관리자 지시 (10/20): H 는 이제 미리 볼 수 있다 - 아직 따르지 못하는 merge 규칙으로 - final t = CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --filter="merge rules.txt"'); + final t = CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --filter="merge C:/jj_rules/rules.txt"'); // 목록 파일은 전체 경로 (관리자 판단 10/20) await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, t)); await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200); expect(find.textContaining('미리 볼 수 없습니다'), findsOneWidget); - expect(find.textContaining('merge rules.txt'), findsWidgets); + expect(find.textContaining('merge C:/jj_rules/rules.txt'), findsWidgets); FilledButton run() => tester.widget(find.widgetWithText(FilledButton, '실행')); expect(run().onPressed, isNull, reason: '체크 전에는 실행할 수 없음'); await tester.tap(find.text('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)')); diff --git a/test/live_sync_scene_test.dart b/test/live_sync_scene_test.dart index d94d63b..6834bd8 100644 --- a/test/live_sync_scene_test.dart +++ b/test/live_sync_scene_test.dart @@ -311,7 +311,7 @@ void main() { testWidgets('[지금 지우기 (확인)] 창: 빨간 "미리 볼 수 없음" · 체크해야 [지우고 맞추기] · [지우지 않기] 는 없음', (tester) async { if (sceneRsync == null) return markTestSkipped('앱에 든 rsync 가 없음'); - c.settings.rsyncOptions = '-a --filter="merge rules.txt"'; // 아직 따르지 못하는 규칙 (H 는 이제 따름) + c.settings.rsyncOptions = '-a --filter="merge C:/jj_rules/rules.txt"'; // 아직 따르지 못하는 규칙 (H 는 이제 따름) · 목록 파일은 전체 경로 final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true); c.settings.liveSyncPairs = [pair]; final live = LiveSync(c); diff --git a/test/rsync_msys_args_test.dart b/test/rsync_msys_args_test.dart index f782fb8..fcc6299 100644 --- a/test/rsync_msys_args_test.dart +++ b/test/rsync_msys_args_test.dart @@ -36,6 +36,38 @@ void main() { expect(unreadableOptions('-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"', windows: true), isEmpty); }); + test('관리자 판단 (10/20): 목록 파일의 상대 경로는 막는다 (rsync 와 미리 보기가 다른 작업 폴더 기준으로 읽음) - 전체 경로 · 폴더마다 읽는 규칙은 그대로', () { + for (final o in [ + '-a --exclude-from=list.txt', + '-a --exclude-from list.txt', + '-a --include-from=./a.txt', + '-a --files-from=sub/files.txt', + '-a --filter=". rules.txt"', + '-a --filter="merge,- rules.txt"', + '-a --filter ". w.txt"', + '-a -f". y.txt"', + '-avf ". z.txt"', + '-a --exclude-from=~/list.txt', + ]) { + expect(unreadableOptions(o, windows: true), [contains('목록 파일은 전체 경로로 적어 주세요')], reason: o); + } + for (final o in [ + r'-a --exclude-from=C:\lists\l.txt', + '-a --exclude-from=C:/lists/l.txt', + '-a --include-from=/cygdrive/c/l.txt', + r'-a --files-from=\\nas\share\l.txt', + '-a --exclude-from /sdcard/l.txt', + '-a --filter=". /cygdrive/c/rules.txt"', + r'-a -f". C:\r.txt"', + '-a --filter=": .rsync-filter"', + '-a --filter="dir-merge .x"', + '-a -F', + '-a --exclude=list.txt', + ]) { + expect(unreadableOptions(o, windows: true), isEmpty, reason: o); + } + }); + group('실제 rsync', () { final rsync = sceneRsync; late Directory tmp;