ionian/packages/server/src/route/contents.ts

227 lines
9.6 KiB
TypeScript

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