diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 4f2cd50..e154ab1 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1349,6 +1349,7 @@ "원본 유지 (재인코딩 없음)": "Keep original (no re-encoding)", "원본 크기": "Original size", "원본 파일 지우기 (--remove-source-files)": "Delete source files (--remove-source-files)", + "원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "Could not remove the source files (no permission or in use). They were copied to the destination and the source is still there.", "원본 파일이 지워집니다 (이동).": "Source files will be deleted (move).", "원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "Source files will be deleted (move). This is on because the rsync options in Settings contain --remove-source-files - turn it off to keep the source this time.", "원본 폴더": "Source folder", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 1fdd1b5..1c82010 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1349,6 +1349,7 @@ "원본 유지 (재인코딩 없음)": "元のまま(再エンコードなし)", "원본 크기": "元のサイズ", "원본 파일 지우기 (--remove-source-files)": "元ファイルを削除 (--remove-source-files)", + "원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "元のファイルを削除できませんでした (権限がないか使用中)。移動先にはコピーされ、元のファイルは残っています。", "원본 파일이 지워집니다 (이동).": "元のファイルは削除されます (移動)。", "원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "元のファイルは削除されます (移動)。設定の rsync オプションに --remove-source-files があるためオンにしています - オフにすると今回は元を残します。", "원본 폴더": "元フォルダー", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 6dd709c..ac12912 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1349,6 +1349,7 @@ "원본 유지 (재인코딩 없음)": "保持原样(不重新编码)", "원본 크기": "原始尺寸", "원본 파일 지우기 (--remove-source-files)": "删除源文件 (--remove-source-files)", + "원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "无法删除源文件(没有权限或正在使用)。已复制到目标,源文件仍保留。", "원본 파일이 지워집니다 (이동).": "源文件将被删除(移动)。", "원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "源文件将被删除(移动)。因为设置中的 rsync 选项含有 --remove-source-files 所以已勾选 - 取消勾选则本次保留源文件。", "원본 폴더": "源文件夹", diff --git a/lib/app/transfer_job.dart b/lib/app/transfer_job.dart index b0be387..c2368c0 100644 --- a/lib/app/transfer_job.dart +++ b/lib/app/transfer_job.dart @@ -14,6 +14,29 @@ import '../l10n/tr.dart'; /// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다: /// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률 /// - 아래쪽: 지금 항목 (폴더) 의 파일 중 몇 개를 했는지 +/// 215: rsync 가 실패했을 때 보일 글 - 원본을 못 지웠으면 쉬운 말 한 줄, 그리고 오류 · 권한 줄을 끝쪽부터 (전송한 파일 이름이 아니라). +/// 예전에는 출력의 앞쪽 8줄이라 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다 +String rsyncFailureText(int code, List errLines, List allLines) { + final all = [...errLines, ...allLines]; + final removeFailed = all.any((l) { + final x = l.toLowerCase(); + return x.contains('failed to remove') || x.contains('cannot delete') || x.contains('unable to remove'); + }); + bool important(String l) { + final x = l.toLowerCase(); + return x.contains('error') || x.contains('fail') || x.contains('denied') || x.contains('cannot') || x.contains('permission'); + } + + final picked = errLines.where(important).toList(); + final lines = picked.isNotEmpty ? picked : (errLines.isNotEmpty ? errLines : allLines); + final tail = lines.length > 6 ? lines.sublist(lines.length - 6) : lines; + return [ + if (removeFailed) tr('원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.'), + trf('rsync 종료 코드 {0}', [code]), + ...tail, + ].join('\n'); +} + class TransferJob extends ChangeNotifier { final List sources; final String dest; @@ -302,6 +325,9 @@ class TransferJob extends ChangeNotifier { /// 214: 이동인데 원본에 남긴 파일 (맞춘 뒤 바뀜) final skippedSources = []; + /// 215: 프로그램이 오류 출력 (stderr) 으로 낸 줄 (끝의 20줄) + final errLog = []; + void _addLog(String line) { if (line.trim().isEmpty) return; log.add(line.trim()); @@ -314,6 +340,11 @@ class TransferJob extends ChangeNotifier { final b = _proc!.stderr.transform(enc.decoder).listen((t) { for (final l in t.split(RegExp(r'[\r\n]'))) { _addLog(l); + // 215: 오류 줄은 따로 (전송한 파일 이름에 밀려 실패 이유가 빠지지 않게) + if (l.trim().isNotEmpty) { + errLog.add(l.trim()); + if (errLog.length > 20) errLog.removeAt(0); + } } }); final code = await _proc!.exitCode; @@ -363,7 +394,7 @@ class TransferJob extends ChangeNotifier { }, ); if (cancelled) return; - if (!rsyncOk(code)) throw ProcessException(exe, const [], '${trf('rsync 종료 코드 {0}', [code])}\n${log.take(8).join('\n')}', code); + if (!rsyncOk(code)) throw ProcessException(exe, const [], rsyncFailureText(code, errLog, log), code); for (final i in idx) { filesDone[i] = filesTotal[i]; made.add(p.join(dest, p.basename(sources[i]))); diff --git a/test/move_settled_test.dart b/test/move_settled_test.dart index 65c89c2..98f5b2e 100644 --- a/test/move_settled_test.dart +++ b/test/move_settled_test.dart @@ -77,6 +77,23 @@ void main() { expect(File(p.join(src, 'a.txt')).existsSync(), isFalse); }); + test('215: rsync 가 원본을 못 지우면 쉬운 말 + 오류 줄 (끝쪽) 을 보인다 - 전송한 파일 이름만 보이지 않게', () { + final out = [for (var i = 0; i < 15; i++) 'movie$i.mkv']; + final err = [ + 'sender failed to remove movie3.mkv: Permission denied (13)', + 'sender failed to remove movie4.mkv: Permission denied (13)', + 'rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1338) [sender=3.4.1]', + ]; + final text = rsyncFailureText(23, err, [...out, ...err]); + expect(text, startsWith('원본 파일을 지우지 못했습니다')); + expect(text, contains('rsync 종료 코드 23')); + expect(text, contains('Permission denied')); + expect(text, contains('(code 23)')); + expect(text, isNot(contains('movie10.mkv')), reason: '전송한 파일 이름 줄이 아니라 오류 줄'); + // 원본 지우기와 상관없는 실패는 그 말을 붙이지 않는다 + expect(rsyncFailureText(12, ['rsync error: error in rsync protocol data stream (code 12)'], const []), isNot(contains('원본 파일을 지우지'))); + }); + group('214 b: WebDAV', () { late Directory remote; late TestDavServer server;