66 lines
3.8 KiB
TypeScript
66 lines
3.8 KiB
TypeScript
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<string>();
|
|
const formats: Record<string, string> = {
|
|
"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 });
|
|
}
|
|
}
|