From c070486c840ac357ede307194032a9c1b4d4e830 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:30:37 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EC=A7=81=EC=A0=91=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EC=9D=B4=EB=8F=99=EC=9D=80=20=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EC=AA=BD=EC=9D=B4=20=EB=8D=94=20=EC=83=88=EA=B2=83?= =?UTF-8?q?=EC=9D=B4=EB=A9=B4=20=EA=B1=B4=EB=84=88=EB=9C=80=20=C2=B7=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=20=EB=B3=B4=EA=B8=B0=20=EA=B2=BD=EA=B3=A0=20?= =?UTF-8?q?(229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 데이터 (버그): 현재 방식 · WebDAV 의 이동이 받는 쪽의 더 새 파일을 예전 원본으로 덮어쓰고 원본까지 지웠다 - 환경 설정 '받는 쪽이 더 새것이면 건너뛰기' (copySkipNewer, 처음 값 켜짐 - 관리자 판단). rsync -u 와 같음 - 건너뛴 파일은 이동이라도 원본에 남기고 'n개는 원본에 남음' 으로 알림 - 미리 보기: 건너뛰는 것 (받는 쪽이 더 새것 - 건너뜀), 더 새 파일을 덮어쓸 때 주황 굵은 경고 - 시험 sync_preview_test 기대값: -u 일 때 leftold.txt 는 목록에서 빠지지 않고 skipNewer 로 보임 (관리자 승인) Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 6 ++++ assets/l10n/ja.json | 6 ++++ assets/l10n/zh-Hans.json | 6 ++++ lib/app/copy_center.dart | 1 + lib/app/settings.dart | 6 ++++ lib/app/transfer_job.dart | 6 +++- lib/core/sync_preview.dart | 18 ++++++++--- lib/ui/copy_sync_settings.dart | 7 +++++ lib/ui/explorer_page.dart | 5 ++- lib/ui/monitor_page.dart | 2 +- lib/ui/sync_preview_view.dart | 21 ++++++++++--- test/move_settled_test.dart | 52 ++++++++++++++++++++++++++++++++ test/sync_preview_test.dart | 3 +- test/sync_preview_view_test.dart | 18 +++++++++++ 14 files changed, 144 insertions(+), 13 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 1d110e3..47060dc 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -46,9 +46,11 @@ "'사진' 모델로 바꿔 다시": "Switch to 'Photos' and redo", "(AI 번역을 쓸 수 없는 기기라 언어를 더할 수 없습니다)": "(Languages cannot be added because AI translation is unavailable on this device)", "({0}개 지움)": "(delete {0})", + "(더 새 파일을 덮어씀)": "(overwrites a newer file)", "(덮어씀)": "(overwritten)", "(동영상을 세 번 누르면 옮깁니다)": "(Tapping a video three times moves it)", "(받는 쪽에만 있음)": "(only on receiving side)", + "(받는 쪽이 더 새것 - 건너뜀)": "(receiving side is newer - skipped)", "(비어 있음)": "(empty)", "(설명 없음)": "(no description)", "(지워짐)": "(deleted)", @@ -518,6 +520,7 @@ "그림을 읽을 수 없습니다": "Cannot read the image", "그림을 페이지로 넣기 (이 페이지 뒤)": "Add images as pages (after this page)", "그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "Almost the same image, and the last step is about 10× faster (when downloaded)", + "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "{0} of them are newer on the receiving side - the newer files will be overwritten with older ones", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "Text → image · image → image (on this device; download models in Settings)", "글을 격자에 반영": "Apply text to grid", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "Text · button size. Change it any time with − · + on the top bar; press the number in the middle to return to this size (now {0}%)", @@ -908,6 +911,8 @@ "받는 쪽에만 있음 {0} (그대로 둠)": "Only on receiving side {0} (kept)", "받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)": "{0} items will be deleted on the receiving side (--delete, cannot be undone)", "받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "{0} files on the receiving side will be overwritten (same name, different content - the original is lost)", + "받는 쪽이 더 새것 {0} (건너뜀)": "Newer on receiving side {0} (skipped)", + "받는 쪽이 더 새것이면 건너뛰기": "Skip when the receiving side is newer", "받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "Download size: about {0}MB · downloaded files · AI models · settings are kept", "받다 만 AI 모델": "Unfinished AI models", "받다 만 다운로드": "Unfinished downloads", @@ -2029,6 +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", "현재 유지": "Keep as is", "현재 페이지": "Current page", "현재 페이지 추가": "Add current page", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 9a720f1..fb1c6f4 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -46,9 +46,11 @@ "'사진' 모델로 바꿔 다시": "「写真」モデルに変えてやり直す", "(AI 번역을 쓸 수 없는 기기라 언어를 더할 수 없습니다)": "(この端末では AI 翻訳が使えないため言語を追加できません)", "({0}개 지움)": "({0} 個を削除)", + "(더 새 파일을 덮어씀)": "(新しいファイルを上書き)", "(덮어씀)": "(上書き)", "(동영상을 세 번 누르면 옮깁니다)": "(動画を 3 回タップすると移動します)", "(받는 쪽에만 있음)": "(受け取る側にだけある)", + "(받는 쪽이 더 새것 - 건너뜀)": "(受け取る側が新しい - スキップ)", "(비어 있음)": "(空)", "(설명 없음)": "(説明なし)", "(지워짐)": "(削除)", @@ -518,6 +520,7 @@ "그림을 읽을 수 없습니다": "画像を読み込めません", "그림을 페이지로 넣기 (이 페이지 뒤)": "画像をページとして追加 (このページの後)", "그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "画像はほぼ同じで、最後の段階が約 10 倍速くなります (ダウンロード済みの場合)", + "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "そのうち {0} 件は受け取る側の方が新しいです - 新しいファイルを古いもので上書きします", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "テキスト → 画像 · 画像 → 画像 (端末内で。モデルは設定でダウンロード)", "글을 격자에 반영": "テキストをグリッドに反映", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字・ボタンの大きさです。上部バーの − ・ + でその都度変更でき、中央の数字を押すとこの大きさに戻ります(現在 {0}%)", @@ -908,6 +911,8 @@ "받는 쪽에만 있음 {0} (그대로 둠)": "受け取る側にだけある {0} (そのまま)", "받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)": "受け取る側で {0} 個が削除されます (--delete、元に戻せません)", "받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "受け取る側の {0} 件を上書きします (同じ名前で内容が違うもの - 元のものはなくなります)", + "받는 쪽이 더 새것 {0} (건너뜀)": "受け取る側が新しい {0} (スキップ)", + "받는 쪽이 더 새것이면 건너뛰기": "受け取る側が新しければスキップ", "받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "ダウンロードサイズ: 約 {0}MB・ダウンロードしたファイル・AI モデル・設定はそのまま", "받다 만 AI 모델": "ダウンロード途中の AI モデル", "받다 만 다운로드": "ダウンロード途中のファイル", @@ -2029,6 +2034,7 @@ "허용했으면 다시 확인": "許可したらもう一度確認", "헝가리어": "ハンガリー語", "현재 방식": "現在の方式", + "현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "現在の方式 · WebDAV でフォルダーの中身を合わせるとき (rsync の -u と同じ)。スキップしたファイルは移動でも元の場所に残します", "현재 유지": "現状維持", "현재 페이지": "現在のページ", "현재 페이지 추가": "現在のページを追加", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 3880a36..2b28eba 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -46,9 +46,11 @@ "'사진' 모델로 바꿔 다시": "改用“照片”模型重新处理", "(AI 번역을 쓸 수 없는 기기라 언어를 더할 수 없습니다)": "(此设备无法使用 AI 翻译,因此无法添加语言)", "({0}개 지움)": "(删除 {0} 个)", + "(더 새 파일을 덮어씀)": "(覆盖较新的文件)", "(덮어씀)": "(覆盖)", "(동영상을 세 번 누르면 옮깁니다)": "(连按视频三次会移动它)", "(받는 쪽에만 있음)": "(仅在接收方)", + "(받는 쪽이 더 새것 - 건너뜀)": "(接收端较新 - 已跳过)", "(비어 있음)": "(空)", "(설명 없음)": "(无说明)", "(지워짐)": "(将删除)", @@ -518,6 +520,7 @@ "그림을 읽을 수 없습니다": "无法读取图片", "그림을 페이지로 넣기 (이 페이지 뒤)": "将图片作为页面加入 (本页之后)", "그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "图片几乎相同,最后一步快约 10 倍 (已下载时)", + "그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "其中 {0} 个在接收端较新 - 将用旧文件覆盖较新的文件", "글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "文字 → 图片 · 图片 → 图片(在本设备上;模型在设置中下载)", "글을 격자에 반영": "将文本应用到网格", "글자 · 버튼 크기입니다. 위쪽 막대의 − · + 로 그때그때 바꿀 수 있고, 가운데 숫자를 누르면 이 크기로 돌아갑니다 (지금 {0}%)": "文字 · 按钮大小。可随时用顶部栏的 − · + 调整,点击中间的数字恢复为此大小(当前 {0}%)", @@ -908,6 +911,8 @@ "받는 쪽에만 있음 {0} (그대로 둠)": "仅在接收方 {0} (保留)", "받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)": "接收方将删除 {0} 个项目 (--delete,无法撤销)", "받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)": "将覆盖接收端的 {0} 个文件(同名但内容不同 - 原文件将丢失)", + "받는 쪽이 더 새것 {0} (건너뜀)": "接收端较新 {0}(跳过)", + "받는 쪽이 더 새것이면 건너뛰기": "接收端较新时跳过", "받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "下载大小: 约 {0}MB · 已下载的文件 · AI 模型 · 设置均保留", "받다 만 AI 모델": "未下载完的 AI 模型", "받다 만 다운로드": "未完成的下载", @@ -2029,6 +2034,7 @@ "허용했으면 다시 확인": "允许后重新检查", "헝가리어": "匈牙利语", "현재 방식": "当前方式", + "현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다": "使用当前方式 · WebDAV 同步文件夹内容时(与 rsync 的 -u 相同)。跳过的文件即使是移动也会保留在源中", "현재 유지": "保持现状", "현재 페이지": "当前页面", "현재 페이지 추가": "添加当前页面", diff --git a/lib/app/copy_center.dart b/lib/app/copy_center.dart index 640b5f6..e7f6d58 100644 --- a/lib/app/copy_center.dart +++ b/lib/app/copy_center.dart @@ -119,6 +119,7 @@ class CopyCenter extends ChangeNotifier { contents: t.contents, prune: t.move ? t.prune : '', conflict: conflict, + skipNewer: c.settings.copySkipNewer, ); jobs[t.id] = job; job.addListener(notifyListeners); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 26fb839..72c876a 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -373,6 +373,10 @@ class AppSettings { /// 48: 복사 · 이동할 때 받는 폴더에 같은 이름이 있으면 'ask' (확인 창에서 고름, 처음 값) · 'rename' · 'overwrite' · 'skip' String copyConflict = 'ask'; + + /// 229: 앱이 직접 하는 복사 · 이동 (현재 방식 · WebDAV 의 안의 것 맞추기) 에서 받는 쪽이 더 새것이면 건너뛴다 (rsync -u 와 같음). + /// 처음 값 켜짐 - 예전 원본으로 새 파일을 덮어쓰고 (이동이면 원본까지 지워) 새 내용을 잃었다 (관리자 판단) + bool copySkipNewer = true; String copyMethodFolder = 'builtin'; String rsyncOptions = '-avPog'; String robocopyOptions = '/E /COPY:DAT /DCOPY:T /R:2 /W:2'; @@ -667,6 +671,7 @@ class AppSettings { 'explorerPaths': explorerPaths, 'copyMethodFile': copyMethodFile, 'copyConflict': copyConflict, + 'copySkipNewer': copySkipNewer, 'copyMethodFolder': copyMethodFolder, 'rsyncPaths': rsyncPaths, 'components': components, @@ -853,6 +858,7 @@ class AppSettings { ..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x'] ..copyMethodFile = _method(j['copyMethodFile']) ..copyConflict = const ['rename', 'overwrite', 'skip'].contains(j['copyConflict']) ? j['copyConflict'] as String : 'ask' + ..copySkipNewer = j['copySkipNewer'] != false ..copyMethodFolder = _method(j['copyMethodFolder']) ..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x'] ..components = j['components'] is List diff --git a/lib/app/transfer_job.dart b/lib/app/transfer_job.dart index c2368c0..0816112 100644 --- a/lib/app/transfer_job.dart +++ b/lib/app/transfer_job.dart @@ -62,6 +62,9 @@ class TransferJob extends ChangeNotifier { /// 48: 같은 이름이 있을 때 (앱이 직접 복사 · 이동할 때만 - robocopy · rsync 는 그 프로그램의 규칙) final NameConflict conflict; + /// 229: 앱이 직접 맞출 때 받는 쪽이 더 새것이면 건너뛴다 (환경 설정 - 옵션 칸이 없는 현재 방식 · WebDAV 도). 건너뛴 원본은 이동이라도 남긴다 + final bool skipNewer; + /// 같은 이름이라 건너뛴 항목 (원본 경로) final skipped = []; @@ -77,6 +80,7 @@ class TransferJob extends ChangeNotifier { this.contents = false, this.prune = '', this.conflict = NameConflict.rename, + this.skipNewer = false, }); /// 지금 복사하는 파일 (원본 기준 상대 경로, rsync 는 이름을 먼저 알리고 그 파일의 진행률을 알린다) @@ -290,7 +294,7 @@ class TransferJob extends ChangeNotifier { if (contents && (isDav(sources[i]) || FileSystemEntity.isDirectorySync(sources[i]))) { // 폴더 "안의 것" 맞추기 (rsync 원본/ 대상/ 과 같은 뜻): -u · --delete · 원본 파일 지우기 (--remove-source-files) 도 settled.clear(); - await _ops!.mirror(sources[i], dest, update: _update, delete: _delete, strict: move); + await _ops!.mirror(sources[i], dest, update: _update || skipNewer, delete: _delete, strict: move); if (move && !cancelled) await _deleteSettled(); made.add(dest); } else { diff --git a/lib/core/sync_preview.dart b/lib/core/sync_preview.dart index 3e5258e..ba6521f 100644 --- a/lib/core/sync_preview.dart +++ b/lib/core/sync_preview.dart @@ -17,6 +17,9 @@ enum PreviewAction { /// 받는 쪽에만 있어 지워짐 (--delete) delete, + + /// 229: 받는 쪽이 더 새것이라 건너뜀 (-u · 환경 설정 "받는 쪽이 더 새것이면 건너뛰기") + skipNewer, } class PreviewItem { @@ -28,7 +31,10 @@ class PreviewItem { final bool toRight; final bool isDir; final int size; - const PreviewItem(this.rel, this.action, {required this.toRight, this.isDir = false, this.size = 0}); + const PreviewItem(this.rel, this.action, {required this.toRight, this.isDir = false, this.size = 0, this.newer = false}); + + /// 229: [PreviewAction.update] 인데 받는 쪽이 더 새것 (예전 원본으로 새 파일을 덮어씀) + final bool newer; } /// rsync 의 지우기 옵션인지: --delete · --delete-before/during/delay/after/excluded/missing-args · --del (별칭) @@ -115,9 +121,13 @@ Future> previewSync(String src, String dst, if (t == null) { out.add(PreviewItem(r, PreviewAction.add, toRight: toRight, size: e.size)); } else if (t.size != e.size || t.modified.difference(e.modified).inSeconds.abs() > 2) { - // -u: 받는 쪽이 더 새 것이면 건너뜀 - if (update && t.modified.isAfter(e.modified.add(const Duration(seconds: 2)))) continue; - out.add(PreviewItem(r, PreviewAction.update, toRight: toRight, size: e.size)); + // -u: 받는 쪽이 더 새 것이면 건너뜀. 229: 건너뛰는 것도 · 더 새 것을 덮어쓰는 것도 목록에 따로 + final newer = t.modified.isAfter(e.modified.add(const Duration(seconds: 2))); + if (update && newer) { + out.add(PreviewItem(r, PreviewAction.skipNewer, toRight: toRight, size: e.size)); + continue; + } + out.add(PreviewItem(r, PreviewAction.update, toRight: toRight, size: e.size, newer: newer)); } } for (final e in there.values) { diff --git a/lib/ui/copy_sync_settings.dart b/lib/ui/copy_sync_settings.dart index 61a89b5..50fb61a 100644 --- a/lib/ui/copy_sync_settings.dart +++ b/lib/ui/copy_sync_settings.dart @@ -151,6 +151,13 @@ class _CopySyncSettingsState extends State { onChanged: (v) => c.updateSettings((x) => x.copyConflict = v ?? 'ask'), ), ), + // 229: 폴더 안의 것을 맞출 때 (모니터링의 현재 방식 · WebDAV) - rsync 의 -u 와 같다 + SwitchListTile( + value: s.copySkipNewer, + onChanged: (v) => c.updateSettings((x) => x.copySkipNewer = v), + title: Text(tr('받는 쪽이 더 새것이면 건너뛰기')), + subtitle: Text(tr('현재 방식 · WebDAV 로 폴더 안의 것을 맞출 때 (rsync 의 -u 와 같음). 건너뛴 파일은 이동이라도 원본에 남깁니다')), + ), SettingTile( title: Text(tr('속도 제한 (KB/s)')), subtitle: Text(tr('0 = 제한 없음. 모든 방법에 적용: rsync --bwlimit, 현재 방식은 앱이 조절, robocopy 는 /IPG 로 비슷하게. ' diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 660f8e1..18a03a0 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2163,7 +2163,10 @@ class _ExplorerPageState extends State { : [ for (final t in tasks) ...await previewSync(t.sources.first, t.dest, - toRight: t.sources.first == l, delete: deletes(), update: optionsUpdate(opts(t))), + toRight: t.sources.first == l, + delete: deletes(), + // 229: WebDAV 가 끼면 앱이 직접 - 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" 도 따른다 + update: optionsUpdate(opts(t)) || (_viaDav([l, r]) && c.settings.copySkipNewer)), ], // 97: 다시 비교하는 동안은 예전 결과로 실행하지 못하게 onStart: () => set(() { diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index ec2b536..42a8cd0 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -343,7 +343,7 @@ class _CopyCardState extends State<_CopyCard> { Future _run(CopyTask t) async { // 104: 지우기가 들어 있으면 Rsync 화면과 같은 미리 보기를 거친다 (그사이 대상에 생긴 파일을 묻지 않고 지우지 않게). // 210: 이동 (원본 지우기) 도 - 카드의 [실행] 은 따로 묻는 창이 없어 곧바로 원본을 지웠다. 복사만은 지금처럼 바로 - if (!await confirmDeletingRun(context, t, includeMove: true) || !mounted) return; + if (!await confirmDeletingRun(context, t, includeMove: true, skipNewer: widget.c.settings.copySkipNewer) || !mounted) return; widget.c.note(trf('rsync 시작 ({0}): {1} → {2}', [tr('모니터링의 [실행]'), t.sources.join(', '), t.dest])); String? exe; if (t.method == 'rsync') { diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index a3c1829..a5b57e5 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -98,12 +98,15 @@ class _SyncPreviewViewState extends State { items.where((x) => x.action == a && (right == null || x.toRight == right)).length; final dels = n(PreviewAction.delete); final overwrites = n(PreviewAction.update); + final overNewer = items.where((x) => x.action == PreviewAction.update && x.newer).length; + final skipped = n(PreviewAction.skipNewer); final more = items.length >= widget.limit; final parts = [ for (final right in [true, false]) if (n(PreviewAction.add, right) + n(PreviewAction.update, right) > 0) trf('{0} 새로 {1} · 덮어씀 {2}', [right ? '→' : '←', n(PreviewAction.add, right), n(PreviewAction.update, right)]), if (n(PreviewAction.onlyTarget) > 0) trf('받는 쪽에만 있음 {0} (그대로 둠)', [n(PreviewAction.onlyTarget)]), + if (skipped > 0) trf('받는 쪽이 더 새것 {0} (건너뜀)', [skipped]), ]; return Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row(children: [ @@ -122,6 +125,10 @@ class _SyncPreviewViewState extends State { if (overwrites > 0) Text(trf('받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)', [overwrites]), style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)), + // 229: 받는 쪽이 더 새 파일을 예전 원본으로 덮어쓰게 되면 따로 크게 (건너뛰기를 끄거나 rsync 에 -u 가 없을 때) + if (overNewer > 0) + Text(trf('그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다', [overNewer]), + style: const TextStyle(fontSize: 12.5, color: Colors.orangeAccent, fontWeight: FontWeight.w800)), if (items.isNotEmpty) Container( margin: const EdgeInsets.only(top: 6), @@ -139,7 +146,9 @@ class _SyncPreviewViewState extends State { Text( switch (x.action) { PreviewAction.add => '${_arrow(x)} + ${x.rel}${x.isDir ? '/' : ' (${formatSize(x.size)})'}', - PreviewAction.update => '${_arrow(x)} ~ ${x.rel} (${formatSize(x.size)}) ${tr('(덮어씀)')}', + PreviewAction.update => + '${_arrow(x)} ~ ${x.rel} (${formatSize(x.size)}) ${x.newer ? tr('(더 새 파일을 덮어씀)') : tr('(덮어씀)')}', + PreviewAction.skipNewer => ' · ${x.rel} ${tr('(받는 쪽이 더 새것 - 건너뜀)')}', PreviewAction.onlyTarget => ' = ${x.rel}${x.isDir ? '/' : ''} ${tr('(받는 쪽에만 있음)')}', PreviewAction.delete => '${_arrow(x)} − ${x.rel}${x.isDir ? '/' : ''} ${tr('(지워짐)')}', }, @@ -149,7 +158,7 @@ class _SyncPreviewViewState extends State { color: switch (x.action) { PreviewAction.delete => Colors.redAccent, PreviewAction.update => Colors.orangeAccent, - PreviewAction.onlyTarget => JjColors.textDim, + PreviewAction.onlyTarget || PreviewAction.skipNewer => JjColors.textDim, _ => null, }, ), @@ -173,14 +182,16 @@ bool taskDeletes(CopyTask t) { /// 104: 지우기가 들어 있는 작업은 어디서 실행하든 (모니터링 [실행] · 옵션 저장 뒤 실행 · 최종 정리) Rsync 화면과 같은 미리 보기를 거친다. /// 지울 목록이 나온 뒤에만 [실행] 이 켜진다. 지우기가 없으면 묻지 않고 true -/// [includeMove] (210): 이동 (원본 파일 지우기) 도 미리 보기 · 확인을 거친다 - 모니터링의 [실행] 처럼 따로 묻는 창이 없을 때 -Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeMove = false}) async { +/// [includeMove] (210): 이동 (원본 파일 지우기) 도 미리 보기 · 확인을 거친다 - 모니터링의 [실행] 처럼 따로 묻는 창이 없을 때. +/// [skipNewer] (229): 앱이 직접 할 때 (현재 방식 · WebDAV) 받는 쪽이 더 새것이면 건너뛰는지 (환경 설정) +Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeMove = false, bool skipNewer = false}) async { final deletes = taskDeletes(t); final moves = includeMove && t.move; if (!deletes && !moves) return true; List? preview; Object? error; - final update = optionsUpdate(t.options); + final direct = t.method == 'builtin' || [...t.sources, t.dest].any(isDav); + final update = optionsUpdate(t.options) || (direct && skipNewer); String dstOf(String src) => t.contents ? t.dest : vJoin(t.dest, vBasename(src)); final ok = await showDialog( context: context, diff --git a/test/move_settled_test.dart b/test/move_settled_test.dart index 7ac91fa..27eb080 100644 --- a/test/move_settled_test.dart +++ b/test/move_settled_test.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/transfer_job.dart'; +import 'package:jj_mkvmaker/core/sync_preview.dart'; +import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/core/sync_tools.dart' show CopyMethod; import 'package:jj_mkvmaker/core/webdav.dart'; import 'package:path/path.dart' as p; @@ -109,6 +111,42 @@ void main() { // (속성만 다른 "수정됨" 파일은 /IS · /IT 로도 남는다 - 그때는 216 의 "n개는 원본에 남음" 으로 알린다) }); + test('229: 앱 직접 이동 - 받는 쪽이 더 새것이면 덮어쓰지 않고 원본도 남긴다 (환경 설정 처음 값 켜짐)', () async { + expect(AppSettings.fromJson({}).copySkipNewer, isTrue); + expect(AppSettings.fromJson((AppSettings()..copySkipNewer = false).toJson()).copySkipNewer, isFalse); + final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src'); + old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2))); + File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst'); + final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, skipNewer: true); + await job.run(); + expect(job.error, isNull); + expect(File(p.join(dst, 'older.txt')).readAsStringSync(), 'NEW version in dst', reason: '새 내용 그대로'); + expect(old.existsSync(), isTrue, reason: '건너뛴 원본은 이동이라도 남김'); + expect(File(p.join(src, 'a.txt')).existsSync(), isFalse, reason: '나머지는 옮김'); + expect(job.leftInSource, 1); + expect(job.doneSummary(), contains('1개는 원본에 남음')); + }); + + test('229: 건너뛰기를 끄면 (사용자가 고름) 예전처럼 덮어쓴다', () async { + final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src'); + old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2))); + File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst'); + final job = TransferJob(sources: [src], dest: dst, move: false, contents: true, skipNewer: false); + await job.run(); + expect(File(p.join(dst, 'older.txt')).readAsStringSync(), 'old in src'); + }); + + test('229: 미리 보기 - 건너뛰면 "받는 쪽이 더 새것 - 건너뜀", 아니면 덮어쓸 줄에 "더 새 것" 표시', () async { + final old = File(p.join(src, 'older.txt'))..writeAsStringSync('old in src'); + old.setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2))); + File(p.join(dst, 'older.txt')).writeAsStringSync('NEW version in dst'); + final skip = await previewSync(src, dst, toRight: true, update: true); + expect([for (final x in skip) if (x.rel == 'older.txt') x.action], [PreviewAction.skipNewer]); + final over = await previewSync(src, dst, toRight: true); + final o = over.firstWhere((x) => x.rel == 'older.txt'); + expect([o.action, o.newer], [PreviewAction.update, true]); + }); + group('214 b: WebDAV', () { late Directory remote; late TestDavServer server; @@ -138,6 +176,20 @@ void main() { expect(File(p.join(remote.path, 'a.txt')).readAsStringSync(), 'a'); }); + test('229 (WebDAV 로): 받는 쪽이 더 새것이면 건너뛰고 원본 남김', () async { + HttpOverrides.global = null; + File(p.join(src, 'older.txt')) + ..writeAsStringSync('old in src') + ..setLastModifiedSync(DateTime.now().subtract(const Duration(days: 2))); + File(p.join(remote.path, 'older.txt')).writeAsStringSync('NEW version in dst'); + final job = TransferJob(sources: [src], dest: 'dav://nas/', move: true, contents: true, skipNewer: true); + await job.run(); + expect(job.error, isNull); + expect(File(p.join(remote.path, 'older.txt')).readAsStringSync(), 'NEW version in dst'); + expect(File(p.join(src, 'older.txt')).existsSync(), isTrue); + expect(File(p.join(src, 'a.txt')).existsSync(), isFalse); + }); + test('214 d (WebDAV 로): 로컬 원본의 링크는 따라가지 않는다', () async { HttpOverrides.global = null; final outside = Directory(p.join(tmp.path, 'outside2'))..createSync(); diff --git a/test/sync_preview_test.dart b/test/sync_preview_test.dart index 97495b8..2cb14ea 100644 --- a/test/sync_preview_test.dart +++ b/test/sync_preview_test.dart @@ -61,7 +61,8 @@ void main() { test('→ --delete -u: 받는 쪽에만 있는 것은 지워짐 · 받는 쪽이 새것이면 건너뜀', () async { final r = m(await previewSync(a, b, toRight: true, delete: true, update: true)); expect(r['only_b.txt'], PreviewAction.delete); - expect(r.containsKey('leftold.txt'), isFalse); + // 229: 건너뛰는 것도 목록에 "받는 쪽이 더 새것 - 건너뜀" 으로 (예전에는 빠져 있었다) + expect(r['leftold.txt'], PreviewAction.skipNewer); expect(r.containsKey('half.mkv.jjpart'), isFalse); }); diff --git a/test/sync_preview_view_test.dart b/test/sync_preview_view_test.dart index 8d85c6e..9eeeb41 100644 --- a/test/sync_preview_view_test.dart +++ b/test/sync_preview_view_test.dart @@ -32,4 +32,22 @@ void main() { await tester.pumpAndSettle(); expect(find.textContaining('덮어씁니다'), findsNothing); }); + + testWidgets('229: 받는 쪽이 더 새것 - 건너뛰면 "(건너뜀)", 덮어쓰게 되면 따로 굵게 경고', (tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: SyncPreviewView( + compute: () async => const [ + PreviewItem('skip.txt', PreviewAction.skipNewer, toRight: true, size: 3), + PreviewItem('older.txt', PreviewAction.update, toRight: true, size: 13, newer: true), + ], + ), + ), + )); + await tester.pumpAndSettle(); + expect(find.textContaining('받는 쪽이 더 새것 1 (건너뜀)'), findsOneWidget); + expect(find.text('그중 1개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다'), findsOneWidget); + expect(find.textContaining('(더 새 파일을 덮어씀)'), findsOneWidget); + expect(find.textContaining('(받는 쪽이 더 새것 - 건너뜀)'), findsOneWidget); + }); }