JJ_MKVMaker/lib/core/files/transfer_guard.dart

130 lines
4.9 KiB
Dart

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<void> 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<int> removeEmptyDirs(String root, {bool keepRoot = true}) async {
if (isDav(root)) return _removeEmptyDirsV(root, keepRoot: keepRoot);
var n = 0;
Future<bool> 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<int> _removeEmptyDirsV(String root, {bool keepRoot = true}) async {
var n = 0;
Future<bool> 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;
}