91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
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;
|
|
}
|
|
}
|