feat: implement container logs modal, service diagnostics inspector, and directory sizer tool
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
This commit is contained in:
185
server.js
185
server.js
@@ -509,6 +509,127 @@ async function getDisks() {
|
||||
return disks;
|
||||
}
|
||||
|
||||
function isPathAllowed(userPath) {
|
||||
if (!userPath) return false;
|
||||
const resolved = path.resolve(userPath);
|
||||
return resolved.startsWith('/host/volume1') || resolved.startsWith('/host/volume2') || resolved === '/';
|
||||
}
|
||||
|
||||
async function getDirectorySize(dirPath, maxDepth = 3, currentDepth = 0) {
|
||||
let size = 0;
|
||||
try {
|
||||
const stats = await fsp.lstat(dirPath);
|
||||
if (stats.isSymbolicLink()) return 0;
|
||||
if (stats.isFile()) {
|
||||
return stats.size;
|
||||
}
|
||||
if (stats.isDirectory()) {
|
||||
if (currentDepth >= maxDepth) return 0;
|
||||
const files = await fsp.readdir(dirPath, { withFileTypes: true });
|
||||
const promises = files.map(async (file) => {
|
||||
const fullPath = path.join(dirPath, file.name);
|
||||
if (file.isSymbolicLink()) return 0;
|
||||
if (file.isDirectory()) {
|
||||
return getDirectorySize(fullPath, maxDepth, currentDepth + 1);
|
||||
}
|
||||
if (file.isFile()) {
|
||||
try {
|
||||
const fileStats = await fsp.lstat(fullPath);
|
||||
return fileStats.size;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const sizes = await Promise.all(promises);
|
||||
size = sizes.reduce((acc, curr) => acc + curr, 0);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
async function scanDirectoryChildren(dirPath) {
|
||||
if (!isPathAllowed(dirPath)) {
|
||||
throw new Error('Unzulässiger Pfad. Zugriff verweigert.');
|
||||
}
|
||||
|
||||
const files = await fsp.readdir(dirPath, { withFileTypes: true });
|
||||
const results = [];
|
||||
|
||||
const promises = files.map(async (file) => {
|
||||
const fullPath = path.join(dirPath, file.name);
|
||||
try {
|
||||
if (file.isSymbolicLink()) {
|
||||
results.push({ name: file.name, path: fullPath, isDirectory: false, size: 0, isLink: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.isDirectory()) {
|
||||
const dirSize = await getDirectorySize(fullPath, 2);
|
||||
results.push({ name: file.name, path: fullPath, isDirectory: true, size: dirSize });
|
||||
} else {
|
||||
const stats = await fsp.lstat(fullPath);
|
||||
results.push({ name: file.name, path: fullPath, isDirectory: false, size: stats.size });
|
||||
}
|
||||
} catch {
|
||||
results.push({ name: file.name, path: fullPath, isDirectory: file.isDirectory(), size: 0, error: true });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
results.sort((a, b) => b.size - a.size);
|
||||
return results;
|
||||
}
|
||||
|
||||
function inspectService(urlStr) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const parsed = new URL(urlStr);
|
||||
const client = parsed.protocol === 'https:' ? https : http;
|
||||
const start = Date.now();
|
||||
const req = client.get(parsed.href, {
|
||||
timeout: 4000,
|
||||
headers: { 'User-Agent': 'MischLabs-Diagnostics/1.0' }
|
||||
}, (res) => {
|
||||
const chunks = [];
|
||||
let bodyLength = 0;
|
||||
res.on('data', (chunk) => {
|
||||
if (bodyLength < 2000) {
|
||||
chunks.push(chunk);
|
||||
bodyLength += chunk.length;
|
||||
}
|
||||
});
|
||||
res.on('end', () => {
|
||||
const duration = Date.now() - start;
|
||||
const bodyPreview = Buffer.concat(chunks).toString('utf8').slice(0, 1000);
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 400,
|
||||
statusCode: res.statusCode,
|
||||
statusMessage: res.statusMessage,
|
||||
latency: duration,
|
||||
headers: res.headers,
|
||||
bodyPreview: bodyPreview
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve({ ok: false, error: 'Timeout nach 4 Sekunden', latency: 4000 });
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
resolve({ ok: false, error: err.message, latency: Date.now() - start });
|
||||
});
|
||||
} catch (err) {
|
||||
resolve({ ok: false, error: err.message, latency: 0 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseWatchtower(lines) {
|
||||
const sessions = [];
|
||||
let current = null;
|
||||
@@ -828,6 +949,70 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerLogsMatch = req.url?.match(/^\/api\/containers\/([^/?#]+)\/logs(?:[?#].*)?$/);
|
||||
if (containerLogsMatch) {
|
||||
if (req.method !== 'GET') {
|
||||
json(res, 405, { error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAdminRequest(req);
|
||||
const containerName = decodeURIComponent(containerLogsMatch[1]);
|
||||
const urlParams = new URL(req.url, 'http://localhost').searchParams;
|
||||
const tail = Number(urlParams.get('tail') || 150);
|
||||
const logs = await dockerLogs(containerName, tail);
|
||||
json(res, 200, { name: containerName, logs });
|
||||
} catch (error) {
|
||||
json(res, error.statusCode || 500, { error: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/api/services/inspect')) {
|
||||
if (req.method !== 'GET') {
|
||||
json(res, 405, { error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAdminRequest(req);
|
||||
const urlParams = new URL(req.url, 'http://localhost').searchParams;
|
||||
const inspectUrl = urlParams.get('url');
|
||||
if (!inspectUrl) {
|
||||
json(res, 400, { error: 'Missing url parameter' });
|
||||
return;
|
||||
}
|
||||
const data = await inspectService(inspectUrl);
|
||||
json(res, 200, data);
|
||||
} catch (error) {
|
||||
json(res, error.statusCode || 500, { error: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/api/disks/scan-folder')) {
|
||||
if (req.method !== 'GET') {
|
||||
json(res, 405, { error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAdminRequest(req);
|
||||
const urlParams = new URL(req.url, 'http://localhost').searchParams;
|
||||
const scanPath = urlParams.get('path');
|
||||
if (!scanPath) {
|
||||
json(res, 400, { error: 'Missing path parameter' });
|
||||
return;
|
||||
}
|
||||
const data = await scanDirectoryChildren(scanPath);
|
||||
json(res, 200, { path: scanPath, children: data });
|
||||
} catch (error) {
|
||||
json(res, error.statusCode || 500, { error: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await serveStatic(req, res);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user