151 lines
5.9 KiB
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));
|