e56a6a1608
- 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
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
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 });
|
||
}
|