99 lines
4.0 KiB
JavaScript
99 lines
4.0 KiB
JavaScript
import { createReadStream, existsSync, mkdirSync, statSync } from "node:fs";
|
|
import { appendFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { createServer } from "node:http";
|
|
import { extname, join, normalize } from "node:path";
|
|
|
|
const packagedRoot = join(process.cwd(), "public");
|
|
const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out");
|
|
const port = Number(process.env.APP_PORT || 3200);
|
|
const dataDirectory = process.env.DATA_DIR || join(process.cwd(), "data");
|
|
const usageLogPath = join(dataDirectory, "usage.jsonl");
|
|
mkdirSync(dataDirectory, { recursive: true });
|
|
const mimeTypes = {
|
|
".css": "text/css; charset=utf-8",
|
|
".html": "text/html; charset=utf-8",
|
|
".ico": "image/x-icon",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".png": "image/png",
|
|
".rsc": "text/x-component",
|
|
".svg": "image/svg+xml",
|
|
".webp": "image/webp",
|
|
};
|
|
|
|
function sendJson(response, status, value) {
|
|
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
response.end(JSON.stringify(value));
|
|
}
|
|
|
|
function writeUsageLog(request, response) {
|
|
let body = "";
|
|
request.setEncoding("utf8");
|
|
request.on("data", (chunk) => {
|
|
body += chunk;
|
|
if (body.length > 16_384) request.destroy();
|
|
});
|
|
request.on("end", async () => {
|
|
try {
|
|
const input = JSON.parse(body || "{}");
|
|
if (!['page_view', 'conversion'].includes(input.event)) {
|
|
sendJson(response, 400, { error: "Invalid event" });
|
|
return;
|
|
}
|
|
const entry = {
|
|
timestamp: new Date().toISOString(),
|
|
event: input.event,
|
|
client_hash: request.headers["x-forwarded-for"] || request.socket.remoteAddress
|
|
? createHash("sha256").update(String(request.headers["x-forwarded-for"] || request.socket.remoteAddress)).digest("hex").slice(0, 16)
|
|
: undefined,
|
|
...(input.event === "conversion" ? {
|
|
width: Math.max(8, Math.min(256, Number(input.width) || 0)),
|
|
height: Math.max(8, Math.min(256, Number(input.height) || 0)),
|
|
sampling_strategy: input.sampling_strategy === "dominant" ? "dominant" : "smooth",
|
|
color_limit: Math.max(2, Math.min(64, Number(input.color_limit) || 0)),
|
|
actual_colors: Math.max(0, Math.min(221, Number(input.actual_colors) || 0)),
|
|
bead_count: Math.max(0, Math.min(65_536, Number(input.bead_count) || 0)),
|
|
transparent_cells: Math.max(0, Math.min(65_536, Number(input.transparent_cells) || 0)),
|
|
} : {}),
|
|
};
|
|
await appendFile(usageLogPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
sendJson(response, 202, { ok: true });
|
|
} catch {
|
|
sendJson(response, 400, { error: "Invalid request" });
|
|
}
|
|
});
|
|
}
|
|
|
|
createServer((request, response) => {
|
|
const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname);
|
|
if (pathname === "/api/usage" && request.method === "POST") {
|
|
writeUsageLog(request, response);
|
|
return;
|
|
}
|
|
if (pathname.startsWith("/api/")) {
|
|
sendJson(response, 404, { error: "Not found" });
|
|
return;
|
|
}
|
|
const relativePath = normalize(pathname).replace(/^([/\\])+/, "");
|
|
let filePath = join(root, relativePath || "index.html");
|
|
|
|
if (!filePath.startsWith(root) || !existsSync(filePath)) filePath = join(root, "index.html");
|
|
if (existsSync(filePath) && statSync(filePath).isDirectory()) filePath = join(filePath, "index.html");
|
|
|
|
if (!existsSync(filePath)) {
|
|
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
response.end("Not found");
|
|
return;
|
|
}
|
|
|
|
response.writeHead(200, {
|
|
"Content-Type": mimeTypes[extname(filePath)] || "application/octet-stream",
|
|
"Cache-Control": filePath.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
|
|
});
|
|
if (request.method === "HEAD") response.end();
|
|
else createReadStream(filePath).pipe(response);
|
|
}).listen(port, "0.0.0.0", () => {
|
|
console.log(`Pixel bead pattern web listening on ${port}`);
|
|
});
|