import 'dart:io'; import '../../l10n/tr.dart'; import '../vfs.dart'; import '../webdav.dart' show DavException, DavLockedException; // 약속 (나누기 file_ops): 전송의 안전장치 - 취소 표시 · "원본을 읽을 수 없음" · 지우기 전 원본 확인 · 빈 폴더 정리. // 입력: 원본 · 대상 폴더 경로 (로컬 · WebDAV). 출력: 없음 / 지운 폴더 수. // 오류: [ensureSourceForDelete] 는 원본을 읽지 못하거나 원본이 비었는데 대상에 파일이 있으면 [SourceUnreadableException] // (그때는 아무것도 지우지 않게). [removeEmptyDirs] 는 지우지 못한 폴더를 남기고 던지지 않는다. /// 사용자가 복사 · 이동 · 맞추기를 멈췄다 class FileOpCancelled implements Exception { const FileOpCancelled(); } /// 동기화 · 미러: 원본을 읽지 못했거나 (권한 꺼짐 · SD 카드 빠짐 · 네트워크 끊김) 원본이 통째로 비었는데 대상에는 파일이 있다. /// "원본에 없는 것을 대상에서 지우기" 가 원본을 빈 것으로 보고 대상을 모두 지우지 않도록, 아무것도 지우지 않고 멈춘다. class SourceUnreadableException implements Exception { final String path; final Object? cause; /// 읽기는 됐지만 원본이 비어 있음 (대상에는 파일이 있음) final bool empty; const SourceUnreadableException(this.path, {this.cause, this.empty = false}); /// 128: 마스터 비밀번호를 기다리는 중 (읽지 못한 것이 아님) bool get locked => cause is DavLockedException; /// 원인을 짧게 (예외 원문 대신 - 68) String get detail { final c = cause; if (c == null) return ''; if (c is FileSystemException) { if (c.message == '없음' && c.osError == null) return tr('폴더가 없습니다'); return c.osError?.message.trim().isNotEmpty == true ? c.osError!.message.trim() : c.message; } if (c is DavException) return c.message; return '$c'.split('\n').first; } @override String toString() => empty ? trf('원본 폴더가 비어 있어 멈췄습니다 (대상 파일은 지우지 않음). SD 카드 · 네트워크 · 권한을 확인하세요: {0}', [path]) : trf('원본 폴더를 읽을 수 없어 멈췄습니다 (대상 파일은 지우지 않음): {0}{1}', [path, detail.isEmpty ? '' : ' · $detail']); } /// "지우기 포함" 으로 맞추기 전에: 원본을 읽을 수 있는지, 원본이 통째로 비었는데 대상에 파일이 있지 않은지. /// 문제가 있으면 [SourceUnreadableException] (아무것도 지우지 않게). /// [allowEmpty]: 사용자가 지울 목록을 보고 "원본이 비어도 그래도 맞추기" 를 고른 경우 (70) - 읽지 못하는 것은 그래도 막는다 Future ensureSourceForDelete(String src, String dst, {bool allowEmpty = false}) async { List<({String path, bool isDir, int size, DateTime modified})> items; try { if (!await vExists(src)) throw const FileSystemException('없음'); items = await vList(src, strict: true); } catch (e) { throw SourceUnreadableException(src, cause: e); } if (items.isNotEmpty || allowEmpty) return; var dstItems = const <({String path, bool isDir, int size, DateTime modified})>[]; try { dstItems = await vList(dst); } catch (_) {} if (dstItems.isNotEmpty) throw SourceUnreadableException(src, empty: true); } /// [root] 안의 빈 폴더를 안쪽부터 지운다 (find root/ -type d -empty -delete 와 같음). 지운 폴더 수. /// [keepRoot] 면 [root] 자신은 비어도 남긴다. 파일 · 링크가 하나라도 있는 폴더는 그대로. Future removeEmptyDirs(String root, {bool keepRoot = true}) async { if (isDav(root)) return _removeEmptyDirsV(root, keepRoot: keepRoot); var n = 0; Future walk(Directory d) async { var empty = true; final items = await d.list(followLinks: false).handleError((_) => empty = false).toList(); for (final e in items) { if (e is Directory && await walk(e)) { try { await e.delete(); n++; } catch (_) { empty = false; } } else { empty = false; } } return empty; } final d = Directory(root); if (!await d.exists()) return 0; if (await walk(d) && !keepRoot) { try { await d.delete(); n++; } catch (_) {} } return n; } /// [removeEmptyDirs] 의 WebDAV 판 Future _removeEmptyDirsV(String root, {bool keepRoot = true}) async { var n = 0; Future walk(String dir) async { var empty = true; for (final e in await vList(dir)) { if (e.isDir && await walk(e.path)) { try { await vDelete(e.path); n++; } catch (_) { empty = false; } } else { empty = false; } } return empty; } if (!await vExists(root)) return 0; if (await walk(root) && !keepRoot) { try { await vDelete(root); n++; } catch (_) {} } return n; }