177 lines
6.3 KiB
JavaScript
177 lines
6.3 KiB
JavaScript
// server/RobotConfigService.js
|
|
//
|
|
// Self-contained Express-Modul für die robot.json-Verwaltung.
|
|
// Einbinden mit einer Zeile:
|
|
//
|
|
// const robotConfigService = require('./RobotConfigService');
|
|
// robotConfigService.register(app, { apiKey: process.env.ROBOT_API_KEY });
|
|
//
|
|
// Routen:
|
|
// GET /api/robot → aktuelle robot.json
|
|
// PUT /api/robot → überschreibt robot.json, legt Snapshot an (Auth)
|
|
// GET /api/robot/history → Liste aller Snapshots
|
|
// GET /api/robot/history/:ts → einen bestimmten Snapshot abrufen
|
|
|
|
'use strict';
|
|
|
|
const express = require('express');
|
|
const fs = require('fs');
|
|
const fsp = require('fs/promises');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const DATA_DIR = path.join(__dirname, '..', 'data', 'robot');
|
|
const ROBOT_JSON = path.join(DATA_DIR, 'robot.json');
|
|
const KEY_FILE = path.join(DATA_DIR, '.apikey');
|
|
|
|
// ── Timestamp ────────────────────────────────────────────────────────────────
|
|
|
|
function makeTimestamp() {
|
|
const now = new Date();
|
|
const p = (n, w = 2) => String(n).padStart(w, '0');
|
|
return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
}
|
|
|
|
// ── API-Key-Auflösung ────────────────────────────────────────────────────────
|
|
|
|
function resolveApiKey(provided) {
|
|
if (provided) return provided;
|
|
try {
|
|
return fs.readFileSync(KEY_FILE, 'utf8').trim();
|
|
} catch {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
const key = crypto.randomBytes(24).toString('hex');
|
|
fs.writeFileSync(KEY_FILE, key, 'utf8');
|
|
console.log(`[RobotConfigService] Kein ROBOT_API_KEY gesetzt — neuer Key generiert: ${key}`);
|
|
console.log(`[RobotConfigService] Gespeichert in: ${KEY_FILE}`);
|
|
return key;
|
|
}
|
|
}
|
|
|
|
// ── Datei-Operationen ────────────────────────────────────────────────────────
|
|
|
|
async function readRobotJson() {
|
|
const raw = await fsp.readFile(ROBOT_JSON, 'utf8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
async function writeRobotJson(data) {
|
|
await fsp.mkdir(DATA_DIR, { recursive: true });
|
|
const ts = makeTimestamp();
|
|
const snapshotFile = `robot_${ts}.json`;
|
|
|
|
try {
|
|
const current = await fsp.readFile(ROBOT_JSON, 'utf8');
|
|
await fsp.writeFile(path.join(DATA_DIR, snapshotFile), current, 'utf8');
|
|
} catch {
|
|
// Noch kein robot.json vorhanden — kein Snapshot nötig
|
|
}
|
|
|
|
await fsp.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8');
|
|
await pruneSnapshots();
|
|
return { snapshotFile };
|
|
}
|
|
|
|
async function listHistory() {
|
|
try {
|
|
const files = await fsp.readdir(DATA_DIR);
|
|
return files
|
|
.filter(f => /^robot_\d{8}_\d{6}\.json$/.test(f))
|
|
.sort()
|
|
.reverse()
|
|
.map(f => ({ filename: f, day: f.slice(6, 14), timestamp: f.slice(15, 21) }));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function readSnapshot(ts) {
|
|
const raw = await fsp.readFile(path.join(DATA_DIR, `robot_${ts}.json`), 'utf8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
// ── Pruning ──────────────────────────────────────────────────────────────────
|
|
//
|
|
// Regel: Pro Tag maximal 100 Snapshots.
|
|
// Hat ein Tag mehr als 100, wird nur der neueste behalten.
|
|
|
|
async function pruneSnapshots() {
|
|
let files;
|
|
try { files = await fsp.readdir(DATA_DIR); } catch { return; }
|
|
|
|
const byDay = {};
|
|
for (const f of files) {
|
|
if (!/^robot_\d{8}_\d{6}\.json$/.test(f)) continue;
|
|
const day = f.slice(6, 14);
|
|
if (!byDay[day]) byDay[day] = [];
|
|
byDay[day].push(f);
|
|
}
|
|
|
|
for (const [day, dayFiles] of Object.entries(byDay)) {
|
|
if (dayFiles.length <= 100) continue;
|
|
dayFiles.sort(); // älteste zuerst
|
|
const toDelete = dayFiles.slice(0, -1); // neueste behalten
|
|
for (const f of toDelete) {
|
|
try { await fsp.unlink(path.join(DATA_DIR, f)); } catch { /* ignorieren */ }
|
|
}
|
|
console.log(`[RobotConfigService] Tag ${day}: ${toDelete.length} Snapshots bereinigt, behalten: ${dayFiles[dayFiles.length - 1]}`);
|
|
}
|
|
}
|
|
|
|
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
|
|
function isAuthorized(req, key) {
|
|
return (req.headers['authorization'] ?? '') === `Bearer ${key}`;
|
|
}
|
|
|
|
// ── Registrierung ─────────────────────────────────────────────────────────────
|
|
|
|
function register(app, options = {}) {
|
|
const apiKey = resolveApiKey(options.apiKey);
|
|
const router = express.Router();
|
|
router.use(express.json({ limit: '5mb' }));
|
|
|
|
router.get('/api/robot/history', async (req, res) => {
|
|
try {
|
|
return res.json({ history: await listHistory() });
|
|
} catch (err) {
|
|
return res.status(500).json({ error: String(err) });
|
|
}
|
|
});
|
|
|
|
router.get('/api/robot/history/:ts', async (req, res) => {
|
|
try {
|
|
return res.json(await readSnapshot(req.params.ts));
|
|
} catch {
|
|
return res.status(404).json({ error: 'Snapshot nicht gefunden' });
|
|
}
|
|
});
|
|
|
|
router.get('/api/robot', async (req, res) => {
|
|
try {
|
|
return res.json(await readRobotJson());
|
|
} catch {
|
|
return res.status(404).json({ error: 'robot.json nicht gefunden' });
|
|
}
|
|
});
|
|
|
|
router.put('/api/robot', async (req, res) => {
|
|
if (!isAuthorized(req, apiKey)) {
|
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
}
|
|
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
return res.status(400).json({ error: 'Body muss ein JSON-Objekt sein' });
|
|
}
|
|
try {
|
|
const result = await writeRobotJson(req.body);
|
|
return res.json({ ok: true, ...result });
|
|
} catch (err) {
|
|
return res.status(500).json({ error: String(err) });
|
|
}
|
|
});
|
|
|
|
app.use(router);
|
|
}
|
|
|
|
module.exports = { register, readRobotJson, writeRobotJson, listHistory, readSnapshot };
|