WIP 원격 확인 창: 옵션에 거르기 (--exclude 등) 가 있으면 "원격은 거르기를 따르지 않음" 주황 경고 · 안내 글에 SMB · FTP (검토 대기)

관리자 지시 (10/19, 15 의 원격 동기화 검토 M2): 원격 (WebDAV · SFTP · SMB · FTP) 은 rsync 대신 앱이 직접 맞추며 옵션의 거르기
(--exclude · --include · --filter · -f · --exclude-from · --include-from · -C · -F) 를 따르지 않는다 - 지키려고 뺀 것도 복사하고, 지우기면 지운다.
미리 보기도 거르기 없이 계산해 실제와 같지만, 확인 창은 이것을 알리지 않았다.
- 새 optionsFilter (sync_preview.dart): 옵션에 거르기가 있는지.
- Rsync 화면 확인 창 · 지우기 포함 작업의 확인 창 (모니터링 [실행] 등, confirmDeletingRun): 원격이고 거르기가 있으면 주황 경고
  "원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 … 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다."
- 원격 안내 글 "WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다" → "WebDAV · SFTP · SMB · FTP: …" (SMB · FTP 도 같은 길).
- 관리자 지적: 6278403 에서 test/webdav_test.dart · test/sftp_test.dart 의 줄바꿈이 파일 전체에서 CRLF 로 바뀐 것을 원래 (LF) 로 되돌림
  (6278403~1 과 비교하면 +27 · +23 줄만 남음).
_mirrorV 가 RsyncFilter 를 따르게 하는 것은 next3 와 합친 뒤의 다음 일 (관리자 결정).

시험: sync_preview_test (새 1: optionsFilter 있음 10 · 없음 6, -e "ssh -C" 는 아님) · explorer_webdav_test (안내 글 · 거르기 없으면 경고 없음) ·
sync_preview_view · webdav · sftp 31 통과, 임시 폴더 흔적 0, analyze 문제 없음, l10n 빠짐 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next2
RyuJongJin 2026-10-19 22:23:05 +09:00
parent 62784039e4
commit 22ae03953d
10 changed files with 542 additions and 491 deletions

View File

@ -356,6 +356,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}", "WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} failed: {1} {2}{3}",
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV, OpenSubtitles, etc. Server list and user names are kept", "WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV, OpenSubtitles, etc. Server list and user names are kept",
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "Keeps WebDAV and OpenSubtitles passwords in this device's secure storage. When off, nothing is saved (existing ones are removed) and you enter them each time the app starts", "WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "Keeps WebDAV and OpenSubtitles passwords in this device's secure storage. When off, nothing is saved (existing ones are removed) and you enter them each time the app starts",
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV · SFTP · SMB · FTP: the app syncs these itself instead of rsync (compares size and modified time; follows -u, --delete and --remove-source-files from the options).",
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV / SFTP reads everything inside from the server, which can take a long time. Analyze?", "WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV / SFTP reads everything inside from the server, which can take a long time. Analyze?",
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV / SFTP can't be added to the MKV list. Copy them to this device first.", "WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "Videos on WebDAV / SFTP can't be added to the MKV list. Copy them to this device first.",
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV / SFTP files show only name, size and date", "WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV / SFTP files show only name, size and date",
@ -1892,6 +1893,7 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
"우르두어": "Urdu", "우르두어": "Urdu",
"우크라이나어": "Ukrainian", "우크라이나어": "Ukrainian",
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "Remote locations (WebDAV · SFTP · SMB · FTP) do not follow the filter options (--exclude, --include, --filter, …) - excluded items are copied too, and deleted if the run deletes. The comparison list above is what will actually happen.",
"원래 이름으로 되돌리지 못한 것: {0}": "Couldn't restore the original names of: {0}", "원래 이름으로 되돌리지 못한 것: {0}": "Couldn't restore the original names of: {0}",
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place", "원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)", "원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",

View File

