rsync 실패 글에 원본을 못 지운 이유 · 오류 줄 (215)

관리자 지시: 실패 글이 출력의 앞쪽 8줄이라 전송한 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다.
오류 출력 (stderr) 을 따로 모아 오류 · 권한 줄을 끝쪽부터 보이고, 원본을 못 지웠으면 (failed to remove 등) 쉬운 말 한 줄을 앞에 붙인다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-11 22:39:12 +09:00
parent 44663ec2b9
commit b0d7f021f7
5 changed files with 52 additions and 1 deletions

View File

@ -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",

View File

@ -1349,6 +1349,7 @@
"원본 유지 (재인코딩 없음)": "元のまま(再エンコードなし)",
"원본 크기": "元のサイズ",
"원본 파일 지우기 (--remove-source-files)": "元ファイルを削除 (--remove-source-files)",
"원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "元のファイルを削除できませんでした (権限がないか使用中)。移動先にはコピーされ、元のファイルは残っています。",
"원본 파일이 지워집니다 (이동).": "元のファイルは削除されます (移動)。",
"원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "元のファイルは削除されます (移動)。設定の rsync オプションに --remove-source-files があるためオンにしています - オフにすると今回は元を残します。",
"원본 폴더": "元フォルダー",

View File

@ -1349,6 +1349,7 @@
"원본 유지 (재인코딩 없음)": "保持原样(不重新编码)",
"원본 크기": "原始尺寸",
"원본 파일 지우기 (--remove-source-files)": "删除源文件 (--remove-source-files)",
"원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.": "无法删除源文件(没有权限或正在使用)。已复制到目标,源文件仍保留。",
"원본 파일이 지워집니다 (이동).": "源文件将被删除(移动)。",
"원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "源文件将被删除(移动)。因为设置中的 rsync 选项含有 --remove-source-files 所以已勾选 - 取消勾选则本次保留源文件。",
"원본 폴더": "源文件夹",

View File

@ -14,6 +14,29 @@ import '../l10n/tr.dart';
/// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다:
/// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률
/// - 아래쪽: 지금 항목 (폴더) 의 파일 중 몇 개를 했는지
/// 215: rsync 가 실패했을 때 보일 글 - 원본을 못 지웠으면 쉬운 말 한 줄, 그리고 오류 · 권한 줄을 끝쪽부터 (전송한 파일 이름이 아니라).
/// 예전에는 출력의 앞쪽 8줄이라 파일 이름만 보이고 "Permission denied" · "rsync error … (code 23)" 가 빠지기 쉬웠다
String rsyncFailureText(int code, List<String> errLines, List<String> 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<String> sources;
final String dest;
@ -302,6 +325,9 @@ class TransferJob extends ChangeNotifier {
/// 214: 이동인데 원본에 남긴 파일 (맞춘 뒤 바뀜)
final skippedSources = <String>[];
/// 215: 프로그램이 오류 출력 (stderr) 으로 낸 줄 (끝의 20줄)
final errLog = <String>[];
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])));

View File

@ -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;