/// 약속 (modular_sync #6 · F18): 복사 · 이동 · 동기화가 사용자에게 보이는 실패 글 - 예외 이름 ("ProcessException:") · Dart 의 영어 파일 오류 /// ("Cannot copy file") 를 빼고 화면 언어로. 입력: 예외 (와 원본 경로들), 출력: 글. 원래 글 ('$e') 은 부르는 쪽이 작업 기록에 남긴다. /// 쓰는 곳: 복사 · 이동 작업 (copy_center) · 실시간 동기화 상태 (live_sync) library; import 'dart:io'; import '../core/file_ops.dart' show isSameOrInside; import '../l10n/tr.dart'; /// 237: 사용자에게 보이는 실패 글 - "ProcessException:" 같은 예외 이름 · "Command:" 줄은 빼고 내용만. /// 원래 글 ('$e') 은 작업 기록에 그대로 남긴다. /// 245: 앱이 직접 할 때의 파일 오류 (Dart 의 "Cannot delete file" 등 영어) 는 화면 언어로. [sources] 안의 파일을 못 지웠으면 /// rsync 때 (215) 와 같은 글 - 이동에서 원본만 남은 것 String userErrorText(Object e, {List sources = const []}) => switch (e) { ProcessException(:final message) when message.isNotEmpty => message, FileSystemException(:final message, :final path, :final osError) => [ _fileErrorText(message, path, sources), if (path != null && path.isNotEmpty) path, if (osError != null && osError.message.isNotEmpty) osError.message, ].join(' · '), _ => '$e'.replaceFirst(RegExp(r'^\w*(Exception|Error)(\([^)]*\))?: '), ''), }; String _fileErrorText(String message, String? path, List sources) { final m = message.toLowerCase(); if (m.startsWith('cannot delete') || m.startsWith('deletion failed')) { final inSource = path != null && sources.any((s) => isSameOrInside(path, s)); return inSource ? tr('원본 파일을 지우지 못했습니다 (권한이 없거나 사용 중). 대상에는 복사됐고 원본은 남아 있습니다.') : tr('지우지 못했습니다 (권한이 없거나 사용 중)'); } if (m.startsWith('cannot copy')) return tr('파일을 복사하지 못했습니다'); if (m.startsWith('cannot rename')) return tr('옮기거나 이름을 바꾸지 못했습니다'); if (m.startsWith('cannot open') || m.startsWith('cannot read')) return tr('파일을 읽지 못했습니다'); if (m.startsWith('cannot create') || m.startsWith('creation failed') || m.startsWith('cannot write')) { return tr('만들거나 쓰지 못했습니다'); } return message; } /// 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'); }