21. Загрузка файлов
Remix обрабатывает загрузку файлов через unstable_parseMultipartFormData. Файлы можно сохранять локально или в облачное хранилище (S3, Cloudflare R2).
Базовая загрузка файла
Заголовок раздела «Базовая загрузка файла»import { unstable_parseMultipartFormData, unstable_createFileUploadHandler, json,} from "@remix-run/node";
export async function action({ request }) { const uploadHandler = unstable_createFileUploadHandler({ directory: "./public/uploads", maxPartSize: 5_000_000, // 5MB });
const formData = await unstable_parseMultipartFormData( request, uploadHandler );
const file = formData.get("avatar");
if (!(file instanceof File)) { return json({ error: "Файл не загружен" }, { status: 400 }); }
return json({ name: file.name, size: file.size, type: file.type, });}Загрузка в память (для небольших файлов)
Заголовок раздела «Загрузка в память (для небольших файлов)»import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from "@remix-run/node";
export async function action({ request }) { const uploadHandler = unstable_createMemoryUploadHandler({ maxPartSize: 500_000, // 500KB });
const formData = await unstable_parseMultipartFormData( request, uploadHandler );
const image = formData.get("image") as File; const buffer = await image.arrayBuffer();
// Обработка в памяти: ресайз, конвертация и т.д. const processed = await processImage(Buffer.from(buffer));
return json({ url: processed.url });}Загрузка в S3
Заголовок раздела «Загрузка в S3»import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: process.env.AWS_REGION });
const s3UploadHandler: UploadHandler = async ({ name, filename, data }) => { if (name !== "file") { return undefined; // игнорируем другие поля }
const chunks: Uint8Array[] = []; for await (const chunk of data) { chunks.push(chunk); } const buffer = Buffer.concat(chunks);
const key = \`uploads/\${Date.now()}-\${filename}\`;
await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, Body: buffer, ContentType: "image/jpeg", }));
return \`https://s3.amazonaws.com/\${process.env.S3_BUCKET}/\${key}\`;};Форма загрузки с прогрессом
Заголовок раздела «Форма загрузки с прогрессом»import { useNavigation } from "@remix-run/react";
export default function UploadForm() { const navigation = useNavigation(); const isUploading = navigation.state === "submitting";
return ( <Form method="post" encType="multipart/form-data"> <input type="file" name="file" accept="image/*" required /> <button type="submit" disabled={isUploading}> {isUploading ? "Загрузка..." : "Загрузить"} </button> </Form> );}Валидация типа и размера
Заголовок раздела «Валидация типа и размера»const uploadHandler: UploadHandler = async ({ name, filename, data, contentType }) => { // Проверяем тип файла const allowedTypes = ["image/jpeg", "image/png", "image/webp"]; if (!allowedTypes.includes(contentType)) { throw new Response("Неверный тип файла", { status: 400 }); }
// Проверяем расширение const ext = path.extname(filename ?? "").toLowerCase(); const allowedExts = [".jpg", ".jpeg", ".png", ".webp"]; if (!allowedExts.includes(ext)) { throw new Response("Неверное расширение", { status: 400 }); }
// Дальше — загрузка};