114 lines
3.4 KiB
TypeScript

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 },
);
}
}