import type { Context, Next } from "koa"; import Router from "koa-router"; import { join } from "node:path"; import type { Document, QueryListOption, } from "dbtype"; import type { DocumentAccessor } from "../model/doc.ts"; import { AdminOnlyMiddleware as AdminOnly, createPermissionCheckMiddleware as PerCheck, Permission as Per, } from "../permission/permission.ts"; import { AllContentRouter } from "./all.ts"; import type { ContentLocation } from "./context.ts"; import { sendError } from "./error_handler.ts"; import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util.ts"; import { oshash } from "src/util/oshash.ts"; const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const document = await controller.findById(num, true); if (document === undefined) { return sendError(404, "document does not exist."); } ctx.body = document; ctx.type = "json"; }; const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const document = await controller.findById(num, true); if (document === undefined) { return sendError(404, "document does not exist."); } ctx.body = document.tags; ctx.type = "json"; }; const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const query_limit = ctx.query.limit; const query_cursor = ctx.query.cursor; const query_word = ctx.query.word; const query_content_type = ctx.query.content_type; const query_offset = ctx.query.offset; const query_use_offset = ctx.query.use_offset; if ([ query_limit, query_cursor, query_word, query_content_type, query_offset, query_use_offset, ].some((x) => Array.isArray(x))) { return sendError(400, "paramter can not be array"); } const limit = Math.min(ParseQueryNumber(query_limit) ?? 20, 100); const cursor = ParseQueryNumber(query_cursor); const word = ParseQueryArgString(query_word); const content_type = ParseQueryArgString(query_content_type); const offset = ParseQueryNumber(query_offset); if (Number.isNaN(limit) || Number.isNaN(cursor) || Number.isNaN(offset)) { return sendError(400, "parameter limit, cursor or offset is not a number"); } const allow_tag = ParseQueryArray(ctx.query.allow_tag); const [ok, use_offset] = ParseQueryBoolean(query_use_offset); if (!ok) { return sendError(400, "use_offset must be true or false."); } const option: QueryListOption = { limit: limit, allow_tag: allow_tag, word: word, cursor: cursor, eager_loading: true, offset: offset, use_offset: use_offset ?? false, content_type: content_type, }; const document = await controller.findList(option); ctx.body = document; ctx.type = "json"; }; const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); if (ctx.request.type !== "json") { return sendError(400, "update fail. invalid document type: it is not json."); } if (typeof ctx.request.body !== "object") { return sendError(400, "update fail. invalid argument: not"); } const content_desc: Partial & { id: number } = { id: num, ...ctx.request.body, }; const success = await controller.update(content_desc); ctx.body = JSON.stringify(success); ctx.type = "json"; }; const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { let tag_name = ctx.params.tag; const num = Number.parseInt(ctx.params.num); if (typeof tag_name === "undefined") { return sendError(400, "??? Unreachable"); } tag_name = String(tag_name); const c = await controller.findById(num); if (c === undefined) { return sendError(404); } const r = await controller.addTag(c, tag_name); ctx.body = JSON.stringify(r); ctx.type = "json"; }; const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { let tag_name = ctx.params.tag; const num = Number.parseInt(ctx.params.num); if (typeof tag_name === "undefined") { return sendError(400, "?? Unreachable"); } tag_name = String(tag_name); const c = await controller.findById(num); if (c === undefined) { return sendError(404); } const r = await controller.delTag(c, tag_name); ctx.body = JSON.stringify(r); ctx.type = "json"; }; const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const r = await controller.del(num); ctx.body = JSON.stringify(r); ctx.type = "json"; }; const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const document = await controller.findById(num, true); if (document === undefined) { return sendError(404, "document does not exist."); } if (document.deleted_at !== null) { return sendError(404, "document has been removed."); } const path = join(document.basepath, document.filename); ctx.state.location = { path: path, type: document.content_type, additional: document.additional, } as ContentLocation; await next(); }; function RehashContentHandler(controller: DocumentAccessor) { return async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const c = await controller.findById(num); if (c === undefined || c.deleted_at !== null) { return sendError(404); } const filepath = join(c.basepath, c.filename); let new_hash: string; try { new_hash = (await oshash(filepath)).toString(); } catch (e) { // if file is not found, return 404 if ( (e as NodeJS.ErrnoException).code === "ENOENT") { return sendError(404, "file not found"); } throw e; } const r = await controller.update({ id: num, content_hash: new_hash, }); ctx.body = JSON.stringify(r); ctx.type = "json"; }; } function getSimilarDocumentHandler(controller: DocumentAccessor) { return async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const c = await controller.findById(num, true); if (c === undefined) { return sendError(404); } const r = await controller.getSimilarDocument(c); ctx.body = r; ctx.type = "json"; }; } function getRescanDocumentHandler(controller: DocumentAccessor) { return async (ctx: Context, next: Next) => { const num = Number.parseInt(ctx.params.num); const c = await controller.findById(num, true); if (c === undefined) { return sendError(404); } await controller.rescanDocument(c); // 204 No Content ctx.status = 204; }; } export const getContentRouter = (controller: DocumentAccessor) => { const ret = new Router(); ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller)); ret.get("/:num(\\d+)", PerCheck(Per.QueryContent), ContentIDHandler(controller)); ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller)); ret.post("/:num(\\d+)", AdminOnly, UpdateContentHandler(controller)); ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller)); // ret.post("/",AdminOnly,CreateContentHandler(controller)); ret.get("/:num(\\d+)/similars", PerCheck(Per.QueryContent), getSimilarDocumentHandler(controller)); ret.get("/:num(\\d+)/tags", PerCheck(Per.QueryContent), ContentTagIDHandler(controller)); ret.post("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), AddTagHandler(controller)); ret.del("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), DelTagHandler(controller)); ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), new AllContentRouter().routes()); ret.post("/:num(\\d+)/_rehash", AdminOnly, RehashContentHandler(controller)); ret.post("/:num(\\d+)/_rescan", AdminOnly, getRescanDocumentHandler(controller)); return ret; }; export default getContentRouter;