Files
blackheart-website/src/app/api/admin/upload/route.ts
T
diana.dolgolyova e56a6a1608 fix: remove fallback content, fix video upload and positioning
- Remove hardcoded fallback data — DB is sole content source
- Sections render conditionally when data exists
- Hero video slots save after each upload (not only when all 3 filled)
- Video positions preserved (left/center/right) with empty string slots
- Client-side 10MB hard limit on video uploads with clear error
- Server-side upload error handling for body size limits
- Guard Team section against empty members array
- Clean up old uploaded images and videos
2026-03-29 22:17:11 +03:00

80 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";
const IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp", "image/avif"];
const VIDEO_TYPES = ["video/mp4", "video/webm"];
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif"];
const VIDEO_EXTENSIONS = [".mp4", ".webm"];
const IMAGE_FOLDERS = ["team", "master-classes", "news", "classes"];
const VIDEO_FOLDERS = ["hero"];
const ALL_FOLDERS = [...IMAGE_FOLDERS, ...VIDEO_FOLDERS];
const IMAGE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
const VIDEO_MAX_SIZE = 10 * 1024 * 1024; // 10MB
export async function POST(request: NextRequest) {
let formData: FormData;
try {
formData = await request.formData();
} catch (e) {
const msg = e instanceof Error ? e.message : "Upload failed";
return NextResponse.json(
{ error: msg.includes("size") || msg.includes("multipart") ? "Файл слишком большой (макс. 10 МБ)" : `Ошибка загрузки: ${msg}` },
{ status: 413 }
);
}
const file = formData.get("file") as File | null;
const rawFolder = (formData.get("folder") as string) || "team";
const folder = ALL_FOLDERS.includes(rawFolder) ? rawFolder : "team";
const isVideoFolder = VIDEO_FOLDERS.includes(folder);
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const allowedTypes = isVideoFolder ? VIDEO_TYPES : IMAGE_TYPES;
if (!allowedTypes.includes(file.type)) {
const msg = isVideoFolder
? "Допустимы только MP4 и WebM"
: "Допустимы только JPEG, PNG, WebP и AVIF";
return NextResponse.json({ error: msg }, { status: 400 });
}
const maxSize = isVideoFolder ? VIDEO_MAX_SIZE : IMAGE_MAX_SIZE;
if (file.size > maxSize) {
const label = isVideoFolder ? "10 МБ" : "5 МБ";
return NextResponse.json(
{ error: `Файл слишком большой (макс. ${label})` },
{ status: 400 }
);
}
// Validate and sanitize filename
const ext = path.extname(file.name).toLowerCase() || (isVideoFolder ? ".mp4" : ".webp");
const allowedExts = isVideoFolder ? VIDEO_EXTENSIONS : IMAGE_EXTENSIONS;
if (!allowedExts.includes(ext)) {
return NextResponse.json(
{ error: "Недопустимое расширение файла" },
{ status: 400 }
);
}
const baseName = file.name
.replace(ext, "")
.toLowerCase()
.replace(/[^a-z0-9а-яё-]/gi, "-")
.replace(/-+/g, "-")
.slice(0, 50);
const fileName = `${baseName}-${Date.now()}${ext}`;
const subDir = isVideoFolder ? path.join("video") : path.join("images", folder);
const dir = path.join(process.cwd(), "public", subDir);
await mkdir(dir, { recursive: true });
const buffer = Buffer.from(await file.arrayBuffer());
const filePath = path.join(dir, fileName);
await writeFile(filePath, buffer);
const publicPath = `/${subDir.replace(/\\/g, "/")}/${fileName}`;
return NextResponse.json({ path: publicPath });
}