149 lines
6.1 KiB
Dart
149 lines
6.1 KiB
Dart
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<String?> Function() sevenZip;
|
|
|
|
/// 읽어 둔 압축을 잊을 때 (바뀜 · 없어짐) - 부르는 쪽이 그 안의 읽어 둔 목록을 지우게
|
|
final void Function(String arc)? onForget;
|
|
|
|
/// 펼쳐 본 압축의 목록 (압축 경로 → 항목)
|
|
final _entries = <String, List<ArchiveEntry>>{};
|
|
|
|
/// 압축을 읽은 때의 (크기, 바뀐 때)
|
|
final _stamps = <String, (int, DateTime)>{};
|
|
|
|
/// [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<ArchiveRead> 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<FileEntry> list(String dir) {
|
|
final arc = archiveOf(dir)!;
|
|
final rel = samePath(dir, arc) ? '' : innerName(arc, dir);
|
|
final out = <String, FileEntry>{};
|
|
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);
|
|
}
|
|
}
|