Files
2026-06-18 10:27:16 +02:00

146 lines
5.0 KiB
JavaScript

import { createServer as createHttpServer } from "node:http";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const projectDir = dirname(fileURLToPath(import.meta.url));
function sendJson(response, statusCode, value) {
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(JSON.stringify(value));
}
function sendText(response, statusCode, text, contentType = "text/plain; charset=utf-8") {
response.writeHead(statusCode, {
"content-type": contentType,
"cache-control": "no-store"
});
response.end(text);
}
async function readRequestBody(request, limitBytes = 5 * 1024 * 1024) {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > limitBytes) {
throw new Error("Payload zu gross");
}
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function validateMapData(value) {
if (!value || typeof value !== "object") {
throw new Error("Karte muss ein Objekt sein");
}
if (!Array.isArray(value.systems) || !Array.isArray(value.connections)) {
throw new Error("systems und connections muessen Arrays sein");
}
const ids = new Set();
for (const system of value.systems) {
if (!system || typeof system !== "object") {
throw new Error("Ungueltiges System");
}
if (typeof system.id !== "string" || typeof system.name !== "string") {
throw new Error("System braucht id und name");
}
if (!Number.isFinite(system.x) || !Number.isFinite(system.y)) {
throw new Error("System braucht x/y Koordinaten");
}
ids.add(system.id);
}
for (const connection of value.connections) {
if (!connection || typeof connection !== "object") {
throw new Error("Ungueltige Verbindung");
}
if (typeof connection.id !== "string" || typeof connection.from !== "string" || typeof connection.to !== "string") {
throw new Error("Verbindung braucht id/from/to");
}
if (!ids.has(connection.from) || !ids.has(connection.to)) {
throw new Error("Verbindung verweist auf unbekanntes System");
}
}
return {
source: typeof value.source === "string" ? value.source : undefined,
background: typeof value.background === "string" ? value.background : "#05080f",
grid: value.grid && typeof value.grid === "object" ? value.grid : undefined,
viewBox: value.viewBox && typeof value.viewBox === "object" ? value.viewBox : undefined,
systems: value.systems,
connections: value.connections
};
}
async function readInitialMap(htmlPath) {
const html = await readFile(htmlPath, "utf8");
const match = html.match(/<script type="application\/json" id="initial-map-data">([\s\S]*?)<\/script>/);
if (!match) {
throw new Error("initial-map-data nicht gefunden");
}
return validateMapData(JSON.parse(match[1]));
}
export function createMapServer(options = {}) {
const htmlPath = options.htmlPath || join(projectDir, "star-map.html");
const dataDir = options.dataDir || process.env.DATA_DIR || join(projectDir, "data");
const dataPath = join(dataDir, "map.json");
return createHttpServer(async (request, response) => {
try {
const url = new URL(request.url, "http://localhost");
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/star-map.html")) {
const html = await readFile(htmlPath, "utf8");
sendText(response, 200, html, "text/html; charset=utf-8");
return;
}
if (url.pathname === "/api/map" && request.method === "GET") {
if (existsSync(dataPath)) {
sendJson(response, 200, validateMapData(JSON.parse(await readFile(dataPath, "utf8"))));
return;
}
sendJson(response, 200, await readInitialMap(htmlPath));
return;
}
if (url.pathname === "/api/map" && request.method === "PUT") {
const body = await readRequestBody(request);
const mapData = validateMapData(JSON.parse(body));
await mkdir(dataDir, { recursive: true });
const tempPath = `${dataPath}.tmp`;
await writeFile(tempPath, `${JSON.stringify(mapData, null, 2)}\n`, "utf8");
await rename(tempPath, dataPath);
sendJson(response, 200, { ok: true });
return;
}
sendJson(response, 404, { error: "Nicht gefunden" });
} catch (error) {
const status = error instanceof SyntaxError || /ungueltig|braucht|muss|verweist|Payload|systems/i.test(error.message)
? 400
: 500;
sendJson(response, status, { error: error.message });
}
});
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || "127.0.0.1";
const server = createMapServer();
server.listen(port, host, () => {
console.log(`Star Map Editor: http://${host}:${port}`);
});
}