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 folders; /// 큰 파일 (큰 것부터, [StorageScanner.topFiles] 개까지) final List bigFiles; /// 폴더 → 바로 안의 폴더들 (차례 없음) final Map> 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 biggestIn(String dir) => [...?children[key(dir)]]..sort((a, b) => sizeOf(b).compareTo(sizeOf(a))); } /// 87: 고른 폴더 (또는 드라이브) 아래를 한 번 훑어 [StorageScan] 을 만든다 class StorageScanner { /// 읽기 (시험에서 바꿔 끼움) - 80 과 같이 링크는 빼고 수만 final Future Function(String dir) list; final int topFiles; StorageScanner({Future Function(String dir)? list, this.topFiles = 200}) : list = list ?? FolderSizes.listForSize; /// [onProgress]: (읽은 폴더 수, 지금까지 크기). [cancelled] 가 참이면 [StorageScanCancelled] Future scan(String root, {void Function(int dirs, int bytes)? onProgress, bool Function()? cancelled}) async { final key = StorageScan.key; final own = {}; // 폴더 바로 안의 파일 크기 · 수 final children = >{}; final parentOf = {}; final order = []; // 읽은 차례 (위 → 아래) final seen = {}; var big = []; 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.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));