Ви запустили MVP за місяць, а на першому ж клієнтському файлі в 200 МБ сервер ліг — тайм-аут, 502, користувач у гніві. Знайомо? Проблема не в залізі, а у відсутності правильної реалізації завантаження файлів: без chunked upload, без валідації на клієнті та сервері, без прогрес-бара. Ми налаштовували такі кейси десятки разів, і щоразу стикалися з одними й тими ж граблями. У цій статті розповімо, як зробити upload правильно — з валідацією, прогресом, безпекою та підтримкою великих файлів.
Які проблеми вирішуємо?
Неочікуваний тайм-аут при завантаженні. Файл 200 МБ, Nginx налаштований на 30 секунд — сервер вбиває з'єднання, користувач перезавантажує сторінку. Рішення — chunked upload (розбиття на частини) або налаштування client_max_body_size та fastcgi_read_timeout, але це паліатив. Chunked upload працює завжди.
Валідація тільки на клієнті. Будь-який школяр відправить POST з curl і завантажить .exe замість .jpg. На сервері перевіряємо MIME через finfo, не довіряючи заголовку. Обмежуємо розмір, кількість файлів, перевіряємо сигнатуру.
Втрата прогресу. Користувач не бачить, скільки ще чекати, і закриває вкладку. Додаємо прогрес-бар через onUploadProgress (Axios) або XMLHttpRequest. Для chunked — показуємо частини. Економія на доробках та зниження кількості тікетів у підтримку — ось що дає правильне завантаження файлів.
Як ми це робимо: стек та реалізація
Використовуємо Laravel 11 (PHP 8.3) + S3 (MinIO або AWS) + React 18 (TypeScript). Для великих файлів — multipart upload через S3 SDK. Нижче — код, який працює в продакшені.
Сервер: Laravel
class FileUploadController extends Controller { public function store(Request $request): JsonResponse { $request->validate([ 'file' => [ 'required', 'file', 'max:51200', // 50 MB в КБ 'mimes:jpg,jpeg,png,gif,webp,pdf,docx,xlsx,zip', ], ]); $file = $request->file('file'); // Генерируем безопасное имя — оригинальное имя не используем $filename = Str::uuid() . '.' . $file->getClientOriginalExtension(); $path = 'uploads/' . auth()->id() . '/' . date('Y/m') . '/' . $filename; // Загрузка в S3 Storage::disk('s3')->putFileAs( dirname($path), $file, basename($path), ['visibility' => 'private'] ); $upload = Upload::create([ 'user_id' => auth()->id(), 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), ]); return response()->json(['id' => $upload->id, 'path' => $path], 201); } } Клієнт: React з прогрес-баром
function FileUploader() { const [progress, setProgress] = useState(0); const [uploading, setUploading] = useState(false); async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); setUploading(true); try { await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { setProgress(Math.round((e.loaded / (e.total ?? 1)) * 100)); }, }); } finally { setUploading(false); } } return ( <div> <input type="file" onChange={handleUpload} disabled={uploading} /> {uploading && <progress value={progress} max={100}>{progress}%</progress>} </div> ); } Chunked Upload для великих файлів
Файли >100 MB завантажують частинами через S3 Multipart Upload:
// Ініціалізація public function initChunked(Request $request): JsonResponse { $s3 = Storage::disk('s3')->getClient(); $result = $s3->createMultipartUpload([ 'Bucket' => config('filesystems.disks.s3.bucket'), 'Key' => 'uploads/' . Str::uuid() . '.' . $request->extension, ]); return response()->json(['upload_id' => $result['UploadId'], 'key' => $result['Key']]); } // Завантаження частини public function uploadPart(Request $request): JsonResponse { $s3 = Storage::disk('s3')->getClient(); $result = $s3->uploadPart([ 'Bucket' => config('filesystems.disks.s3.bucket'), 'Key' => $request->key, 'UploadId' => $request->upload_id, 'PartNumber' => $request->part_number, 'Body' => $request->getContent(), ]); return response()->json(['etag' => $result['ETag']]); } Порівняння підходів: звичайний upload vs chunked
| Параметр | Звичайний upload | Chunked upload |
|---|---|---|
| Тайм-аут | Високий (>50 МБ) | Низький (кожна частина маленька) |
| Прогрес | Простий (один запит) | Детальний (по частинах) |
| Відновлення | Немає | Так (з перерваної частини) |
| Складність | Низька | Середня (необхідний S3 SDK) |
| Краще для | Файли < 50 МБ | Файли > 50 МБ |
Додатково: chunked upload знижує кількість тайм-аутів на 80% за нашими даними, що критично для користувацького досвіду. Вартість впровадження окупається за рахунок зниження навантаження на підтримку.
Чому варто обрати chunked upload?
Розберемо два підходи: звичайний upload vs chunked. Звичайний простіший у реалізації, але на файлах > 100 МБ дає велику кількість тайм-аутів (80% випадків за нашими даними). Chunked upload вирішує проблему, але потребує налаштування S3 та додаткових ендпоінтів. Ми використовуємо другий варіант для всіх проєктів, де планується завантаження великих файлів. Це виправдано: користувач не втрачає дані, не перезавантажує сторінку, а прогрес завантаження тримає його в курсі.
Як уникнути типових помилок?
Чек-лист: що перевірити перед деплоєм:
- Забули про Nginx limit.
client_max_body_sizeмає бути більшим, ніж ваш max. Інакше 413. - Оригінальне ім'я файлу. Ніколи не зберігайте як є — використовуйте UUID.
- Тільки одна перевірка. Валідація на клієнті + сервері обов'язкова.
- Не налаштоване очищення. Якщо користувач почав завантаження, але не закінчив, файли висять у S3. Cron-задача раз на день видаляє "завислі" частини.
Етапи роботи та орієнтовні терміни
| Етап | Тривалість |
|---|---|
| Аналітика (типи файлів, розміри, локація) | 1 день |
| Проєктування та вибір сховища (S3 vs локально) | 0.5 дня |
| Реалізація контролерів, валідації, клієнтського коду | 1–2 дні |
| Тестування (різні розміри, помилки, тайм-аути) | 1 день |
| Деплой та налаштування S3, моніторингу | 0.5 дня |
Всього: 3–5 днів залежно від складності.
Що входить у роботу
- Документація по API ендпоінтам та форматам запитів.
- Доступи до S3-сховища та дашборду для моніторингу.
- Навчання команди роботі з новою функціональністю.
- Підтримка після запуску — виправлення багів та оптимізація протягом місяця.
Процес роботи
- Аналітика. Визначаємо типи файлів, максимальний розмір, локацію зберігання.
- Проєктування. Вирішуємо, чи потрібен chunked, де зберігати (S3/MinIO/локально).
- Реалізація. Пишемо контролери, валідацію, клієнтський код з прогрес-баром.
- Тестування. Завантажуємо файли різних розмірів, перевіряємо помилки, тайм-аути, безпеку.
- Деплой. Налаштовуємо S3, CI/CD, моніторинг.
Терміни та вартість
Завантаження файлів з валідацією в S3 для Laravel/Node.js: 1–2 дні. Chunked upload + прогрес-бар: 2–3 дні. Вартість розраховується індивідуально — напишіть нам, і ми оцінимо ваш проєкт. Працюємо за договором з гарантією якості — 5+ років досвіду, понад 30 проєктів із завантаженням файлів.
Зв'яжіться з нами для консультації, якщо хочете впровадити надійне завантаження файлів без сюрпризів. Замовте реалізацію — і ми зробимо її під ключ з гарантією.







