diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 9469ef0..42bb5c6 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1305,6 +1305,7 @@ "받다 만 AI 모델": "Unfinished AI models", "받다 만 다운로드": "Unfinished downloads", "받다 만 조각 · {0}": "Partial download · {0}", + "받다 만 파일 {0}개도 지워집니다": "{0} partially downloaded files are deleted too", "받아서 열기": "Download and open", "받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "Downloaded AI models ({0}) - Download/JJ_MKVMaker/AI 모델 보관", "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "Copy downloaded AI models ({0}) to Download/JJ_MKVMaker and restore them after reinstalling (needs {0} more space for a while)", @@ -1716,6 +1717,7 @@ "안쪽 파일을 확인하는 중 …": "Checking files inside …", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "{0} files inside are newer on the receiving side - they are skipped when merging (they stay in the source)", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "{0} files inside are newer on the receiving side - merging overwrites them with older ones", + "알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "I understand - run anyway (the delete list may differ from what actually happens)", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)", "암호 ZIP 으로 묶는 중 · {0}": "Making password ZIP · {0}", "암호로 묶기": "Make password ZIP", @@ -2023,6 +2025,7 @@ "이 언어 지우기": "Remove this language", "이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "This connection is neither signed nor encrypted ({0}). Others on the same network could read or change the files, so it's best to turn on SMB signing or SMB3 encryption in the NAS settings", "이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "This connection isn't encrypted ({0}). Others on the same network could see file contents, so it's best to turn on SMB3 encryption in the NAS settings", + "이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "Some filter rules in these options can't be previewed - more may actually be deleted: {0}", "이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.", "이 이동 버튼 지우기": "Delete this move button", "이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "These options will be used the next time this move runs. Run it now with the new options?", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 591f7f0..3d6a2f1 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1305,6 +1305,7 @@ "받다 만 AI 모델": "ダウンロード途中の AI モデル", "받다 만 다운로드": "ダウンロード途中のファイル", "받다 만 조각 · {0}": "途中までのダウンロード · {0}", + "받다 만 파일 {0}개도 지워집니다": "途中までの受信ファイル {0} 件も削除されます", "받아서 열기": "ダウンロードして開く", "받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "ダウンロードした AI モデル ({0}) - Download/JJ_MKVMaker/AI 모델 보관", "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "ダウンロードした AI モデル ({0}) を Download/JJ_MKVMaker にコピーし、再インストール後に復元する (一時的に {0} 余分に必要)", @@ -1716,6 +1717,7 @@ "안쪽 파일을 확인하는 중 …": "中のファイルを確認中 …", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせてもスキップします (元に残ります)", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "中に受け取る側の方が新しいファイルが {0} 件 - 合わせると古いもので上書きします", + "알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "理解した上で実行します (削除リストが実際と異なる場合があります)", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)", "암호 ZIP 으로 묶는 중 · {0}": "パスワード付き ZIP にまとめています · {0}", "암호로 묶기": "パスワード付きでまとめる", @@ -2023,6 +2025,7 @@ "이 언어 지우기": "この言語を削除", "이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "この接続は署名も暗号化もされません ({0})。同じネットワークの他の人がファイルの内容を見たり変えたりできるため、NAS の設定で SMB 署名か SMB3 暗号化をオンにすることをおすすめします", "이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "この接続は暗号化されていません ({0})。同じネットワークの他の人がファイルの内容を見られるため、NAS の設定で SMB3 暗号化をオンにすることをお勧めします", + "이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "このオプションの一部のフィルター規則はプレビューできません - 実際にはさらに削除される場合があります: {0}", "이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。", "이 이동 버튼 지우기": "この移動ボタンを削除", "이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 5c1841d..1add3ca 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1305,6 +1305,7 @@ "받다 만 AI 모델": "未下载完的 AI 模型", "받다 만 다운로드": "未完成的下载", "받다 만 조각 · {0}": "未完成的下载 · {0}", + "받다 만 파일 {0}개도 지워집니다": "{0} 个未完成的文件也会被删除", "받아서 열기": "下载后打开", "받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "已下载的 AI 模型 ({0}) - Download/JJ_MKVMaker/AI 모델 보관", "받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "将已下载的 AI 模型 ({0}) 复制到 Download/JJ_MKVMaker,重新安装后恢复 (暂时需要额外 {0})", @@ -1716,6 +1717,7 @@ "안쪽 파일을 확인하는 중 …": "正在检查里面的文件 …", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)": "里面有 {0} 个文件在接收端较新 - 合并时会跳过(保留在源中)", "안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다": "里面有 {0} 个文件在接收端较新 - 合并会用旧文件覆盖", + "알고 실행합니다 (지울 목록이 실제와 다를 수 있음)": "我已了解并执行 (删除列表可能与实际不同)", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)", "암호 ZIP 으로 묶는 중 · {0}": "正在创建加密 ZIP · {0}", "암호로 묶기": "加密压缩", @@ -2023,6 +2025,7 @@ "이 언어 지우기": "删除此语言", "이 연결은 서명도 암호화도 되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 보거나 바꿀 수 있으니, NAS 설정에서 SMB 서명이나 SMB3 암호화를 켜는 것이 좋습니다": "此连接既未签名也未加密 ({0})。同一网络中的其他人可能查看或篡改文件内容,建议在 NAS 设置中开启 SMB 签名或 SMB3 加密", "이 연결은 암호화되지 않습니다 ({0}). 같은 네트워크의 다른 사람이 파일 내용을 볼 수 있으니, NAS 설정에서 SMB3 암호화를 켜는 것이 좋습니다": "此连接未加密 ({0})。同一网络中的其他人可能看到文件内容,建议在 NAS 设置中开启 SMB3 加密", + "이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}": "这些选项中的部分过滤规则无法预览 - 实际可能会删除更多: {0}", "이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。", "이 이동 버튼 지우기": "删除此移动按钮", "이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?", diff --git a/lib/core/rsync_filter.dart b/lib/core/rsync_filter.dart index a1ef1a0..ce5eee3 100644 --- a/lib/core/rsync_filter.dart +++ b/lib/core/rsync_filter.dart @@ -46,6 +46,11 @@ class RsyncFilter { for (final l in lines) { final s = l.trimRight(); if (s.isEmpty || s.startsWith('#') || s.startsWith(';')) continue; + // 15 검토 F3: "!" (목록 지우기) · 다른 규칙 머리 (P · H · R · S · 고치개 · merge) 는 미리 볼 수 없다 - 적용하지 않고 알린다 + if (s == '!' || RegExp(r'^([PHRS]|[-+],[^ ]*|[.:]|merge|dir-merge)(\s|$)').hasMatch(s)) { + unsupported.add('${include ? '--include-from' : '--exclude-from'}: $s'); + continue; + } // 파일 안에서도 "+ " · "- " 머리를 따른다 if (s.startsWith('+ ')) { rules.add(RsyncFilterRule(s.substring(2), include: true)); @@ -58,7 +63,10 @@ class RsyncFilter { } void filterRule(String r) { - final m = RegExp(r'^(\+|-|include|exclude)(?:,[a-z!]*)?\s+(.+)$').firstMatch(r.trim()); + // 15 검토 F1: 고치개 (",s" 보내는 쪽만 · ",r" 받는 쪽만 · ",!" 반대 · ",p" 등) 가 붙은 규칙은 뜻이 달라 + // (예: "-,s a.txt" 는 받는 쪽 a.txt 를 지키지 않아 --delete 로 지워진다) 미리 볼 수 없다 - 적용하지 않고 알린다. + // H · S · P · R · merge 도 같다 + final m = RegExp(r'^(\+|-|include|exclude)\s+(.+)$').firstMatch(r.trim()); if (m == null) { unsupported.add('--filter=$r'); return; diff --git a/lib/core/sync_preview.dart b/lib/core/sync_preview.dart index e3d0368..28c17c1 100644 --- a/lib/core/sync_preview.dart +++ b/lib/core/sync_preview.dart @@ -115,7 +115,7 @@ Future> previewSync(String src, String dst, for (final x in items) { if (out.length >= limit) return; final name = vBasename(x.path); - if (isPartialFile(name) || isAppTempName(name)) continue; + if (isAppTempName(name)) continue; final r = '$rel/$name'; if (filter.excluded(r, isDir: x.isDir)) continue; if (x.isDir && await _hasExcluded(x.path, r, filter)) { @@ -179,7 +179,8 @@ Future> previewSync(String src, String dst, } for (final e in there.values) { final name = vBasename(e.path); - if (names.contains(name) || isPartialFile(name) || isAppTempName(name)) continue; + // 15 검토 F4: 받는 쪽에만 있는 받다 만 파일 (*.part 등) 도 --delete 면 지워진다 - 지울 목록에 (지우지 않으면 셀 것 없음) + if (names.contains(name) || isAppTempName(name) || (isPartialFile(name) && !delete)) continue; final r = rel.isEmpty ? name : '$rel/$name'; if (filter.excluded(r, isDir: e.isDir)) { // 거른 것은 rsync 가 건드리지 않는다 (--delete-excluded 와 --delete 면 지운다) diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index fc06ae2..d0917de 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -3375,6 +3375,9 @@ class _ExplorerPageState extends State { // 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에) List? preview; Object? previewError; + // 15 검토 F2: 미리 볼 수 없는 거르기 규칙을 알고 실행하는지 (지우기가 들어 있을 때) + var filterAck = false; + List filterLimits() => _viaDav([l, r]) ? const [] : {for (final t in tasks) ...RsyncFilter.parse(opts(t)).unsupported}.toList(); // 93: 지우는 실행 (--delete 등. 100: WebDAV 도 따른다) 은 비교가 끝나 지울 목록이 보인 뒤에만 bool deletes() => tasks.any((t) => optionsDelete(opts(t))); // 105: 원본이 대상 안인데 지우기가 들어가면 막는다 - 창 안에서 옵션이 바뀔 때 (원본 파일 지우기) 와 실행 직전에도 본다 @@ -3450,6 +3453,8 @@ class _ExplorerPageState extends State { ], // 72: 무엇이 바뀌는지 파일별로 (→ · ← · 받는 쪽에만 있음 · 지워짐) const SizedBox(height: 8), + if (filterLimits().isNotEmpty) + FilterLimitNote(unsupported: filterLimits(), deletes: deletes(), ack: filterAck, onAck: (v) => set(() => filterAck = v)), SyncPreviewView( // 옵션이 바뀌면 (원본 파일 지우기) 다시 비교 key: ValueKey([for (var i = 0; i < tasks.length; i++) runOpts(i)].join('|')), @@ -3507,7 +3512,9 @@ class _ExplorerPageState extends State { final dels = preview?.where((x) => x.action == PreviewAction.delete).length ?? 0; return FilledButton( style: dels > 0 ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null, - onPressed: nestedDelete() || (deletes() && preview == null) ? null : () => Navigator.pop(ctx, true), + onPressed: nestedDelete() || (deletes() && preview == null) || (deletes() && filterLimits().isNotEmpty && !filterAck) + ? null + : () => Navigator.pop(ctx, true), // 110: 글자는 늘 같게 (비교가 끝나 글이 길어지면 버튼 배치가 바뀌어 [취소] 를 누르려다 [실행] 을 누르게 됨). // 지울 수는 위의 빨간 글 "받는 쪽에서 N개가 지워집니다" 로 보인다 child: Text(removeSource ? tr('이동') : tr('실행')), diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index fff0ed7..89808c8 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../app/settings.dart' show CopyTask; -import '../core/file_ops.dart' show formatSize; +import '../core/file_ops.dart' show formatSize, isPartialFile; import '../core/rsync_filter.dart'; import '../core/sync_preview.dart'; import '../core/sync_tools.dart'; @@ -98,6 +98,7 @@ class _SyncPreviewViewState extends State { int n(PreviewAction a, [bool? right]) => items.where((x) => x.action == a && (right == null || x.toRight == right)).length; final dels = n(PreviewAction.delete); + final partials = items.where((x) => x.action == PreviewAction.delete && isPartialFile(x.rel)).length; final overwrites = n(PreviewAction.update); final overNewer = items.where((x) => x.action == PreviewAction.update && x.newer).length; final skipped = n(PreviewAction.skipNewer); @@ -124,6 +125,10 @@ class _SyncPreviewViewState extends State { if (dels > 0) Text(trf('받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)', [dels]), style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), + // 15 검토 F4: 받는 쪽에만 있는 받다 만 파일 (*.jjpart 등) 도 지워진다는 것을 따로 + if (partials > 0) + Text(trf('받다 만 파일 {0}개도 지워집니다', [partials]), + style: const TextStyle(fontSize: 12, color: Colors.redAccent)), // 211: 내용이 다른 같은 이름은 받는 쪽 파일이 바뀐다 - 목록의 "~" 만으로는 알아채기 어려웠다 if (overwrites > 0) Text(trf('받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)', [overwrites]), @@ -206,6 +211,7 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM if (!deletes && !moves) return true; List? preview; Object? error; + var filterAck = false; final direct = t.method == 'builtin' || [...t.sources, t.dest].any(isDav); // robocopy 는 /XO (옵션 칸 또는 환경 설정) 가 -u 와 같다 final robo = t.method == 'robocopy' && !direct; @@ -242,6 +248,9 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), ), const SizedBox(height: 8), + // 15 검토 F2: 미리 볼 수 없는 거르기 규칙 (늘 같은 자리 - 비교 결과와 상관없이 옵션으로 정해짐) + if (filter.unsupported.isNotEmpty) + FilterLimitNote(unsupported: filter.unsupported, deletes: deletes, ack: filterAck, onAck: (v) => set(() => filterAck = v)), SyncPreviewView( compute: () async => [ for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update, @@ -279,7 +288,7 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), FilledButton( style: dels > 0 || moves ? FilledButton.styleFrom(backgroundColor: JjColors.danger) : null, - onPressed: preview == null ? null : () => Navigator.pop(ctx, true), + onPressed: preview == null || (deletes && filter.unsupported.isNotEmpty && !filterAck) ? null : () => Navigator.pop(ctx, true), child: Text(tr('실행')), // 110: 글자를 바꾸지 않는다 (버튼 자리가 움직이지 않게) ), ], @@ -289,3 +298,28 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM ); return ok == true; } + +/// 15 검토 F2 (177): rsync 옵션에 미리 볼 수 없는 거르기 규칙 (고치개 · H · P · merge · -F · -C · 읽지 못한 파일 등) 이 있으면 +/// 지울 목록이 실제보다 적게 보일 수 있다 - 빨갛게 알리고, 지우기 (--delete) 와 함께면 체크해야 [실행] 이 켜진다 (72 · 104 와 같게) +class FilterLimitNote extends StatelessWidget { + final List unsupported; + final bool deletes; + final bool ack; + final ValueChanged onAck; + const FilterLimitNote({super.key, required this.unsupported, required this.deletes, required this.ack, required this.onAck}); + + @override + Widget build(BuildContext context) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(trf('이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}', [unsupported.join(' · ')]), + style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), + if (deletes) + CheckboxListTile( + dense: true, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + value: ack, + onChanged: (v) => onAck(v ?? false), + title: Text(tr('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)'), style: const TextStyle(fontSize: 12)), + ), + ]); +} diff --git a/test/dismiss_test.dart b/test/dismiss_test.dart index 3e94662..cd14d23 100644 --- a/test/dismiss_test.dart +++ b/test/dismiss_test.dart @@ -219,6 +219,38 @@ void main() { await tester.pumpAndSettle(); }); + testWidgets('177 · 15 검토 F2: 미리 볼 수 없는 거르기 규칙 + --delete 면 빨간 경고 · 체크해야 [실행] 이 켜진다', (tester) async { + final src = p.join(tmp.path, 'fs'), dst = p.join(tmp.path, 'fd'); + File(p.join(src, 'a.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('a'); + File(p.join(dst, 'old.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('o'); + bool? r; + final t = CopyTask(id: '1', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --filter="H a.txt"'); + 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('H a.txt'), findsWidgets); + FilledButton run() => tester.widget(find.widgetWithText(FilledButton, '실행')); + expect(run().onPressed, isNull, reason: '체크 전에는 실행할 수 없음'); + await tester.tap(find.text('알고 실행합니다 (지울 목록이 실제와 다를 수 있음)')); + await tester.pump(); + expect(run().onPressed, isNotNull); + await tester.tap(find.text('취소').last); + await tester.pumpAndSettle(); + expect(r, isFalse); + // 따를 수 있는 규칙만 있으면 경고 · 체크 없음 + final ok = CopyTask(id: '2', sources: [src], dest: dst, contents: true, method: 'rsync', options: '-av --delete --exclude=*.log'); + await host(tester, (ctx) async => r = await confirmDeletingRun(ctx, ok)); + await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 200); + expect(find.textContaining('미리 볼 수 없습니다'), findsNothing); + expect(run().onPressed, isNotNull); + await tester.tap(find.text('취소').last); + await tester.pumpAndSettle(); + }); + testWidgets('236: 모니터링 카드 [실행] 뒤 옵션 칸에 포커스가 남지 않는다 (키보드가 저절로 올라와 끝 알림을 가리지 않게)', skip: !Platform.isWindows, (tester) async { final src = p.join(tmp.path, 'cp'), dst = p.join(tmp.path, 'cpd'); File(p.join(src, 'a.txt')) diff --git a/test/rsync_filter_test.dart b/test/rsync_filter_test.dart index adf01d6..8a4f117 100644 --- a/test/rsync_filter_test.dart +++ b/test/rsync_filter_test.dart @@ -71,6 +71,18 @@ void main() { expect(ex('--exclude=**/deep/**', 'a/deep/x'), isTrue); expect(ex('--exclude=**/deep/**', 'deep', dir: true), isFalse); }); + test('15 검토 F1 · F3: 고치개 (,s · ,r · ,! · ,p) · H · S · P · R · merge 와 exclude-from 의 ! · 머리 줄은 적용하지 않고 "미리 볼 수 없음"', () { + for (final o in ['--filter="-,s a.txt"', '--filter="+,r a.txt"', '--filter="-,! a.txt"', '--filter="-,p a.txt"', '--filter="H a.txt"', + '--filter="P a.txt"', '--filter="S a.txt"', '--filter="R a.txt"', '--filter=". rules"', '--filter=": .rsync-filter"']) { + final f = RsyncFilter.parse(o); + expect(f.unsupported, isNotEmpty, reason: o); + expect(f.excluded('a.txt', isDir: false), isFalse, reason: '$o 는 지키지 않음 (미리 보기가 더 적게 지우게 되지 않게)'); + } + final g = RsyncFilter.parse('--exclude-from=x', + readLines: (_) => ['*.log', '!', 'P keep.txt', 'H a.txt', '-,s b.txt', '. more', '- c.txt']); + expect(g.unsupported, hasLength(5)); + expect(g.excluded('c.txt', isDir: false), isTrue, reason: '보통 줄은 그대로'); + }); test('거르기가 없으면 아무것도 거르지 않음 · 묶은 짧은 옵션은 거르기가 아님', () { final f = RsyncFilter.parse('-avPog --delete'); expect(f.isEmpty, isTrue); @@ -187,6 +199,18 @@ void main() { }); } + for (final opts in ['--filter="-,s a.txt"', '--filter="H a.txt"']) { + test('15 재현: $opts + --delete - 실제 rsync 는 받는 쪽 a.txt 를 지움 → 미리 보기는 그 규칙을 "미리 볼 수 없음" 으로 알림', () async { + if (!Platform.isWindows || !File(rsync).existsSync()) return markTestSkipped('앱에 든 rsync 가 없음'); + File(p.join(dst, 'a.txt')).writeAsStringSync('받는 쪽'); + final r = await Process.run(rsync, ['-r', '-8', '--delete', '-i', ...splitOptions(opts), '${toCygwinPath(src)}/', '${toCygwinPath(dst)}/'], + stdoutEncoding: utf8, stderrEncoding: utf8); + expect(r.exitCode, 0, reason: '${r.stderr}'); + expect(File(p.join(dst, 'a.txt')).existsSync(), isFalse, reason: '실제 rsync 는 지움 (보내는 쪽만 빼기 · 받는 쪽은 지키지 않음)'); + expect(RsyncFilter.parse(opts).unsupported, isNotEmpty, reason: '미리 보기는 이 규칙을 따르지 못한다고 알려야 함'); + }); + } + test('--exclude-from 파일', () async { if (!Platform.isWindows || !File(rsync).existsSync()) return markTestSkipped('앱에 든 rsync 가 없음'); final list = File(p.join(tmp.path, 'excl.txt'))..writeAsStringSync('# 빼기\n+ keep.log\n*.log\nbuild/\n'); diff --git a/test/sync_preview_test.dart b/test/sync_preview_test.dart index 084ef07..6828039 100644 --- a/test/sync_preview_test.dart +++ b/test/sync_preview_test.dart @@ -63,7 +63,10 @@ void main() { expect(r['only_b.txt'], PreviewAction.delete); // 229: 건너뛰는 것도 목록에 "받는 쪽이 더 새것 - 건너뜀" 으로 (예전에는 빠져 있었다) expect(r['leftold.txt'], PreviewAction.skipNewer); - expect(r.containsKey('half.mkv.jjpart'), isFalse); + // 15 검토 F4 (10/19): 받는 쪽에만 있는 받다 만 파일도 --delete 로 지워지므로 지울 목록에 (예전에는 빠져 적게 보였다) + expect(r['half.mkv.jjpart'], PreviewAction.delete); + final keep = m(await previewSync(a, b, toRight: true, update: true)); + expect(keep.containsKey('half.mkv.jjpart'), isFalse, reason: '지우지 않으면 셀 것 없음'); }); test('⇄: 한쪽에만 있는 것은 서로 건너가고, 다른 파일은 새것 쪽에서 (한 번만)', () async {