Перейти к содержимому

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 });
}
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 });
}
// Дальше — загрузка
};