Разработка формы с загрузкой файлов на сайте
Форма с загрузкой файлов — один из тех компонентов, где пользовательский опыт и надёжность бэкенда одинаково важны. Плохая реализация ломается на больших файлах, не даёт обратной связи и теряет данные при ошибках сети. Правильная — работает в любых условиях.
Что входит в реализацию
Клиентская часть:
- Drag-and-drop зона + кнопка «Выбрать файл»
- Превью для изображений (через
FileReaderилиURL.createObjectURL) - Прогресс-бар загрузки с реальными процентами
- Валидация: тип файла, размер, количество
- Обработка ошибок с человекочитаемыми сообщениями
- Отмена загрузки через
AbortController
Серверная часть:
- Multipart upload с поддержкой больших файлов (chunked upload при необходимости)
- Валидация MIME-типа по содержимому файла, а не только по расширению
- Антивирусная проверка через ClamAV или стороннее API (опционально)
- Хранение: локально, S3-совместимое хранилище (MinIO, AWS S3, Cloudflare R2)
- Генерация уникальных имён файлов, изоляция по пользователям
Технический стек
| Слой | Варианты |
|---|---|
| UI-компонент | React + react-dropzone, Vue + собственный хук |
| HTTP-загрузка | XMLHttpRequest (прогресс), fetch + ReadableStream |
| Бэкенд | Laravel (Storage facade), Node.js (multer, busboy) |
| Хранилище | AWS S3, MinIO, локальный диск |
| Превью изображений | Canvas API, sharp на сервере |
Пример: базовая загрузка с прогрессом
function uploadFile(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.open('POST', '/api/upload');
xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').content);
xhr.send(formData);
});
}
Chunked upload для больших файлов
Если нужна загрузка файлов от 100 МБ и выше — используем разбивку на части. Стандарт де-факто — tus protocol, для S3 — Multipart Upload API.
// tus-js-client
import { Upload } from 'tus-js-client';
const upload = new Upload(file, {
endpoint: '/api/upload/tus',
chunkSize: 5 * 1024 * 1024, // 5 MB chunks
retryDelays: [0, 1000, 3000, 5000],
metadata: { filename: file.name, filetype: file.type },
onProgress(bytesUploaded, bytesTotal) {
const pct = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
console.log(`${pct}%`);
},
onSuccess() {
console.log('Done:', upload.url);
},
});
upload.start();
На сервере Laravel — пакет ankurk91/laravel-tus-upload или собственная реализация через tus-php.
Валидация на сервере (Laravel)
$request->validate([
'file' => [
'required',
'file',
'max:102400', // 100 MB
'mimes:jpg,jpeg,png,pdf,docx',
function ($attribute, $value, $fail) {
$mime = mime_content_type($value->getRealPath());
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowed)) {
$fail('Тип файла не разрешён.');
}
},
],
]);
Безопасность
-
Никогда не доверять
$_FILES['type']— толькоmime_content_type()илиfinfo - Хранить файлы вне
public/или в отдельном S3 bucket без публичного доступа - Раздавать файлы через подписанные URL (S3 Presigned URLs) с TTL
- Ограничивать rate limiting на эндпоинт загрузки
- Сканировать архивы (zip bomb protection): проверять соотношение сжатия
Сроки
Базовая форма с drag-and-drop, прогрессом и S3-хранилищем — 3–4 рабочих дня. Chunked upload с возобновлением, антивирусной проверкой и административным интерфейсом управления файлами — 7–10 дней.