@ -356,6 +356,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}", "WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失敗: {1} {2}{3}",
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV・OpenSubtitles など。サーバー一覧と ID は残します", "WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV・OpenSubtitles など。サーバー一覧と ID は残します",
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します", "WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "WebDAV・OpenSubtitles のパスワードをこの端末の安全な保存領域に保存します。オフにすると保存せず (既存のものも削除)、アプリを起動するたびに入力します",
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV・SFTP・SMB・FTP: rsync の代わりにアプリが直接同期します (サイズ・更新日時で比較、オプションのうち -u・--delete・--remove-source-files に従う)。",
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV・SFTP はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?", "WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV・SFTP はサーバーから中身をすべて読むため、時間がかかることがあります。分析しますか?",
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV・SFTP の動画は MKV リストに追加できません。先にこの端末へコピーしてください。", "WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "WebDAV・SFTP の動画は MKV リストに追加できません。先にこの端末へコピーしてください。",
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV・SFTP のファイルは名前・サイズ・日付のみ表示します", "WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV・SFTP のファイルは名前・サイズ・日付のみ表示します",
@ -1892,6 +1893,7 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
"우르두어": "ウルドゥー語", "우르두어": "ウルドゥー語",
"우크라이나어": "ウクライナ語", "우크라이나어": "ウクライナ語",
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "リモート (WebDAV・SFTP・SMB・FTP) はオプションのフィルター (--exclude・--include・--filter など) に従いません - 除外したものもコピーし、削除を含む場合は削除します。上の比較一覧が実際に行う内容です。",
"원래 이름으로 되돌리지 못한 것: {0}": "元の名前に戻せなかったもの: {0}", "원래 이름으로 되돌리지 못한 것: {0}": "元の名前に戻せなかったもの: {0}",
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります", "원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)", "원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",

View File

@ -356,6 +356,7 @@
"WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}", "WebDAV {0} 실패: {1} {2}{3}": "WebDAV {0} 失败: {1} {2}{3}",
"WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV · OpenSubtitles 等。保留服务器列表和用户名", "WebDAV · OpenSubtitles 등. 서버 목록과 아이디는 남깁니다": "WebDAV · OpenSubtitles 等。保留服务器列表和用户名",
"WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入", "WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다": "将 WebDAV · OpenSubtitles 密码保存在本设备的安全存储中。关闭后不保存 (已有的也会删除),每次启动应用时重新输入",
"WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).": "WebDAV · SFTP · SMB · FTP:由应用代替 rsync 直接同步 (比较大小和修改时间,遵循选项中的 -u · --delete · --remove-source-files)。",
"WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV · SFTP 需要从服务器读取全部内容,可能需要较长时间。要分析吗?", "WebDAV · SFTP 는 서버에서 안의 것을 모두 읽어 오래 걸릴 수 있습니다. 분석할까요?": "WebDAV · SFTP 需要从服务器读取全部内容,可能需要较长时间。要分析吗?",
"WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV · SFTP 上的视频加入 MKV 列表。请先复制到本设备。", "WebDAV · SFTP 의 동영상은 MKV 목록에 넣을 수 없습니다. 먼저 이 기기로 복사하세요.": "无法将 WebDAV · SFTP 上的视频加入 MKV 列表。请先复制到本设备。",
"WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV · SFTP 文件仅显示名称、大小和日期", "WebDAV · SFTP 파일은 이름 · 크기 · 날짜만 봅니다": "WebDAV · SFTP 文件仅显示名称、大小和日期",
@ -1892,6 +1893,7 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。", "요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
"우르두어": "乌尔都语", "우르두어": "乌尔都语",
"우크라이나어": "乌克兰语", "우크라이나어": "乌克兰语",
"원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.": "远程 (WebDAV · SFTP · SMB · FTP) 不遵循选项中的过滤规则 (--exclude · --include · --filter 等) - 被排除的项目也会复制,如包含删除则会删除。上面的比较列表就是实际会执行的操作。",
"원래 이름으로 되돌리지 못한 것: {0}": "未能恢复原名称: {0}", "원래 이름으로 되돌리지 못한 것: {0}": "未能恢复原名称: {0}",
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目", "원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)", "원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",

View File

