132 lines
3.4 KiB
TypeScript
132 lines
3.4 KiB
TypeScript
import type { Context } from "koa";
|
|
import Router from "koa-router";
|
|
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap";
|
|
import type { ContentContext } from "./context";
|
|
import { since_last_modified } from "./util";
|
|
import type { ZipReader } from "@zip.js/zip.js";
|
|
import type { FileHandle } from "node:fs/promises";
|
|
import { Readable } from "node:stream";
|
|
|
|
/**
|
|
* zip stream cache.
|
|
*/
|
|
const ZipStreamCache = new Map<string, {
|
|
reader: ZipReader<FileHandle>,
|
|
handle: FileHandle,
|
|
refCount: number,
|
|
}>();
|
|
|
|
|
|
function markUseZip(path: string) {
|
|
const ret = ZipStreamCache.get(path);
|
|
if (ret) {
|
|
ret.refCount++;
|
|
}
|
|
return ret !== undefined;
|
|
}
|
|
|
|
async function acquireZip(path: string, marked = false) {
|
|
const ret = ZipStreamCache.get(path);
|
|
if (!ret) {
|
|
const obj = await readZip(path);
|
|
const check = ZipStreamCache.get(path);
|
|
if (check) {
|
|
check.refCount++;
|
|
// if the cache is updated, release the previous one.
|
|
releaseZip(path);
|
|
return check.reader;
|
|
}
|
|
// if the cache is not updated, set the new one.
|
|
ZipStreamCache.set(path, {
|
|
reader: obj.reader,
|
|
handle: obj.handle,
|
|
refCount: 1,
|
|
});
|
|
return obj.reader;
|
|
}
|
|
if (!marked) {
|
|
ret.refCount++;
|
|
}
|
|
return ret.reader;
|
|
}
|
|
|
|
function releaseZip(path: string) {
|
|
const obj = ZipStreamCache.get(path);
|
|
if (obj === undefined) {
|
|
console.warn(`warning! duplicate release at ${path}`);
|
|
return;
|
|
}
|
|
if (obj.refCount === 1) {
|
|
const { reader, handle } = obj;
|
|
reader.close().then(() => {
|
|
handle.close();
|
|
});
|
|
ZipStreamCache.delete(path);
|
|
} else {
|
|
obj.refCount--;
|
|
}
|
|
}
|
|
|
|
async function renderZipImage(ctx: Context, path: string, page: number) {
|
|
const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
|
|
const marked = markUseZip(path);
|
|
const zip = await acquireZip(path, marked);
|
|
const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
|
|
const ext = x.filename.split(".").pop();
|
|
return ext !== undefined && image_ext.includes(ext);
|
|
});
|
|
if (0 <= page && page < entries.length) {
|
|
const entry = entries[page];
|
|
const last_modified = entry.lastModDate;
|
|
if (since_last_modified(ctx, last_modified)) {
|
|
return;
|
|
}
|
|
const read_stream = await createReadableStreamFromZip(zip, entry);
|
|
const nodeReadableStream = new Readable();
|
|
nodeReadableStream._read = () => { };
|
|
|
|
read_stream.pipeTo(new WritableStream({
|
|
write(chunk) {
|
|
nodeReadableStream.push(chunk);
|
|
},
|
|
close() {
|
|
nodeReadableStream.push(null);
|
|
},
|
|
}));
|
|
nodeReadableStream.on("error", (err) => {
|
|
console.error(err);
|
|
releaseZip(path);
|
|
});
|
|
nodeReadableStream.on("close", () => {
|
|
releaseZip(path);
|
|
});
|
|
|
|
ctx.body = nodeReadableStream;
|
|
ctx.response.length = entry.uncompressedSize;
|
|
// console.log(`${entry.name}'s ${page}:${entry.size}`);
|
|
ctx.response.type = entry.filename.split(".").pop() as string;
|
|
ctx.status = 200;
|
|
ctx.set("Date", new Date().toUTCString());
|
|
ctx.set("Last-Modified", last_modified.toUTCString());
|
|
} else {
|
|
ctx.status = 404;
|
|
}
|
|
}
|
|
|
|
export class ComicRouter extends Router<ContentContext> {
|
|
constructor() {
|
|
super();
|
|
this.get("/", async (ctx, next) => {
|
|
await renderZipImage(ctx, ctx.state.location.path, 0);
|
|
});
|
|
this.get("/:page(\\d+)", async (ctx, next) => {
|
|
const page = Number.parseInt(ctx.params.page);
|
|
await renderZipImage(ctx, ctx.state.location.path, page);
|
|
});
|
|
this.get("/thumbnail", async (ctx, next) => {
|
|
await renderZipImage(ctx, ctx.state.location.path, 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
export default ComicRouter;
|