Align admin shell with launcher UI
This commit is contained in:
+112
-1
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user