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");
|
return null;
|
||||||
|
|
||||||
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 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) {
|
||||||
|
|
@ -82,7 +106,7 @@ export async function renderComicPage({ path, page, reqHeaders, set }: RenderOpt
|
||||||
await zip.reader.close();
|
await zip.reader.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streamWithCleanup
|
return streamWithCleanup
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await zip.reader.close();
|
await zip.reader.close();
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ 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 }) => {
|
||||||
const limit = Math.min(Number(query.limit ?? 20), 100);
|
const limit = Math.min(Number(query.limit ?? 20), 100);
|
||||||
const option: QueryListOption = {
|
const option: QueryListOption = {
|
||||||
|
|
@ -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