Lumi/plugins/lumi_ai/backend/storage.js
2026-06-12 19:27:43 +02:00

204 lines
7.1 KiB
JavaScript

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",
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
};