initial commit

This commit is contained in:
zhangshun
2026-05-23 09:11:46 +08:00
commit 5d08480921
42 changed files with 10615 additions and 0 deletions

154
server/src/folders.ts Normal file
View File

@@ -0,0 +1,154 @@
import fs from "node:fs/promises";
import path from "node:path";
import { FastifyInstance } from "fastify";
import { z } from "zod";
import { db } from "./db.js";
import { requireAdmin, requireAuth } from "./auth.js";
import { storage } from "./storage.js";
import { FolderRow } from "./types.js";
import { assertValidFolderName, ensureDir, removeIfExists, safeStoragePath, toFolderStorageRelative } from "./utils.js";
const createSchema = z.object({
parentId: z.number().int().nullable().optional(),
name: z.string()
});
const updateSchema = z.object({
name: z.string()
});
function buildTree(rows: FolderRow[]) {
const nodes = rows.map((row) => ({
id: String(row.id),
text: row.name,
parentId: row.parent_id,
path: row.path,
children: [] as unknown[]
}));
const byId = new Map(nodes.map((node) => [Number(node.id), node]));
const roots = [];
for (const node of nodes) {
if (node.parentId && byId.has(node.parentId)) {
byId.get(node.parentId)!.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
function getFolder(id: number) {
return db.prepare("SELECT * FROM folders WHERE id = ?").get(id) as FolderRow | undefined;
}
function childPath(parent: FolderRow | undefined, name: string) {
return parent ? path.posix.join(parent.path, name) : name;
}
function assertCanRenameRoot(folder: FolderRow) {
if (folder.parent_id === null) {
throw new Error("根目录不能重命名");
}
}
export async function folderRoutes(app: FastifyInstance) {
app.get("/api/folders", { preHandler: [requireAuth] }, async () => {
const rows = db.prepare("SELECT * FROM folders ORDER BY parent_id, name").all() as unknown as FolderRow[];
return { folders: rows, tree: buildTree(rows) };
});
app.post("/api/folders", { preHandler: [requireAdmin] }, async (request, reply) => {
const body = createSchema.parse(request.body);
const name = assertValidFolderName(body.name);
const parent = body.parentId ? getFolder(body.parentId) : undefined;
if (body.parentId && !parent) {
return reply.code(404).send({ message: "父目录不存在" });
}
const folderPath = childPath(parent, name);
try {
const result = db.prepare("INSERT INTO folders (parent_id, name, path) VALUES (?, ?, ?)")
.run(body.parentId ?? null, name, folderPath);
await ensureDir(safeStoragePath(toFolderStorageRelative(folderPath)));
return reply.code(201).send({ id: result.lastInsertRowid, name, path: folderPath });
} catch {
return reply.code(409).send({ message: "同级目录已存在" });
}
});
app.put("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const body = updateSchema.parse(request.body);
const name = assertValidFolderName(body.name);
const folder = getFolder(params.id);
if (!folder) {
return reply.code(404).send({ message: "目录不存在" });
}
assertCanRenameRoot(folder);
const parent = folder.parent_id ? getFolder(folder.parent_id) : undefined;
const oldPath = folder.path;
const newPath = childPath(parent, name);
if (storage.provider !== "local") {
const modelCount = db.prepare("SELECT COUNT(*) AS count FROM models WHERE file_path LIKE ?")
.get(`models/${oldPath}/%`) as { count: number };
if (modelCount.count > 0) {
return reply.code(400).send({ message: "当前存储模式下,包含模型的目录不能重命名" });
}
}
const tx = () => {
db.exec("BEGIN");
try {
db.prepare("UPDATE folders SET name = ?, path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
.run(name, newPath, folder.id);
const descendants = db.prepare("SELECT * FROM folders WHERE path LIKE ? ORDER BY LENGTH(path)")
.all(`${oldPath}/%`) as unknown as FolderRow[];
for (const item of descendants) {
const replacedPath = item.path.replace(oldPath + "/", newPath + "/");
db.prepare("UPDATE folders SET path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
.run(replacedPath, item.id);
}
if (storage.provider === "local") {
const models = db.prepare("SELECT id, file_path FROM models WHERE file_path LIKE ?")
.all(`models/${oldPath}/%`) as { id: number; file_path: string }[];
for (const model of models) {
db.prepare("UPDATE models SET file_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
.run(model.file_path.replace(`models/${oldPath}/`, `models/${newPath}/`), model.id);
}
}
db.exec("COMMIT");
} catch (error) {
db.exec("ROLLBACK");
throw error;
}
};
try {
tx();
if (storage.provider === "local") {
await fs.rename(
safeStoragePath(toFolderStorageRelative(oldPath)),
safeStoragePath(toFolderStorageRelative(newPath))
);
}
return { ok: true };
} catch {
return reply.code(409).send({ message: "目录重命名失败,可能存在重名目录" });
}
});
app.delete("/api/folders/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
const folder = getFolder(params.id);
if (!folder) {
return reply.code(404).send({ message: "目录不存在" });
}
if (folder.parent_id === null) {
return reply.code(400).send({ message: "根目录不能删除" });
}
db.prepare("DELETE FROM folders WHERE id = ?").run(folder.id);
await removeIfExists(safeStoragePath(toFolderStorageRelative(folder.path)));
return { ok: true };
});
}