feat: comic 페이지에 대한 헤더 설정 기능 추가 및 라우터 개선
This commit is contained in:
		
							parent
							
								
									e2c451c708
								
							
						
					
					
						commit
						fe5ed4c4aa
					
				
					 2 changed files with 83 additions and 27 deletions
				
			
		| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import type { Context as ElysiaContext } from "elysia";
 | 
					import type { Context as ElysiaContext } from "elysia";
 | 
				
			||||||
import { Readable } from "node:stream";
 | 
					import { Readable } from "node:stream";
 | 
				
			||||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
 | 
					import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
 | 
				
			||||||
 | 
					import { Entry } from "@zip.js/zip.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const imageExtensions = new Set(["gif", "png", "jpeg", "bmp", "webp", "jpg", "avif"]);
 | 
					const imageExtensions = new Set(["gif", "png", "jpeg", "bmp", "webp", "jpg", "avif"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +19,50 @@ type RenderOptions = {
 | 
				
			||||||
	set: ResponseSet;
 | 
						set: ResponseSet;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function setHeadersForEntry(entry: Entry, reqHeaders: Headers, set: ResponseSet) {
 | 
				
			||||||
 | 
						const ext = entry.filename.split(".").pop()?.toLowerCase() ?? "jpeg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						set.headers["content-type"] = extensionToMime(ext);
 | 
				
			||||||
 | 
						if (typeof entry.uncompressedSize === "number") {
 | 
				
			||||||
 | 
							set.headers["content-length"] = entry.uncompressedSize;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const lastModified = entry.lastModDate ?? new Date();
 | 
				
			||||||
 | 
						const ifModifiedSince = reqHeaders.get("if-modified-since");
 | 
				
			||||||
 | 
						set.headers["date"] = new Date().toUTCString();
 | 
				
			||||||
 | 
						set.headers["last-modified"] = lastModified.toUTCString();
 | 
				
			||||||
 | 
						if (ifModifiedSince) {
 | 
				
			||||||
 | 
							const cachedDate = new Date(ifModifiedSince);
 | 
				
			||||||
 | 
							if (!Number.isNaN(cachedDate.valueOf()) && lastModified <= cachedDate) {
 | 
				
			||||||
 | 
								set.status = 304;
 | 
				
			||||||
 | 
								// client's cache is valid
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						set.status = 200;
 | 
				
			||||||
 | 
						return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function headComicPage({ 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;
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const entry = entries[page];
 | 
				
			||||||
 | 
							if (await setHeadersForEntry(entry, reqHeaders, set)) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} finally {
 | 
				
			||||||
 | 
							await zip.reader.close();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function renderComicPage({ path, page, reqHeaders, set }: RenderOptions) {
 | 
					export async function renderComicPage({ path, page, reqHeaders, set }: RenderOptions) {
 | 
				
			||||||
	const zip = await readZip(path);
 | 
						const zip = await readZip(path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,37 +74,16 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (page < 0 || page >= entries.length) {
 | 
							if (page < 0 || page >= entries.length) {
 | 
				
			||||||
			set.status = 404;
 | 
								set.status = 404;
 | 
				
			||||||
			await zip.reader.close();
 | 
					 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const entry = entries[page];
 | 
							const entry = entries[page];
 | 
				
			||||||
		const lastModified = entry.lastModDate ?? new Date();
 | 
							if (await setHeadersForEntry(entry, reqHeaders, set)) {
 | 
				
			||||||
		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;
 | 
								return null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const readStream = await createReadableStreamFromZip(zip.reader, entry);
 | 
							const readStream = await createReadableStreamFromZip(zip.reader, entry);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		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;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// Ensure zip file is closed after stream ends
 | 
							// Ensure zip file is closed after stream ends
 | 
				
			||||||
		const streamWithCleanup = new ReadableStream({
 | 
							const streamWithCleanup = new ReadableStream({
 | 
				
			||||||
			async start(controller) {
 | 
								async start(controller) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,10 +5,11 @@ import type { DocumentAccessor } from "../model/doc.ts";
 | 
				
			||||||
import { AdminOnly, createPermissionCheck, Permission as Per } from "../permission/permission.ts";
 | 
					import { AdminOnly, createPermissionCheck, Permission as Per } from "../permission/permission.ts";
 | 
				
			||||||
import { sendError } from "./error_handler.ts";
 | 
					import { sendError } from "./error_handler.ts";
 | 
				
			||||||
import { oshash } from "src/util/oshash.ts";
 | 
					import { oshash } from "src/util/oshash.ts";
 | 
				
			||||||
import { renderComicPage } from "./comic.ts";
 | 
					import { headComicPage, renderComicPage } from "./comic.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getContentRouter = (controller: DocumentAccessor) => {
 | 
					export const getContentRouter = (controller: DocumentAccessor) => {
 | 
				
			||||||
    return new Elysia({ name: "content-router",
 | 
					    return new Elysia({
 | 
				
			||||||
 | 
					        name: "content-router",
 | 
				
			||||||
        prefix: "/doc",
 | 
					        prefix: "/doc",
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
        .get("/search", async ({ query }) => {
 | 
					        .get("/search", async ({ query }) => {
 | 
				
			||||||
| 
						 | 
					@ -156,6 +157,21 @@ export const getContentRouter = (controller: DocumentAccessor) => {
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    return { document, docId };
 | 
					                    return { document, docId };
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
 | 
					                .head("/comic/thumbnail", async ({ document, request, set }) => {
 | 
				
			||||||
 | 
					                    if (document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                        throw sendError(404);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    const path = join(document.basepath, document.filename);
 | 
				
			||||||
 | 
					                    await headComicPage({
 | 
				
			||||||
 | 
					                        path,
 | 
				
			||||||
 | 
					                        page: 0,
 | 
				
			||||||
 | 
					                        reqHeaders: request.headers,
 | 
				
			||||||
 | 
					                        set,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					                    params: t.Object({ num: t.Numeric() }),
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
                .get("/comic/thumbnail", async ({ document, request, set }) => {
 | 
					                .get("/comic/thumbnail", async ({ document, request, set }) => {
 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					                    if (document.content_type !== "comic") {
 | 
				
			||||||
                        throw sendError(404);
 | 
					                        throw sendError(404);
 | 
				
			||||||
| 
						 | 
					@ -172,6 +188,22 @@ export const getContentRouter = (controller: DocumentAccessor) => {
 | 
				
			||||||
                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
                    params: t.Object({ num: t.Numeric() }),
 | 
					                    params: t.Object({ num: t.Numeric() }),
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
 | 
					                .head("/comic/:page", async ({ document, params: { page }, request, set }) => {
 | 
				
			||||||
 | 
					                    if (document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                        throw sendError(404);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    const pageIndex = page;
 | 
				
			||||||
 | 
					                    const path = join(document.basepath, document.filename);
 | 
				
			||||||
 | 
					                    await headComicPage({
 | 
				
			||||||
 | 
					                        path,
 | 
				
			||||||
 | 
					                        page: pageIndex,
 | 
				
			||||||
 | 
					                        reqHeaders: request.headers,
 | 
				
			||||||
 | 
					                        set,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					                    params: t.Object({ num: t.Numeric(), page: t.Numeric() }),
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
                .get("/comic/:page", async ({ document, params: { page }, request, set }) => {
 | 
					                .get("/comic/:page", async ({ document, params: { page }, request, set }) => {
 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					                    if (document.content_type !== "comic") {
 | 
				
			||||||
                        throw sendError(404);
 | 
					                        throw sendError(404);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue