diff --git a/packages/client/src/hook/useGalleryDoc.ts b/packages/client/src/hook/useGalleryDoc.ts index 71ee319..0021237 100644 --- a/packages/client/src/hook/useGalleryDoc.ts +++ b/packages/client/src/hook/useGalleryDoc.ts @@ -16,6 +16,16 @@ export function useRehashDoc() { }; } +export function useRescanDoc() { + const { mutate } = useSWRConfig(); + return async (id: string) => { + await fetch(`/api/doc/${id}/_rescan`, { + method: "POST", + }); + mutate(`/api/doc/${id}`); + }; +} + export function useGalleryDocSimilar(id: string) { return useSWR(`/api/doc/${id}/similars`, fetcher); } \ No newline at end of file diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx index 6d60c23..cb1c211 100644 --- a/packages/client/src/page/contentInfoPage.tsx +++ b/packages/client/src/page/contentInfoPage.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { useGalleryDoc, useGalleryDocSimilar, useRehashDoc } from "../hook/useGalleryDoc.ts"; +import { useGalleryDoc, useGalleryDocSimilar, useRehashDoc, useRescanDoc } from "../hook/useGalleryDoc.ts"; import TagBadge from "@/components/gallery/TagBadge"; import StyledLink from "@/components/gallery/StyledLink"; import { Link, useLocation } from "wouter"; @@ -8,6 +8,7 @@ import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx"; import { Button } from "@/components/ui/button.tsx"; import { GalleryCard } from "@/components/gallery/GalleryCard.tsx"; import { useEffect, useRef } from "react"; +import { useLogin } from "@/state/user.ts"; export interface ContentInfoPageProps { params: { @@ -35,6 +36,10 @@ function Wrapper({ children }: { children: React.ReactNode }) { export function ContentInfoPage({ params }: ContentInfoPageProps) { const { data, error, isLoading } = useGalleryDoc(params.id); const rehashDoc = useRehashDoc(); + const rescanDoc = useRescanDoc(); + const user = useLogin(); + const username = user?.username; + const isAdmin = username === "admin"; if (isLoading) { return
@@ -69,12 +74,21 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
-
- -
+ {isAdmin && +
+ + +
+ } diff --git a/packages/server/src/db/doc.ts b/packages/server/src/db/doc.ts index 6249c98..d020259 100644 --- a/packages/server/src/db/doc.ts +++ b/packages/server/src/db/doc.ts @@ -9,6 +9,8 @@ import type { } from "dbtype"; import type { NotNull } from "kysely"; import { MyParseJSONResultsPlugin } from "./plugin.ts"; +import { getContentFileConstructor } from "src/content/file.ts"; +import { join } from "path"; type DBDocument = db.Document; @@ -280,6 +282,45 @@ class SqliteDocumentAccessor implements DocumentAccessor { additional: {}, })); } + + async rescanDocument(c: Document) { + return this.kysely.transaction().execute(async (trx) => { + const ContentFile = getContentFileConstructor(c.content_type); + if (ContentFile === undefined) { + throw new Error(`${c.content_type} is not defined.`); + } + const path = join(c.basepath, c.filename); + const content = new ContentFile(path); + const body = await content.createDocumentBody(); + + const { tags, ...rest } = body; + + // update document + await trx.updateTable("document") + .set({ + ...rest, + modified_at: Date.now(), + additional: JSON.stringify(body.additional), + }) + .where("id", "=", c.id) + .execute(); + + // delete old tags + await trx.deleteFrom("doc_tag_relation") + .where("doc_id", "=", c.id) + .execute(); + // update tags and doc_tag_relation + await trx.insertInto("tags") + .values(tags.map((x) => ({ name: x }))) + .onConflict((oc) => oc.doNothing()) + .execute(); + await trx.insertInto("doc_tag_relation") + .values(tags.map((x) => ({ tag_name: x, doc_id: c.id })) + ) + .execute(); + + }); + } } export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => { return new SqliteDocumentAccessor(kysely); diff --git a/packages/server/src/model/doc.ts b/packages/server/src/model/doc.ts index dd9abab..0758041 100644 --- a/packages/server/src/model/doc.ts +++ b/packages/server/src/model/doc.ts @@ -36,6 +36,11 @@ export const isDoc = (c: unknown): c is Document => { }; export interface DocumentAccessor { + /** + * rescan document + * it will update document's metadata and tags from file. + */ + rescanDocument(c: Document): Promise; /** * find list by option * @returns documents list diff --git a/packages/server/src/route/contents.ts b/packages/server/src/route/contents.ts index 03f5296..fdcb12b 100644 --- a/packages/server/src/route/contents.ts +++ b/packages/server/src/route/contents.ts @@ -197,6 +197,19 @@ function getSimilarDocumentHandler(controller: DocumentAccessor) { }; } +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)); @@ -211,6 +224,7 @@ export const getContentRouter = (controller: DocumentAccessor) => { 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; };