import { execFile } from "node:child_process"; import { randomUUID } from "node:crypto"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { uploadFile } from "@/lib/storage/service"; export const runtime = "nodejs"; export const maxDuration = 180; const probe = promisify(execFile); const running = new Set(); const formats: Record = { "video/mp4": "mp4", "video/quicktime": "mov", "video/webm": "webm", "audio/mpeg": "mp3", "audio/mp3": "mp3", "audio/mp4": "m4a", "audio/x-m4a": "m4a", "audio/wav": "wav", "audio/x-wav": "wav", "audio/ogg": "ogg", "audio/flac": "flac", "audio/webm": "webm", }; export async function POST(request: Request) { const session = await auth(); if (!session?.user?.id) return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); if (running.has(session.user.id) || running.size >= 4) return NextResponse.json({ success: false, error: "正在上传,请稍后重试" }, { status: 429 }); running.add(session.user.id); let dir = ""; try { const form = await request.formData(); const file = form.get("file"); if (!(file instanceof File) || !formats[file.type]) return NextResponse.json({ success: false, error: "请上传 MP4、MOV、WebM 视频,或 MP3、M4A、WAV、OGG、FLAC 音频" }, { status: 400 }); if (!file.size || file.size > 125 * 1024 * 1024) return NextResponse.json({ success: false, error: "素材必须非空且不超过 125 MiB" }, { status: 413 }); dir = await mkdtemp(join(tmpdir(), "canvas-upload-")); const input = join(dir, "input"); const buffer = await file.arrayBuffer(); await writeFile(input, Buffer.from(buffer)); let stdout: string; try { ({ stdout } = await probe("ffprobe", ["-v", "error", "-protocol_whitelist", "file", "-format_whitelist", "mov,matroska,webm,mp3,wav,ogg,flac", "-show_streams", "-show_format", "-of", "json", input], { timeout: 15000, maxBuffer: 1024 * 1024 })); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { return NextResponse.json({ success: false, error: "服务器缺少 ffprobe 视频解析组件,请联系管理员安装 FFmpeg" }, { status: 503 }); } return NextResponse.json({ success: false, error: "视频文件无法解析,请确认素材完整且格式受支持" }, { status: 400 }); } const info = JSON.parse(stdout); const kind = file.type.startsWith("video/") ? "video" : "audio"; const stream = info.streams?.find((item: { codec_type: string }) => item.codec_type === kind); const duration = Number(info.format?.duration); if (!stream || !Number.isFinite(duration) || duration <= 0 || duration > 600) return NextResponse.json({ success: false, error: "请上传内容完整、10 分钟以内的音视频" }, { status: 400 }); let uploaded; try { uploaded = await uploadFile(`canvas-${randomUUID()}.${formats[file.type]}`, buffer); } catch (error) { console.error("[Canvas media upload] Storage unavailable:", error); return NextResponse.json({ success: false, error: "媒体存储暂不可用,请稍后重试" }, { status: 503 }); } return NextResponse.json({ success: true, data: { url: uploaded.url, duration, width: stream.width, height: stream.height } }); } catch (error) { console.error("[Canvas media upload] Failed:", error); return NextResponse.json({ success: false, error: "素材上传失败,请确认文件完整或稍后重试" }, { status: 400 }); } finally { running.delete(session.user.id); if (dir) await rm(dir, { recursive: true, force: true }); } }