feat(ui): add operator UI server, stores, and insights
This commit is contained in:
90
src/ui/http-utils.ts
Normal file
90
src/ui/http-utils.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { extname, resolve } from "node:path";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
export function sendJson(response: ServerResponse, statusCode: number, body: unknown): void {
|
||||
const payload = JSON.stringify(body);
|
||||
response.statusCode = statusCode;
|
||||
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
response.end(payload);
|
||||
}
|
||||
|
||||
export function sendText(response: ServerResponse, statusCode: number, body: string): void {
|
||||
response.statusCode = statusCode;
|
||||
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
response.end(body);
|
||||
}
|
||||
|
||||
export async function parseJsonBody<T>(request: IncomingMessage): Promise<T> {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
await new Promise<void>((resolveBody, rejectBody) => {
|
||||
request.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
request.on("end", () => resolveBody());
|
||||
request.on("error", rejectBody);
|
||||
});
|
||||
|
||||
const body = Buffer.concat(chunks).toString("utf8").trim();
|
||||
if (!body) {
|
||||
throw new Error("Request body is required.");
|
||||
}
|
||||
|
||||
return JSON.parse(body) as T;
|
||||
}
|
||||
|
||||
export function methodNotAllowed(response: ServerResponse): void {
|
||||
sendJson(response, 405, {
|
||||
ok: false,
|
||||
error: "Method not allowed.",
|
||||
});
|
||||
}
|
||||
|
||||
export function notFound(response: ServerResponse): void {
|
||||
sendJson(response, 404, {
|
||||
ok: false,
|
||||
error: "Not found.",
|
||||
});
|
||||
}
|
||||
|
||||
export async function serveStaticFile(input: {
|
||||
response: ServerResponse;
|
||||
filePath: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const absolutePath = resolve(input.filePath);
|
||||
const fileStats = await stat(absolutePath);
|
||||
if (!fileStats.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extension = extname(absolutePath).toLowerCase();
|
||||
const contentType = CONTENT_TYPES[extension] ?? "application/octet-stream";
|
||||
input.response.statusCode = 200;
|
||||
input.response.setHeader("Content-Type", contentType);
|
||||
|
||||
await new Promise<void>((resolveStream, rejectStream) => {
|
||||
const stream = createReadStream(absolutePath);
|
||||
stream.on("error", rejectStream);
|
||||
stream.on("end", () => resolveStream());
|
||||
stream.pipe(input.response);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user