import { Elysia, t } from "elysia"; import { join } from "node:path"; import type { Document, QueryListOption } from "dbtype"; import type { DocumentAccessor } from "../model/doc.ts"; import { AdminOnly, createPermissionCheck, Permission as Per } from "../permission/permission.ts"; import { sendError } from "./error_handler.ts"; import { oshash } from "src/util/oshash.ts"; import { headComicPage, renderComicPage } from "./comic.ts"; export const getContentRouter = (controller: DocumentAccessor) => { return new Elysia({ name: "content-router", prefix: "/doc", }) .get("/search", async ({ query }) => { const limit = Math.min(Number(query.limit ?? 20), 100); const option: QueryListOption = { limit: limit, allow_tag: query.allow_tag?.split(",") ?? [], word: query.word, cursor: query.cursor, eager_loading: true, offset: Number(query.offset), use_offset: query.use_offset === 'true', content_type: query.content_type, }; return await controller.findList(option); }, { beforeHandle: createPermissionCheck(Per.QueryContent), query: t.Object({ limit: t.Optional(t.String()), cursor: t.Optional(t.Number()), word: t.Optional(t.String()), content_type: t.Optional(t.String()), offset: t.Optional(t.Number()), use_offset: t.Optional(t.String()), allow_tag: t.Optional(t.String()), }) }) .get("/_gid", async ({ query }) => { const gid_list = query.gid.split(",").map(x => Number.parseInt(x)); if (gid_list.some(x => Number.isNaN(x)) || gid_list.length > 100) { throw sendError(400, "Invalid GID list"); } return await controller.findByGidList(gid_list); }, { beforeHandle: createPermissionCheck(Per.QueryContent), query: t.Object({ gid: t.String() }) }) .get("/:num", async ({ params: { num } }) => { const document = await controller.findById(num, true); if (document === undefined) { throw sendError(404, "document does not exist."); } return document; }, { beforeHandle: createPermissionCheck(Per.QueryContent), params: t.Object({ num: t.Numeric() }) }) .post("/:num", async ({ params: { num }, body }) => { const content_desc: Partial & { id: number } = { id: num, ...body, }; return await controller.update(content_desc); }, { beforeHandle: AdminOnly, params: t.Object({ num: t.Numeric() }), body: t.Object({}, { additionalProperties: true }) }) .delete("/:num", async ({ params: { num } }) => { return await controller.del(num); }, { beforeHandle: AdminOnly, params: t.Object({ num: t.Numeric() }) }) .get("/:num/similars", async ({ params: { num } }) => { const doc = await controller.findById(num, true); if (doc === undefined) { throw sendError(404); } return await controller.getSimilarDocument(doc); }, { beforeHandle: createPermissionCheck(Per.QueryContent), params: t.Object({ num: t.Numeric() }) }) .get("/:num/tags", async ({ params: { num } }) => { const document = await controller.findById(num, true); if (document === undefined) { throw sendError(404, "document does not exist."); } return document.tags; }, { beforeHandle: createPermissionCheck(Per.QueryContent), params: t.Object({ num: t.Numeric() }) }) .post("/:num/tags/:tag", async ({ params: { num, tag } }) => { const doc = await controller.findById(num); if (doc === undefined) { throw sendError(404); } return await controller.addTag(doc, tag); }, { beforeHandle: createPermissionCheck(Per.ModifyTag), params: t.Object({ num: t.Numeric(), tag: t.String() }) }) .delete("/:num/tags/:tag", async ({ params: { num, tag } }) => { const doc = await controller.findById(num); if (doc === undefined) { throw sendError(404); } return await controller.delTag(doc, tag); }, { beforeHandle: createPermissionCheck(Per.ModifyTag), params: t.Object({ num: t.Numeric(), tag: t.String() }) }) .post("/:num/_rehash", async ({ params: { num } }) => { const doc = await controller.findById(num); if (doc === undefined || doc.deleted_at !== null) { throw sendError(404); } const filepath = join(doc.basepath, doc.filename); try { const new_hash = (await oshash(filepath)).toString(); return await controller.update({ id: num, content_hash: new_hash }); } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") { throw sendError(404, "file not found"); } throw e; } }, { beforeHandle: AdminOnly, params: t.Object({ num: t.Numeric() }) }) .post("/:num/_rescan", async ({ params: { num }, set }) => { const doc = await controller.findById(num, true); if (doc === undefined) { throw sendError(404); } await controller.rescanDocument(doc); set.status = 204; // No Content }, { beforeHandle: AdminOnly, params: t.Object({ num: t.Numeric() }) }) .group("/:num", (app) => app .derive(async ({ params: { num } }) => { const docId = typeof num === "number" ? num : Number.parseInt(String(num)); if (Number.isNaN(docId)) { throw sendError(400, "invalid document id"); } const document = await controller.findById(docId, true); if (document === undefined) { throw sendError(404, "document does not exist."); } 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 }) => { if (document.content_type !== "comic") { throw sendError(404); } const path = join(document.basepath, document.filename); const body = await renderComicPage({ path, page: 0, reqHeaders: request.headers, set, }); return body ?? undefined; }, { beforeHandle: createPermissionCheck(Per.QueryContent), 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 }) => { if (document.content_type !== "comic") { throw sendError(404); } const pageIndex = page; const path = join(document.basepath, document.filename); const body = await renderComicPage({ path, page: pageIndex, reqHeaders: request.headers, set, }); return body ?? undefined; }, { beforeHandle: createPermissionCheck(Per.QueryContent), params: t.Object({ num: t.Numeric(), page: t.Numeric() }), }) ); }; export default getContentRouter;