import { randomUUID } from "node:crypto"; import { unlink, writeFile } from "node:fs/promises"; import { NextResponse } from "next/server"; import { z } from "zod"; import { getAuthenticatedUser } from "@/lib/auth"; import { db } from "@/lib/db"; import { getChannel, isSetupComplete } from "@/lib/setup"; import { ensureStorageDirectories, getThumbnailExtension, getThumbnailPath, getVideoPath, } from "@/lib/storage"; export const runtime = "nodejs"; const metadataSchema = z.object({ title: z.string().trim().min(1).max(140), description: z.string().trim().max(5000).optional(), }); const maxVideoBytes = Number(process.env.MAX_VIDEO_UPLOAD_BYTES ?? 1_073_741_824); const maxThumbnailBytes = Number(process.env.MAX_THUMBNAIL_UPLOAD_BYTES ?? 5_242_880); function redirectWithError(request: Request, error: string) { const url = new URL("/dashboard", request.url); url.searchParams.set("error", error); return NextResponse.redirect(url, 303); } function redirectWithSuccess(request: Request, success: string) { const url = new URL("/dashboard", request.url); url.searchParams.set("success", success); return NextResponse.redirect(url, 303); } export async function POST(request: Request) { if (!(await isSetupComplete())) { return NextResponse.redirect(new URL("/setup", request.url), 303); } const user = await getAuthenticatedUser(); if (!user) { return NextResponse.redirect(new URL("/login", request.url), 303); } const channel = await getChannel(); if (!channel) { return NextResponse.redirect(new URL("/setup", request.url), 303); } const formData = await request.formData(); const metadata = metadataSchema.safeParse({ title: formData.get("title"), description: formData.get("description") || undefined, }); if (!metadata.success) { return redirectWithError(request, "Video title is required."); } const videoInput = formData.get("video"); if (!(videoInput instanceof File)) { return redirectWithError(request, "Please select a video file."); } if (videoInput.size <= 0) { return redirectWithError(request, "Video file cannot be empty."); } if (videoInput.size > maxVideoBytes) { return redirectWithError(request, "Video exceeds the configured upload size limit."); } const lowerVideoName = videoInput.name.toLowerCase(); const isMp4 = videoInput.type === "video/mp4" || lowerVideoName.endsWith(".mp4"); if (!isMp4) { return redirectWithError(request, "Only MP4 uploads are supported right now."); } const thumbnailInput = formData.get("thumbnail"); const thumbnailFile = thumbnailInput instanceof File && thumbnailInput.size > 0 ? thumbnailInput : null; let thumbnailExtension: string | null = null; if (thumbnailFile) { if (thumbnailFile.size > maxThumbnailBytes) { return redirectWithError( request, "Thumbnail exceeds the configured upload size limit.", ); } thumbnailExtension = getThumbnailExtension(thumbnailFile); if (!thumbnailExtension) { return redirectWithError( request, "Thumbnail must be JPG, PNG, or WEBP.", ); } } const videoId = randomUUID().replaceAll("-", ""); const videoFilename = `${videoId}.mp4`; const videoPath = getVideoPath(videoFilename); let thumbnailFilename: string | null = null; let thumbnailMimeType: string | null = null; let thumbnailPath: string | null = null; if (thumbnailFile && thumbnailExtension) { thumbnailFilename = `${videoId}${thumbnailExtension}`; thumbnailMimeType = thumbnailFile.type || (thumbnailExtension === ".png" ? "image/png" : thumbnailExtension === ".webp" ? "image/webp" : "image/jpeg"); thumbnailPath = getThumbnailPath(thumbnailFilename); } try { await ensureStorageDirectories(); await writeFile(videoPath, Buffer.from(await videoInput.arrayBuffer())); if (thumbnailFile && thumbnailPath) { await writeFile(thumbnailPath, Buffer.from(await thumbnailFile.arrayBuffer())); } await db.video.create({ data: { id: videoId, channelId: channel.id, title: metadata.data.title, description: metadata.data.description || null, videoFilename, videoMimeType: videoInput.type || "video/mp4", videoSizeBytes: videoInput.size, thumbnailFilename, thumbnailMimeType, }, }); return redirectWithSuccess(request, "Upload complete."); } catch { await Promise.all([ unlink(videoPath).catch(() => undefined), thumbnailPath ? unlink(thumbnailPath).catch(() => undefined) : undefined, ]); return redirectWithError(request, "Upload failed. Please try again."); } }