import { defineConfig, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, writeFile } from "node:fs/promises"; import type { ServerResponse } from "node:http"; import { dirname, extname, join } from "node:path"; import { fileURLToPath } from "node:url"; const projectRoot = dirname(fileURLToPath(import.meta.url)); const maxStorageJsonBodyBytes = 260 * 1024 * 1024; function localStorageApiPlugin(): Plugin { return { name: "nodedc-local-storage-api", configureServer(server) { server.middlewares.use(async (req, res, next) => { const pathname = req.url?.split("?")[0]; if (req.method === "POST" && pathname === "/api/storage/upload") { try { const payload = await readJsonBody(req); const result = await saveUploadedFile(payload); sendJson(res, 200, result); } catch (error) { sendJson(res, 500, { error: error instanceof Error ? error.message : "Upload failed" }); } return; } if (req.method === "POST" && pathname === "/api/storage/data") { try { const payload = await readJsonBody(req); await saveLauncherData(payload); sendJson(res, 200, { ok: true, url: "/storage/launcher-data.json" }); } catch (error) { sendJson(res, 500, { error: error instanceof Error ? error.message : "Data save failed" }); } return; } next(); }); }, configurePreviewServer(server) { server.middlewares.use(async (req, res, next) => { const pathname = req.url?.split("?")[0]; if (req.method === "POST" && pathname === "/api/storage/upload") { try { const payload = await readJsonBody(req); const result = await saveUploadedFile(payload); sendJson(res, 200, result); } catch (error) { sendJson(res, 500, { error: error instanceof Error ? error.message : "Upload failed" }); } return; } if (req.method === "POST" && pathname === "/api/storage/data") { try { const payload = await readJsonBody(req); await saveLauncherData(payload); sendJson(res, 200, { ok: true, url: "/storage/launcher-data.json" }); } catch (error) { sendJson(res, 500, { error: error instanceof Error ? error.message : "Data save failed" }); } return; } next(); }); }, }; } async function readJsonBody(req: NodeJS.ReadableStream) { const chunks: Buffer[] = []; let totalBytes = 0; for await (const chunk of req) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); totalBytes += buffer.byteLength; if (totalBytes > maxStorageJsonBodyBytes) { throw new Error("Файл слишком большой для локального mock-storage"); } chunks.push(buffer); } return JSON.parse(Buffer.concat(chunks).toString("utf8")); } async function saveUploadedFile(payload: unknown) { 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 storedName = buildStoredFileName(payload.fileName, mimeType); const fileBuffer = Buffer.from(match[2], "base64"); await Promise.all( getWritableStorageRoots().map(async (storageRoot) => { const uploadDir = join(storageRoot, "uploads"); await mkdir(uploadDir, { recursive: true }); await writeFile(join(uploadDir, storedName), fileBuffer); }) ); return { ok: true, url: `/storage/uploads/${storedName}`, fileName: storedName, originalFileName: payload.fileName, mimeType, }; } async function saveLauncherData(payload: unknown) { await Promise.all( getWritableStorageRoots().map(async (storageRoot) => { await mkdir(storageRoot, { recursive: true }); await writeFile(join(storageRoot, "launcher-data.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8"); }) ); } function getWritableStorageRoots() { const roots = [join(projectRoot, "public", "storage")]; const distRoot = join(projectRoot, "dist"); if (existsSync(distRoot)) { roots.push(join(distRoot, "storage")); } return roots; } function buildStoredFileName(fileName: string, mimeType: string) { const extension = extname(fileName) || extensionFromMimeType(mimeType); const rawBase = fileName.slice(0, extension ? -extension.length : undefined); const safeBase = rawBase .normalize("NFKD") .replace(/[^\w.-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80) || "upload"; return `${Date.now()}-${randomUUID().slice(0, 8)}-${safeBase}${extension.toLowerCase()}`; } function extensionFromMimeType(mimeType: string) { if (mimeType === "image/jpeg") return ".jpg"; if (mimeType === "image/png") return ".png"; if (mimeType === "image/gif") return ".gif"; if (mimeType === "image/webp") return ".webp"; if (mimeType === "video/mp4") return ".mp4"; if (mimeType === "video/webm") return ".webm"; if (mimeType === "video/quicktime") return ".mov"; return ""; } function sendJson(res: ServerResponse, statusCode: number, payload: unknown) { res.statusCode = statusCode; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(payload)); } function isUploadPayload(payload: unknown): payload is { fileName: string; mimeType: string; dataUrl: string } { if (!payload || typeof payload !== "object") return false; const candidate = payload as { fileName?: unknown; mimeType?: unknown; dataUrl?: unknown }; return typeof candidate.fileName === "string" && typeof candidate.mimeType === "string" && typeof candidate.dataUrl === "string"; } export default defineConfig({ plugins: [react(), localStorageApiPlugin()], });