231 lines
7.7 KiB
TypeScript
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;
|