@ -57,6 +57,16 @@ bool isRsyncDeleteOption(String o) => o == '--del' || o.startsWith('--delete') |
/// rsync 옵션에 지우기 (--delete 계열) 가 있는지 /// rsync 옵션에 지우기 (--delete 계열) 가 있는지
bool optionsDelete(String options) => splitOptions(options).any(isRsyncDeleteOption); bool optionsDelete(String options) => splitOptions(options).any(isRsyncDeleteOption);
/// M2 (관리자 지시 10/19): rsync 옵션에 거르기 (--exclude · --include · --filter · -f · --exclude-from · --include-from · -C · -F) 가 있는지.
/// 원격 (WebDAV · SFTP · SMB · FTP) 은 앱이 직접 맞추며 이 거르기를 따르지 않으므로 확인 창에 알린다
bool optionsFilter(String options) => splitOptions(options).any((o) =>
o == '-f' ||
o == '-F' ||
o == '--cvs-exclude' ||
o == '-C' ||
RegExp(r'^--(exclude|include|filter|exclude-from|include-from)(=|$)').hasMatch(o) ||
(RegExp(r'^-[a-zA-Z]+$').hasMatch(o) && (o.contains('C') || o.contains('F'))));
/// 옵션 글을 낱말로 나누되 따옴표는 그대로 둔 원래 글 조각 (106: --exclude="My Folder" 가 갈라지지 않게) /// 옵션 글을 낱말로 나누되 따옴표는 그대로 둔 원래 글 조각 (106: --exclude="My Folder" 가 갈라지지 않게)
List<String> rawOptionTokens(String s) { List<String> rawOptionTokens(String s) {
final out = <String>[]; final out = <String>[];

View File

@ -3533,8 +3533,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if (_viaDav([l, r])) if (_viaDav([l, r]))
Text(tr('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'), Text(tr('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
style: const TextStyle(fontSize: 12, color: JjColors.accent)), style: const TextStyle(fontSize: 12, color: JjColors.accent)),
// M2 (관리자 지시 10/19): 원격이고 옵션에 거르기가 있으면 - 앱 맞추기는 거르기를 따르지 않는다 (뺀 것도 복사 · 지움)
if (_viaDav([l, r]) && tasks.any((t) => optionsFilter(opts(t))))
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(tr('원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
),
]), ]),
), ),
actions: [ actions: [

View File

@ -236,6 +236,13 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
], ],
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다) // 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)), Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
// M2 (관리자 지시 10/19): 원격 (앱이 직접 맞춤) 인데 옵션에 거르기가 있으면 - 거르기를 따르지 않는다
if (direct && t.method == 'rsync' && optionsFilter(t.options))
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(tr('원격 (WebDAV · SFTP · SMB · FTP) 은 옵션의 거르기 (--exclude · --include · --filter 등) 를 따르지 않습니다 - 거른 것도 복사하고, 지우기가 들어 있으면 지웁니다. 위 비교 목록이 실제로 할 일입니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
),
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히 // 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
if (t.move) if (t.move)
Padding( Padding(

View File

@ -205,7 +205,9 @@ void main() {
// 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠) // 72: 비교 - 보낼 것 · 받는 쪽 (WebDAV) 에만 있는 것 (지우기 없으니 그대로 둠)
expect(find.textContaining('→ + inner.txt'), findsOneWidget); expect(find.textContaining('→ + inner.txt'), findsOneWidget);
expect(find.textContaining('= note.txt'), findsOneWidget); expect(find.textContaining('= note.txt'), findsOneWidget);
expect(find.textContaining('WebDAV · SFTP: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget); expect(find.textContaining('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다'), findsOneWidget);
// M2: 옵션에 거르기가 없으면 거르기 경고는 없다
expect(find.textContaining('거르기 (--exclude'), findsNothing);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행'))); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
final got = File(p.join(remote.path, '원격', 'inner.txt')); final got = File(p.join(remote.path, '원격', 'inner.txt'));
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200); await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);

View File

@ -101,4 +101,23 @@ void main() {
expect(r['only_b.txt'], (PreviewAction.delete, true)); expect(r['only_b.txt'], (PreviewAction.delete, true));
expect(r['new.txt'], (PreviewAction.add, true)); expect(r['new.txt'], (PreviewAction.add, true));
}); });
test('M2 (10/19): optionsFilter - 거르기 옵션이 있는지 (원격 확인 창의 경고)', () {
for (final o in [
'-av --exclude=*.tmp',
'-av --exclude "My Folder"',
'-a --include=*.mkv --exclude=*',
'--filter="- *.bak"',
'-a -f "- x"',
'--exclude-from=list.txt',
'--include-from list.txt',
'-avC',
'--cvs-exclude',
'-aF',
]) {
expect(optionsFilter(o), isTrue, reason: o);
}
for (final o in ['-avPog', '-a --delete', '-avu --remove-source-files', '--delete-excluded', '-e "ssh -C"', '']) {
expect(optionsFilter(o), isFalse, reason: o);
}
});
} }