feat: rescan document

This commit is contained in:
monoid 2024-10-29 00:38:35 +09:00
parent e00c888d7b
commit 0d3128948b
5 changed files with 91 additions and 7 deletions

View File

@ -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) { export function useGalleryDocSimilar(id: string) {
return useSWR<Document[]>(`/api/doc/${id}/similars`, fetcher); return useSWR<Document[]>(`/api/doc/${id}/similars`, fetcher);
} }

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 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 TagBadge from "@/components/gallery/TagBadge";
import StyledLink from "@/components/gallery/StyledLink"; import StyledLink from "@/components/gallery/StyledLink";
import { Link, useLocation } from "wouter"; 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 { Button } from "@/components/ui/button.tsx";
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx"; import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useLogin } from "@/state/user.ts";
export interface ContentInfoPageProps { export interface ContentInfoPageProps {
params: { params: {
@ -35,6 +36,10 @@ function Wrapper({ children }: { children: React.ReactNode }) {
export function ContentInfoPage({ params }: ContentInfoPageProps) { export function ContentInfoPage({ params }: ContentInfoPageProps) {
const { data, error, isLoading } = useGalleryDoc(params.id); const { data, error, isLoading } = useGalleryDoc(params.id);
const rehashDoc = useRehashDoc(); const rehashDoc = useRehashDoc();
const rescanDoc = useRescanDoc();
const user = useLogin();
const username = user?.username;
const isAdmin = username === "admin";
if (isLoading) { if (isLoading) {
return <div className="p-4 flex items-center justify-center h-full"> return <div className="p-4 flex items-center justify-center h-full">
@ -69,12 +74,21 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
</div> </div>
</Link> </Link>
<Card className="flex-1 relative"> <Card className="flex-1 relative">
<div className="absolute top-0 right-0 p-2"> {isAdmin &&
<div className="absolute top-0 right-0 p-2 grid">
<Button variant="ghost" onClick={async () => { <Button variant="ghost" onClick={async () => {
// Rehash // Rehash
await rehashDoc(params.id); await rehashDoc(params.id);
}}>Rehash</Button> }}>Rehash</Button>
<Button variant="ghost" onClick={async () => {
if (!window.confirm("Are you sure?")) {
return;
}
// Rescan
await rescanDoc(params.id);
}}>Rescan</Button>
</div> </div>
}
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<StyledLink to={contentLocation}> <StyledLink to={contentLocation}>

View File

@ -9,6 +9,8 @@ import type {
} from "dbtype"; } from "dbtype";
import type { NotNull } from "kysely"; import type { NotNull } from "kysely";
import { MyParseJSONResultsPlugin } from "./plugin.ts"; import { MyParseJSONResultsPlugin } from "./plugin.ts";
import { getContentFileConstructor } from "src/content/file.ts";
import { join } from "path";
type DBDocument = db.Document; type DBDocument = db.Document;
@ -280,6 +282,45 @@ class SqliteDocumentAccessor implements DocumentAccessor {
additional: {}, 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 => { export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => {
return new SqliteDocumentAccessor(kysely); return new SqliteDocumentAccessor(kysely);

View File

@ -36,6 +36,11 @@ export const isDoc = (c: unknown): c is Document => {
}; };
export interface DocumentAccessor { export interface DocumentAccessor {
/**
* rescan document
* it will update document's metadata and tags from file.
*/
rescanDocument(c: Document): Promise<void>;
/** /**
* find list by option * find list by option
* @returns documents list * @returns documents list

View File

@ -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) => { export const getContentRouter = (controller: DocumentAccessor) => {
const ret = new Router(); const ret = new Router();
ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller)); 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.del("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), DelTagHandler(controller));
ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), new AllContentRouter().routes()); ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), new AllContentRouter().routes());
ret.post("/:num(\\d+)/_rehash", AdminOnly, RehashContentHandler(controller)); ret.post("/:num(\\d+)/_rehash", AdminOnly, RehashContentHandler(controller));
ret.post("/:num(\\d+)/_rescan", AdminOnly, getRescanDocumentHandler(controller));
return ret; return ret;
}; };