kv-clearnup/electron/features/scanner.ts
2026-02-02 08:33:46 +07:00

191 lines
6.6 KiB
TypeScript

import fs from 'fs/promises';
import path from 'path';
import os from 'os';
interface ScanResult {
path: string;
size: number;
lastAccessed: Date;
type: 'node_modules' | 'vendor' | 'venv';
}
export async function scanDirectory(rootDir: string, maxDepth: number = 5): Promise<ScanResult[]> {
const results: ScanResult[] = [];
async function traverse(currentPath: string, depth: number) {
if (depth > maxDepth) return;
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'vendor' || entry.name === '.venv') {
// Found a target
try {
const stats = await fs.stat(fullPath);
results.push({
path: fullPath,
size: 0, // Calculating size is expensive, might do lazily or separate task
lastAccessed: stats.atime,
type: entry.name as any
});
// Don't traverse inside node_modules
continue;
} catch (e) {
console.error(`Error stat-ing ${fullPath}`, e);
}
} else if (!entry.name.startsWith('.')) {
// Recurse normal directories
await traverse(fullPath, depth + 1);
}
}
}
} catch (error) {
console.error(`Error scanning ${currentPath}`, error);
}
}
await traverse(rootDir, 0);
return results;
}
export async function getFolderSize(folderPath: string): Promise<number> {
let total = 0;
try {
const stats = await fs.stat(folderPath);
if (stats.isFile()) return stats.size;
const files = await fs.readdir(folderPath, { withFileTypes: true });
for (const file of files) {
total += await getFolderSize(path.join(folderPath, file.name));
}
} catch (e) {
// ignore errors
}
return total;
}
export interface DeepScanResult {
path: string;
size: number;
isDirectory: boolean;
}
export async function findLargeFiles(rootDir: string, threshold: number = 100 * 1024 * 1024): Promise<DeepScanResult[]> {
const results: DeepScanResult[] = [];
async function traverse(currentPath: string) {
try {
const stats = await fs.stat(currentPath);
if (stats.size > threshold && !stats.isDirectory()) {
results.push({ path: currentPath, size: stats.size, isDirectory: false });
return;
}
if (stats.isDirectory()) {
// SKIP node_modules to prevent self-deletion of the running app!
if (path.basename(currentPath) === 'node_modules') return;
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') && entry.name !== '.Trash') continue;
await traverse(path.join(currentPath, entry.name));
}
}
} catch (e) { /* skip */ }
}
await traverse(rootDir);
return results.sort((a, b) => b.size - a.size);
}
export async function getDeepDiveSummary() {
const home = os.homedir();
const targets = [
path.join(home, 'Downloads'),
path.join(home, 'Documents'),
path.join(home, 'Desktop'),
path.join(home, 'Library/Application Support'),
];
const results: DeepScanResult[] = [];
for (const t of targets) {
console.log(`Scanning ${t}...`);
const large = await findLargeFiles(t, 50 * 1024 * 1024); // 50MB+
console.log(`Found ${large.length} large files in ${t}`);
results.push(...large);
}
return results.slice(0, 20); // Top 20
}
import { exec } from 'child_process';
import util from 'util';
const execPromise = util.promisify(exec);
export async function getDiskUsage() {
try {
// macOS/Linux: df -k / gives 1K-blocks
const { stdout } = await execPromise('df -k /');
const lines = stdout.trim().split('\n');
// Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on
// /dev/disk3s1s1 488245288 15266888 308805360 5% 350280 1957260560 0% /
if (lines.length < 2) return null;
const parts = lines[1].split(/\s+/);
// parts[1] is total in 1K blocks
// parts[2] is used
// parts[3] is available
const total = parseInt(parts[1]) * 1024;
const used = parseInt(parts[2]) * 1024;
const available = parseInt(parts[3]) * 1024;
return {
totalGB: (total / 1024 / 1024 / 1024).toFixed(2),
usedGB: (used / 1024 / 1024 / 1024).toFixed(2),
freeGB: (available / 1024 / 1024 / 1024).toFixed(2)
};
} catch (e) {
console.error("Error getting disk usage:", e);
return null;
}
}
export async function findHeavyFolders(rootDir: string): Promise<DeepScanResult[]> {
try {
console.log(`Deepest scan on: ${rootDir}`);
// du -k -d 2: report size in KB, max depth 2
// sort -nr: numeric reverse sort
// head -n 50: top 50
const { stdout } = await execPromise(`du -k -d 2 "${rootDir}" | sort -nr | head -n 50`);
const lines = stdout.trim().split('\n');
const results = lines.map(line => {
// Trim leading whitespace
const trimmed = line.trim();
// Split by first whitespace only
const firstSpace = trimmed.indexOf('\t'); // du output is usually tab separated or space
// Actually du output on mac is "size<tab>path"
// Robust splitting for size and path
const match = trimmed.match(/^(\d+)\s+(.+)$/);
if (!match) return null;
const sizeK = parseInt(match[1]);
const fullPath = match[2];
return {
path: fullPath,
size: sizeK * 1024, // Convert KB to Bytes
isDirectory: true
};
}).filter(item => item !== null && item.path !== rootDir) as DeepScanResult[];
return results;
} catch (e) {
console.error("Deepest scan failed:", e);
return [];
}
}