191 lines
6.6 KiB
TypeScript
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 [];
|
|
}
|
|
}
|