const fs = require("fs"); const path = require("path"); const { resolveData } = require("./paths"); const CATEGORY_DIRS = { models: "models", runtime: "runtime", logs: "logs", metrics: "metrics", diagnostics: "diagnostics", cache: "cache", feedback: "feedback", corrections: "corrections", evals: "evals", exports: "exports", tools: "tools", tmp: "tmp" }; function folderSize(dir) { if (!fs.existsSync(dir)) return 0; return fs.readdirSync(dir, { withFileTypes: true }).reduce((total, entry) => { if (entry.name === ".gitkeep") return total; const target = path.join(dir, entry.name); return total + (entry.isDirectory() ? folderSize(target) : entry.isFile() ? fs.statSync(target).size : 0); }, 0); } function storageUsage(models = [], selectedModelId = null) { const categories = Object.fromEntries( Object.entries(CATEGORY_DIRS).map(([key, dir]) => [key, folderSize(resolveData(dir))]) ); const installedModels = models.map((model) => { const file = modelPath(model); return { id: model.id, label: model.label, filename: model.filename, selected: model.id === selectedModelId, installed: fs.existsSync(file), size: fs.existsSync(file) ? fs.statSync(file).size : 0 }; }); const runtimeArchives = listFiles(resolveData("tmp")) .filter((file) => /\.(zip|tar\.gz|part)$/i.test(file.name)); return { categories, installed_models: installedModels, runtime_archives: runtimeArchives, total: Object.values(categories).reduce((sum, value) => sum + value, 0) }; } function deleteModel(model, { selectedModelId, gateModelId = null, runtimeRunning, gateRuntimeRunning = false, confirmed }) { if (!model) throw new Error("Unknown model."); if (!confirmed) throw new Error("Model deletion requires confirmation."); if (model.id === selectedModelId && runtimeRunning) { throw new Error("Stop the runtime before deleting the selected model."); } if (model.id === gateModelId && gateRuntimeRunning) { throw new Error("Stop the gate runtime before deleting the gate model."); } const file = modelPath(model); if (!fs.existsSync(file)) return { deleted: false, bytes_recovered: 0 }; const bytes = fs.statSync(file).size; fs.rmSync(file, { force: true }); return { deleted: true, bytes_recovered: bytes }; } function cleanupStorage(categories, { models, selectedModelId, gateModelId = null, runtimeRunning, activeLogPath = null }) { const selected = new Set(Array.isArray(categories) ? categories : []); const result = {}; if (selected.has("unused_models")) { let recovered = 0; for (const model of models) { if (model.id === selectedModelId || model.id === gateModelId) continue; const file = modelPath(model); if (fs.existsSync(file)) { recovered += fs.statSync(file).size; fs.rmSync(file, { force: true }); } } result.unused_models = recovered; } if (selected.has("runtime_archives")) { result.runtime_archives = removeMatching(resolveData("tmp"), (name) => /\.(zip|tar\.gz|part)$/i.test(name)); } if (selected.has("logs")) { result.logs = removeMatching(resolveData("logs"), (_name, _entry, target) => !activeLogPath || path.resolve(target) !== path.resolve(activeLogPath) ); } if (selected.has("diagnostics")) result.diagnostics = removeMatching(resolveData("diagnostics"), () => true); if (selected.has("cache")) result.cache = clearDirectory(resolveData("cache")); if (selected.has("tmp")) result.tmp = clearDirectory(resolveData("tmp")); if (selected.has("metrics")) result.metrics = clearDirectory(resolveData("metrics")); if (selected.has("runtime")) { if (runtimeRunning) throw new Error("Stop the runtime before removing extracted runtime files."); result.runtime = clearDirectory(resolveData("runtime")); } return { recovered_bytes: Object.values(result).reduce((sum, value) => sum + value, 0), categories: result }; } function listLogs() { return listFiles(resolveData("logs")) .filter((file) => file.name.endsWith(".log")) .sort((a, b) => b.modified_at_ms - a.modified_at_ms); } function listLogsPage(pageValue = 1, pageSize = 25) { return paginateFileRows(listLogs(), pageValue, pageSize); } function paginateFileRows(rows, pageValue = 1, pageSize = 25) { const size = Math.max(1, Number.parseInt(pageSize, 10) || 25); const pages = Math.max(1, Math.ceil(rows.length / size)); const page = Math.min(pages, Math.max(1, Number.parseInt(pageValue, 10) || 1)); const start = (page - 1) * size; return { entries: rows.slice(start, start + size), page, pages, page_size: size, total: rows.length }; } function resolveLog(name) { const safeName = path.basename(String(name || "")); if (!safeName || safeName !== name || !safeName.endsWith(".log")) throw new Error("Invalid log file."); const file = resolveData("logs", safeName); if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error("Log file not found."); return file; } function readLogTail(name, maxBytes = 262144) { const file = resolveLog(name); const stat = fs.statSync(file); const length = Math.min(stat.size, maxBytes); const buffer = Buffer.alloc(length); const descriptor = fs.openSync(file, "r"); try { fs.readSync(descriptor, buffer, 0, length, Math.max(0, stat.size - length)); } finally { fs.closeSync(descriptor); } return { name: path.basename(file), size: stat.size, modified_at: stat.mtime.toISOString(), truncated: stat.size > length, content: buffer.toString("utf8") }; } function deleteLog(name, activeLogPath = null) { const file = resolveLog(name); if (activeLogPath && path.resolve(file) === path.resolve(activeLogPath)) { throw new Error("The active runtime log cannot be deleted."); } const bytes = fs.statSync(file).size; fs.rmSync(file, { force: true }); return { deleted: true, bytes_recovered: bytes }; } function modelPath(model) { const file = resolveData("models", model.filename); const modelsDir = resolveData("models"); if (path.dirname(file) !== modelsDir) throw new Error("Invalid model path."); return file; } function listFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => { const file = path.join(dir, entry.name); const stat = fs.statSync(file); return { name: entry.name, size: stat.size, modified_at: stat.mtime.toISOString(), modified_at_ms: stat.mtimeMs }; }); } function removeMatching(dir, predicate) { if (!fs.existsSync(dir)) return 0; let recovered = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const target = path.join(dir, entry.name); if (!predicate(entry.name, entry, target)) continue; recovered += entry.isDirectory() ? folderSize(target) : entry.isFile() ? fs.statSync(target).size : 0; fs.rmSync(target, { recursive: true, force: true }); } return recovered; } function clearDirectory(dir) { return removeMatching(dir, () => true); } module.exports = { folderSize, storageUsage, deleteModel, cleanupStorage, listLogs, listLogsPage, paginateFileRows, resolveLog, readLogTail, deleteLog, modelPath };