104 lines
2.6 KiB
TypeScript
104 lines
2.6 KiB
TypeScript
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<ElysiaContext["set"], "status" | "headers">;
|
|
|
|
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<string, string | number>);
|
|
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;
|
|
}
|
|
}
|