WIP 177 보충 3 (15 검토 F1~F4): 고치개 · H · P · merge · exclude-from 의 ! 등은 적용하지 않고 "미리 볼 수 없음" · 그런 규칙이 있으면 빨간 경고, --delete 와 함께면 체크해야 [실행] · 받는 쪽 받다 만 파일도 지울 목록에 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>next3
parent
86a994690b
commit
63ef362f6d
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -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 メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
||||
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?",
|
||||
|
|
|
|||
|
|
@ -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 盘等) 没有回收站,会直接删除。",
|
||||
"이 이동 버튼 지우기": "删除此移动按钮",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ Future<List<PreviewItem>> 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<List<PreviewItem>> 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 면 지운다)
|
||||
|
|
|
|||
|
|
@ -3375,6 +3375,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에)
|
||||
List<PreviewItem>? preview;
|
||||
Object? previewError;
|
||||
// 15 검토 F2: 미리 볼 수 없는 거르기 규칙을 알고 실행하는지 (지우기가 들어 있을 때)
|
||||
var filterAck = false;
|
||||
List<String> 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<ExplorerPage> {
|
|||
],
|
||||
// 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<ExplorerPage> {
|
|||
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('실행')),
|
||||
|
|
|
|||
|
|
@ -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<SyncPreviewView> {
|
|||
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<SyncPreviewView> {
|
|||
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<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
if (!deletes && !moves) return true;
|
||||
List<PreviewItem>? 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<bool> 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<bool> 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<bool> 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<String> unsupported;
|
||||
final bool deletes;
|
||||
final bool ack;
|
||||
final ValueChanged<bool> 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)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FilledButton>(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'))
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue