JJ_MKVMaker/lib/core/storage_scan.dart

151 lines
5.9 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'file_ops.dart';
import 'folder_size.dart';
import 'vfs.dart';
/// 87: 저장 공간 분석 결과 - 폴더마다 크기 (안쪽 모두 합) · 큰 파일 차례.
/// 80 의 폴더 크기 규칙을 그대로: 링크 · junction 은 따라가지 않고 (수만), 이미 센 폴더 (실제 경로) 는 다시 세지 않고, 취소된다
class StorageScan {
final String root;
/// 폴더 → (크기, 파일 수). 열쇠는 [key] 로
final Map<String, (int, int)> folders;
/// 큰 파일 (큰 것부터, [StorageScanner.topFiles] 개까지)
final List<FileEntry> bigFiles;
/// 폴더 → 바로 안의 폴더들 (차례 없음)
final Map<String, List<String>> children;
/// 읽지 못한 폴더 수 (권한 등 - 크기가 실제보다 작을 수 있음) · 세지 않은 링크 수
final int unreadable, links;
const StorageScan(this.root, this.folders, this.bigFiles, this.children, {this.unreadable = 0, this.links = 0});
static String key(String path) => isDav(path) ? vNorm(path) : path.toLowerCase();
int sizeOf(String dir) => folders[key(dir)]?.$1 ?? 0;
int filesOf(String dir) => folders[key(dir)]?.$2 ?? 0;
/// [dir] 바로 안의 폴더를 큰 것부터
List<String> biggestIn(String dir) => [...?children[key(dir)]]..sort((a, b) => sizeOf(b).compareTo(sizeOf(a)));
/// X-plore 비교 5-1 ⑤ (관리자 지시 10/21): 지운 것 · 옮긴 것을 다시 훑지 않고 결과에서 뺀다 - 폴더면 그 아래 모두, 파일이면 그것.
/// 위 폴더들 (분석한 곳까지) 의 크기 · 파일 수에서 덜어 낸다. [fileSize] 는 큰 파일 목록에 없는 파일의 크기 (모르면 0)
void remove(String path, {int fileSize = 0}) {
final k = key(path);
var (size, files) = (fileSize, 1);
final folder = folders[k];
if (folder != null) {
(size, files) = folder;
// 그 아래 폴더 모두
final stack = [k];
while (stack.isNotEmpty) {
final d = stack.removeLast();
folders.remove(d);
for (final c in children.remove(d) ?? const <String>[]) {
stack.add(key(c));
}
}
bigFiles.removeWhere((f) => isSameOrInside(f.path, path));
} else {
final i = bigFiles.indexWhere((f) => key(f.path) == k);
if (i >= 0) size = bigFiles.removeAt(i).size;
}
final parent = vDirname(path);
children[key(parent)]?.removeWhere((c) => key(c) == k);
// 위 폴더들 (분석한 곳까지) 에서 덜어 냄
var d = parent;
while (true) {
final dk = key(d);
final v = folders[dk];
if (v != null) folders[dk] = ((v.$1 - size).clamp(0, 1 << 62), (v.$2 - files).clamp(0, 1 << 62));
if (dk == key(root) || samePath(vDirname(d), d)) break;
d = vDirname(d);
}
}
}
/// 87: 고른 폴더 (또는 드라이브) 아래를 한 번 훑어 [StorageScan] 을 만든다
class StorageScanner {
/// 읽기 (시험에서 바꿔 끼움) - 80 과 같이 링크는 빼고 수만
final Future<FolderListing> Function(String dir) list;
final int topFiles;
StorageScanner({Future<FolderListing> Function(String dir)? list, this.topFiles = 200}) : list = list ?? FolderSizes.listForSize;
/// [onProgress]: (읽은 폴더 수, 지금까지 크기). [cancelled] 가 참이면 [StorageScanCancelled]
Future<StorageScan> scan(String root, {void Function(int dirs, int bytes)? onProgress, bool Function()? cancelled}) async {
final key = StorageScan.key;
final own = <String, (int, int)>{}; // 폴더 바로 안의 파일 크기 · 수
final children = <String, List<String>>{};
final parentOf = <String, String>{};
final order = <String>[]; // 읽은 차례 (위 → 아래)
final seen = <String>{};
var big = <FileEntry>[];
var bytes = 0, unreadable = 0, links = 0;
final pending = [root];
while (pending.isNotEmpty) {
if (cancelled?.call() ?? false) throw const StorageScanCancelled();
final dir = pending.removeLast();
var real = dir;
if (!isDav(dir)) {
try {
real = Directory(dir).resolveSymbolicLinksSync();
} catch (_) {}
}
if (!seen.add(key(real))) continue;
order.add(dir);
var size = 0, count = 0;
try {
final r = await list(dir);
links += r.links;
for (final e in r.entries) {
if (e.isDir) {
pending.add(e.path);
children.putIfAbsent(key(dir), () => []).add(e.path);
parentOf[key(e.path)] = key(dir);
} else {
final s = e.size > 0 ? e.size : 0;
size += s;
count++;
if (big.length < topFiles || s > big.last.size) {
big.add(e);
big.sort((a, b) => b.size.compareTo(a.size));
if (big.length > topFiles) big = big.sublist(0, topFiles);
}
}
}
} catch (_) {
unreadable++;
}
own[key(dir)] = (size, count);
bytes += size;
if (order.length % 50 == 0) onProgress?.call(order.length, bytes);
// 큰 폴더를 오래 읽을 때도 화면이 멈추지 않게
if (order.length % 200 == 0) await Future<void>.delayed(Duration.zero);
}
// 아래부터 위로 더한다
final total = {for (final e in own.entries) e.key: e.value};
for (final d in order.reversed) {
final k = key(d);
final up = parentOf[k];
if (up == null || !total.containsKey(up)) continue;
final (s, n) = total[k]!;
final (us, un) = total[up]!;
total[up] = (us + s, un + n);
}
onProgress?.call(order.length, bytes);
return StorageScan(root, total, big, children, unreadable: unreadable, links: links);
}
}
class StorageScanCancelled implements Exception {
const StorageScanCancelled();
}
/// 이름만 (화면 줄)
String scanName(String path) => isDav(path) ? vBasename(path) : (p.basename(path).isEmpty ? path : p.basename(path));