197 lines
6.8 KiB
JavaScript
197 lines
6.8 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",
|
|
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, runtimeRunning, 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.");
|
|
}
|
|
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, 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) 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
|
|
};
|