114 lines
3.4 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|