import 'dart:io'; import 'package:path/path.dart' as p; import '../../l10n/tr.dart'; import '../vfs.dart'; import 'path_rules.dart'; import 'transfer_control.dart'; import 'same_file.dart'; // 약속 (나누기 file_ops): 로컬 (같은 기기의 폴더 · 네트워크 공유) 끼리의 복사 · 이동 · 맞추기 · 지우기 · 크기. // 입력: 설정 · 상태 [TransferControl], 원본 경로들 · 받는 폴더. 출력: 만든 경로들 · 복사한 파일 수. // 오류: 취소 → [FileOpCancelled] (만들다 만 파일은 지움) · 폴더를 자기 안으로 → [FileSystemException] · // 맞추기에서 원본 폴더를 읽지 못함 → [SourceUnreadableException] (그 폴더에서는 아무것도 지우지 않음). 그 밖의 파일 오류는 그대로. // WebDAV 등이 섞이면 any_transfer 를 쓴다 (고르는 것은 [FileOps]). /// 전체 크기 (진행률용) Future localTotalSize(List paths) async { var total = 0; for (final s in paths) { final t = FileSystemEntity.typeSync(s, followLinks: false); if (t == FileSystemEntityType.directory) { await for (final e in Directory(s).list(recursive: true, followLinks: false).handleError((_) {})) { if (e is File) { try { total += await e.length(); } catch (_) {} } } } else if (t == FileSystemEntityType.file) { total += await File(s).length(); } } return total; } /// [sources] 를 [destDir] 로 복사 (같은 이름이 있으면 [TransferControl.conflict] 대로). 만든 경로들을 돌려준다. Future> localCopy(TransferControl c, List sources, String destDir, {CopyProgress? onProgress}) async { final total = await localTotalSize(sources); var done = 0; final made = []; // 43: 임시 이름 (.jjpart) 에 다 쓴 뒤 이름을 바꾼다 - 복사 도중 앱이 끝나도 반쯤 쓴 파일이 완성된 이름으로 남지 않게 Future copyFile(File f, String finalTarget) async { c.check(); final target = '$finalTarget.jjpart'; final out = File(target).openWrite(); try { try { await for (final chunk in f.openRead()) { c.check(); out.add(chunk); done += chunk.length; onProgress?.call(f.path, done, total); c.onBytes?.call(chunk.length); await c.throttle(chunk.length); } } finally { await out.close(); } c.check(); } catch (_) { // 취소 · 실패: 만들다 만 파일을 남기지 않는다 try { await File(target).delete(); } catch (_) {} rethrow; } // 덮어쓰기: 새 파일을 다 쓴 뒤에야 옛 파일을 지운다 (도중에 실패하면 옛 파일이 남게) if (File(finalTarget).existsSync()) await File(finalTarget).delete(); await File(target).rename(finalTarget); try { await File(finalTarget).setLastModified(await f.lastModified()); } catch (_) {} c.onFileDone?.call(f.path); } Future copyAny(String src, String target, {bool inner = false}) async { if (FileSystemEntity.isDirectorySync(src)) { await Directory(target).create(recursive: true); await for (final e in Directory(src).list(followLinks: false)) { await copyAny(e.path, p.join(target, p.basename(e.path)), inner: true); } } else { // 229: 합치는 폴더 안에서 받는 쪽이 더 새것이면 예전 것으로 덮지 않는다 if (inner && c.skipNewerInMerge && await isNewerThere(src, target)) { c.keptNewer.add(src); return; } await copyFile(File(src), target); } } for (final s in sources) { if (FileSystemEntity.isDirectorySync(s) && isSameOrInside(destDir, s)) { throw FileSystemException(tr('폴더를 자기 안으로 복사할 수 없습니다'), s); } final target = await c.targetFor(s, destDir); if (target == null) continue; await copyAny(s, target); made.add(target); } return made; } /// 229: [dir] 안에서 [keep] 의 파일만 남기고 지운다 (비게 된 폴더도). [dir] 자체는 남는 것이 있으면 둔다 Future _deleteExcept(String dir, Set keep) async { Future walk(Directory d) async { var empty = true; await for (final e in d.list(followLinks: false)) { if (e is Directory) { if (await walk(e)) { await e.delete(); } else { empty = false; } } else if (keep.contains(p.normalize(e.path))) { empty = false; } else { await e.delete(); } } return empty; } if (await walk(Directory(dir))) await Directory(dir).delete(); } /// 229: [target] 이 있고 [src] 보다 2초 넘게 새것인지 Future isNewerThere(String src, String target) async { final t = File(target); if (!await t.exists()) return false; return SameFile.keepNewer(skipNewer: true, targetModified: await t.lastModified(), modified: await File(src).lastModified()); } /// 229: 같은 이름 창 - 폴더를 합치면 안쪽에서 받는 쪽이 더 새 파일 수 ([limit] 개까지 셈, 로컬만) Future countNewerInside(String src, String dst, {int limit = 5000}) async { var n = 0, seen = 0; Future walk(String s, String d) async { await for (final e in Directory(s).list(followLinks: false).handleError((_) {})) { if (seen++ > limit) return; final t = p.join(d, p.basename(e.path)); if (e is Directory) { if (FileSystemEntity.isDirectorySync(t)) await walk(e.path, t); } else if (await isNewerThere(e.path, t)) { n++; } } } if (FileSystemEntity.isDirectorySync(src) && FileSystemEntity.isDirectorySync(dst)) await walk(src, dst); return n; } /// 이동: 같은 드라이브면 이름 바꾸기, 아니면 복사한 뒤 원본 삭제 Future> localMove(TransferControl c, List sources, String destDir, {CopyProgress? onProgress}) async { final made = []; for (final s in sources) { c.check(); if (isSameOrInside(destDir, s)) throw FileSystemException(tr('폴더를 자기 안으로 옮길 수 없습니다'), s); if (samePath(p.dirname(s), destDir)) { made.add(s); // 이미 그 폴더에 있음 continue; } final target = await c.targetFor(s, destDir); if (target == null) continue; final exists = FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound; if (exists && FileSystemEntity.isDirectorySync(s)) { // 덮어쓰기로 폴더를 합친다: 복사 (안의 같은 이름은 덮어씀) 뒤 원본 삭제 final keptBefore = c.keptNewer.length; made.addAll(await localCopy(c, [s], destDir, onProgress: onProgress)); c.check(); final kept = c.keptNewer.sublist(keptBefore); if (kept.isEmpty) { await deletePaths(c, [s]); } else { // 229: 건너뛴 (받는 쪽이 더 새) 원본은 남기고 옮긴 것만 지운다 await _deleteExcept(s, kept.map(p.normalize).toSet()); } continue; } try { final t = FileSystemEntity.typeSync(s); // 덮어쓰기 (파일): 이름 바꾸기가 옛 파일을 바로 바꿔 놓는다 (다른 드라이브면 아래 복사가 덮어씀) if (t == FileSystemEntityType.directory) { await Directory(s).rename(target); } else { await File(s).rename(target); } made.add(target); } on FileSystemException { // 다른 드라이브 · 저장 장치: 복사 후 삭제 final copied = await localCopy(c, [s], destDir, onProgress: onProgress); await deletePaths(c, [s]); made.addAll(copied); } } return made; } /// 지우기 (로컬 · WebDAV). 없는 것은 그냥 지나간다. 항목마다 취소를 본다 Future deletePaths(TransferControl c, List paths) async { for (final s in paths) { c.check(); if (isDav(s)) { await vDelete(s); continue; } final t = FileSystemEntity.typeSync(s, followLinks: false); if (t == FileSystemEntityType.directory) { await Directory(s).delete(recursive: true); } else if (t != FileSystemEntityType.notFound) { await File(s).delete(); } } }