Align admin shell with launcher UI

This commit is contained in:
dcconstructions
2026-06-26 01:13:41 +03:00
parent 3e789d6887
commit cc45b1c445
5 changed files with 1115 additions and 253 deletions
+112 -1
View File
@@ -1,5 +1,5 @@
import { createServer } from "node:http";
import { readFile, stat, writeFile } from "node:fs/promises";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
@@ -9,6 +9,7 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const port = Number(process.env.PORT || 8090);
const host = process.env.HOST || "127.0.0.1";
const pagePath = join(repoRoot, "content", "pages", "home.json");
const uploadRoot = join(repoRoot, "assets", "uploads");
const MIME = {
".html": "text/html; charset=utf-8",
@@ -19,10 +20,19 @@ const MIME = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".avif": "image/avif",
".webp": "image/webp",
".mp4": "video/mp4",
".webm": "video/webm",
".mov": "video/quicktime",
".m4v": "video/x-m4v",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".glb": "model/gltf-binary",
".gltf": "model/gltf+json",
".woff": "font/woff",
".woff2": "font/woff2",
};
function send(res, status, body, contentType = "text/plain; charset=utf-8") {
@@ -71,6 +81,100 @@ function runNodeScript(scriptName) {
});
}
function isUploadPayload(payload) {
return (
payload &&
typeof payload.fileName === "string" &&
typeof payload.dataUrl === "string" &&
(!payload.mimeType || typeof payload.mimeType === "string") &&
(!payload.bucket || typeof payload.bucket === "string")
);
}
function sanitizePathSegment(value) {
const safe = value
.normalize("NFKD")
.replace(/[^\w.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.toLowerCase();
return safe || "media";
}
function sanitizeUploadBucket(value) {
const segments = String(value || "media")
.split(/[\\/]+/)
.map((segment) => sanitizePathSegment(segment))
.filter(Boolean)
.slice(0, 4);
return segments.length ? segments.join("/") : "media";
}
function extensionFromMimeType(mimeType) {
const map = {
"image/svg+xml": ".svg",
"image/png": ".png",
"image/jpeg": ".jpg",
"image/gif": ".gif",
"image/avif": ".avif",
"image/webp": ".webp",
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/quicktime": ".mov",
"model/gltf-binary": ".glb",
"model/gltf+json": ".gltf",
"font/woff": ".woff",
"font/woff2": ".woff2",
};
return map[mimeType] || "";
}
function buildStoredFileName(fileName, mimeType) {
const extension = (extname(fileName) || extensionFromMimeType(mimeType) || ".bin").toLowerCase();
const base = fileName.slice(0, extension ? -extension.length : undefined);
const safeBase =
base
.normalize("NFKD")
.replace(/[^\w.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.toLowerCase()
.slice(0, 72) || "asset";
return `${safeBase}-${Date.now().toString(36)}${extension}`;
}
async function saveUploadedAsset(payload) {
if (!isUploadPayload(payload)) {
throw new Error("Некорректный payload загрузки");
}
const match = /^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/s.exec(payload.dataUrl);
if (!match) {
throw new Error("Файл должен прийти data-url с base64");
}
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
const bucket = sanitizeUploadBucket(payload.bucket || "media");
const storedName = buildStoredFileName(payload.fileName, mimeType);
const fileBuffer = Buffer.from(match[2], "base64");
const directory = join(uploadRoot, ...bucket.split("/"));
await mkdir(directory, { recursive: true });
await writeFile(join(directory, storedName), fileBuffer);
return {
ok: true,
url: `./assets/uploads/${bucket}/${storedName}`,
fileName: storedName,
originalFileName: payload.fileName,
mimeType,
bucket,
};
}
function safePublicPath(urlPath) {
const withoutQuery = decodeURIComponent(urlPath.split("?")[0]);
const requestPath =
@@ -133,6 +237,13 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
const body = await readBody(req);
const result = await saveUploadedAsset(JSON.parse(body));
sendJson(res, 200, result);
return;
}
if (req.method === "POST" && url.pathname === "/api/extract/home") {
const result = await runNodeScript("extract-home-blocks.mjs");
sendJson(res, 200, { ok: true, ...result });