import 'dart:io'; import 'package:path/path.dart' as p; import '../core/archive_ops.dart'; import '../core/file_ops.dart' show FileEntry, NameConflict, isSameOrInside, samePath; import '../core/vfs.dart' show isDav; /// 약속 (E5c): 압축 (ZIP · 7z · RAR) 을 폴더처럼 본다 - 읽기 전용. 압축 읽기 · 풀기는 archive_ops 를 부르기만 한다. /// - [read]: 압축 경로 → [ArchiveRead] (읽음 · 못 읽음의 까닭). 한 번 읽은 것은 압축 파일의 크기 · 바뀐 때가 같으면 다시 읽지 않고, /// 바뀌었으면 다시 읽고, 없어졌으면 잊는다. /// - [list]: 압축 안 폴더 → 바로 아래 항목 (목록에 폴더 이름이 없어도 경로에서 폴더를 만든다, 위험한 이름 · 링크는 뺀다). /// - [archiveOf] · [inside] · [innerName]: 경로가 든 (펼친) 압축 · 압축 안인지 · 압축 안 이름 ("폴더/파일"). /// - [extractOne]: 압축 안 파일 하나를 [tempRoot] 아래로 꺼냄 → 꺼낸 경로 또는 못 꺼낸 까닭. /// - 7-Zip 실행 파일은 부르는 쪽이 준다 ([sevenZip], 없으면 null - 7z · RAR 는 못 읽음). /// - 다시 쓰는 곳: 탐색기 트리 · 찾기 (압축 안) · 보관함 미리 보기. enum ArchiveRead { ok, gone, noTool, password, failed } class ArchiveBrowser { ArchiveBrowser(this.sevenZip, {this.onForget}); final Future Function() sevenZip; /// 읽어 둔 압축을 잊을 때 (바뀜 · 없어짐) - 부르는 쪽이 그 안의 읽어 둔 목록을 지우게 final void Function(String arc)? onForget; /// 펼쳐 본 압축의 목록 (압축 경로 → 항목) final _entries = >{}; /// 압축을 읽은 때의 (크기, 바뀐 때) final _stamps = {}; /// [read] 가 [ArchiveRead.failed] 일 때의 까닭 (화면에 보일 말) Object? lastError; /// 트리에서 펼칠 수 있는 압축 (이 기기의 파일) static bool canBrowse(FileEntry e) => !e.isDir && !isDav(e.path) && archiveKindOf(e.path) != null; /// 읽어 둔 압축인지 bool contains(String path) => _entries.containsKey(path); /// [path] 가 들어 있는 (읽어 둔) 압축 - 압축 자신도 String? archiveOf(String path) { String? best; for (final a in _entries.keys) { if ((samePath(path, a) || isSameOrInside(path, a)) && (best == null || a.length > best.length)) best = a; } return best; } /// 압축 안의 항목인지 (압축 파일 자신은 아님) bool inside(String path) { final a = archiveOf(path); return a != null && !samePath(a, path); } /// 압축 안 경로 → 압축 안의 이름 ("폴더/파일") String innerName(String archive, String path) => p.relative(path, from: archive).replaceAll('\\', '/'); /// [arc] 의 읽어 둔 목록을 잊는다 (다시 읽게) void forget(String arc) { _entries.remove(arc); _stamps.remove(arc); onForget?.call(arc); } static Future<(int, DateTime)?> _stampOf(String path) async { try { final st = await File(path).stat(); if (st.type == FileSystemEntityType.notFound) return null; return (st.size, st.modified); } catch (_) { return null; } } Future read(String path) async { final stamp = await _stampOf(path); if (_entries.containsKey(path)) { if (stamp != null && stamp == _stamps[path]) return ArchiveRead.ok; forget(path); if (stamp == null) return ArchiveRead.gone; } try { if (archiveKindOf(path) == ArchiveKind.zip) { _entries[path] = await listZip(path); } else { final exe = await sevenZip(); if (exe == null) return ArchiveRead.noTool; _entries[path] = await list7z(exe, path); } if (stamp != null) _stamps[path] = stamp; return ArchiveRead.ok; } on ArchivePasswordException { return ArchiveRead.password; } catch (e) { lastError = e; return ArchiveRead.failed; } } /// 압축 안 폴더 [dir] 의 바로 아래 항목 List list(String dir) { final arc = archiveOf(dir)!; final rel = samePath(dir, arc) ? '' : innerName(arc, dir); final out = {}; for (final a in _entries[arc]!) { if (a.isLink) continue; final n = a.name.replaceAll('\\', '/').replaceAll(RegExp(r'^(\./)+|/+$'), ''); if (n.isEmpty || unsafeArchivePath(n) != null) continue; if (rel.isNotEmpty && !n.startsWith('$rel/')) continue; final rest = rel.isEmpty ? n : n.substring(rel.length + 1); if (rest.isEmpty) continue; final first = rest.split('/').first; final folder = rest.contains('/') || a.isDir; final path = p.joinAll([arc, ...(rel.isEmpty ? first : '$rel/$first').split('/')]); final old = out[first]; if (old == null || (folder && !old.isDir)) { out[first] = FileEntry(path, isDir: folder, size: folder ? 0 : a.size, modified: a.modified ?? DateTime(0)); } } return out.values.toList(); } /// 압축 안 파일 하나를 꺼낸다 → (꺼낸 경로, 못 꺼낸 까닭). 비밀번호가 걸렸으면 ([ArchiveRead.password]), 7-Zip 이 없으면 /// ([ArchiveRead.noTool]), 실패는 ([ArchiveRead.failed], [lastError]), 꺼냈는데 파일이 없으면 ([ArchiveRead.gone]) Future<(String?, ArchiveRead)> extractOne(String path, String tempRoot) async { final arc = archiveOf(path)!; final inner = innerName(arc, path); final dest = p.join(tempRoot, 'jj_arcview', '${arc.hashCode.toUnsigned(32)}'); final plan = planExtract(_entries[arc]!, names: [inner]); if (plan.encrypted) return (null, ArchiveRead.password); try { if (archiveKindOf(arc) == ArchiveKind.zip) { await extractZipPlan(arc, dest, plan, conflict: NameConflict.overwrite); } else { final exe = await sevenZip(); if (exe == null) return (null, ArchiveRead.noTool); await extract7zPlan(exe, arc, dest, plan, conflict: NameConflict.overwrite); } } catch (e) { lastError = e; return (null, ArchiveRead.failed); } final out = p.joinAll([dest, ...inner.split('/')]); return File(out).existsSync() ? (out, ArchiveRead.ok) : (null, ArchiveRead.gone); } }