Files
metube/src/app/api/videos/route.ts
T
2026-04-14 14:39:13 -05:00

160 lines
4.7 KiB
TypeScript

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.");
}
}