import { unlink } from "node:fs/promises"; import path from "node:path"; import { del } from "@vercel/blob"; import { and, eq } from "drizzle-orm"; import { NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { canvasProject, mediaAsset } from "@/lib/db/schema"; import { createLogger } from "@/lib/observability/logger"; import { deleteS3ObjectByPath, resolveS3UploadTarget, } from "@/lib/storage/service"; export const runtime = "nodejs"; const logger = createLogger("video-canvas-director-captures"); const deleteSchema = z.object({ assetId: z.string().uuid().optional(), canvasId: z.string().uuid(), imageUrl: z.string().trim().max(4096).optional(), pathname: z .string() .trim() .max(1024) .transform((value) => value.replace(/^\/+/, "")) .refine((value) => !value.includes("..") && !value.includes("\\"), "文件路径无效") .optional(), }); async function deleteStoredCapture(pathname: string, imageUrl: string) { if (pathname.startsWith("uploads/local/")) { const publicRoot = path.resolve(process.cwd(), "public"); const absolutePath = path.resolve(publicRoot, pathname); if (!absolutePath.startsWith(`${publicRoot}${path.sep}`)) { throw new Error("文件路径越界"); } await unlink(absolutePath).catch((error: NodeJS.ErrnoException) => { if (error.code !== "ENOENT") throw error; }); return; } if ( imageUrl.includes(".public.blob.vercel-storage.com/") && process.env.BLOB_READ_WRITE_TOKEN ) { await del(imageUrl, { token: process.env.BLOB_READ_WRITE_TOKEN }); return; } const target = await resolveS3UploadTarget(); if (!target) throw new Error("没有可用的对象存储配置"); await deleteS3ObjectByPath(pathname, target.config); } export async function DELETE(request: Request) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); } const parsed = deleteSchema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json( { success: false, error: parsed.error.issues[0]?.message || "请求参数无效" }, { status: 400 }, ); } const [project] = await db .select({ id: canvasProject.id }) .from(canvasProject) .where( and( eq(canvasProject.id, parsed.data.canvasId), eq(canvasProject.userId, session.user.id), ), ) .limit(1); if (!project) { return NextResponse.json({ success: false, error: "画布不存在" }, { status: 404 }); } if (parsed.data.pathname) { await deleteStoredCapture( parsed.data.pathname, parsed.data.imageUrl || "", ).catch((error) => { logger.warn("capture file cleanup failed", { error }); }); } if (parsed.data.assetId) { await db .delete(mediaAsset) .where( and( eq(mediaAsset.id, parsed.data.assetId), eq(mediaAsset.userId, session.user.id), ), ); } return NextResponse.json({ success: true }); } catch (error) { logger.error("capture cleanup failed", { error }); return NextResponse.json( { success: false, error: "截图清理失败" }, { status: 500 }, ); } }