import type { Context as ElysiaContext } from "elysia"; import { Readable } from "node:stream"; import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts"; const imageExtensions = new Set(["gif", "png", "jpeg", "bmp", "webp", "jpg", "avif"]); const extensionToMime = (ext: string) => { if (ext === "jpg") return "image/jpeg"; return `image/${ext}`; }; type ResponseSet = Pick; type RenderOptions = { path: string; page: number; reqHeaders: Headers; set: ResponseSet; }; export async function renderComicPage({ path, page, reqHeaders, set }: RenderOptions) { const zip = await readZip(path); try { const entries = (await entriesByNaturalOrder(zip.reader)).filter((entry) => { const ext = entry.filename.split(".").pop()?.toLowerCase(); return ext !== undefined && imageExtensions.has(ext); }); if (page < 0 || page >= entries.length) { set.status = 404; await zip.reader.close(); return null; } const entry = entries[page]; const lastModified = entry.lastModDate ?? new Date(); const ifModifiedSince = reqHeaders.get("if-modified-since"); const headers = (set.headers ??= {} as Record); headers["Date"] = new Date().toUTCString(); headers["Last-Modified"] = lastModified.toUTCString(); if (ifModifiedSince) { const cachedDate = new Date(ifModifiedSince); if (!Number.isNaN(cachedDate.valueOf()) && lastModified <= cachedDate) { set.status = 304; await zip.reader.close(); return null; } } const readStream = await createReadableStreamFromZip(zip.reader, entry); const nodeReadable = new Readable({ read() { // noop }, }); let zipClosed = false; const closeZip = async () => { if (!zipClosed) { zipClosed = true; await zip.reader.close(); } }; readStream.pipeTo(new WritableStream({ write(chunk) { nodeReadable.push(chunk); }, close() { nodeReadable.push(null); }, abort(err) { nodeReadable.destroy(err); }, })).catch((err) => { nodeReadable.destroy(err); }); nodeReadable.on("close", () => { closeZip().catch(console.error); }); nodeReadable.on("error", () => { closeZip().catch(console.error); }); nodeReadable.on("end", () => { closeZip().catch(console.error); }); const ext = entry.filename.split(".").pop()?.toLowerCase() ?? "jpeg"; headers["Content-Type"] = extensionToMime(ext); if (typeof entry.uncompressedSize === "number") { headers["Content-Length"] = entry.uncompressedSize.toString(); } set.status = 200; return nodeReadable; } catch (error) { await zip.reader.close(); throw error; } }