ionian/packages/server/src/route/contents.ts
2024-10-29 00:38:35 +09:00

231 lines
7.7 KiB
TypeScript

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<Document> & { 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;