Ви запустили 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 проєктів із завантаженням файлів.
Зв'яжіться з нами для консультації, якщо хочете впровадити надійне завантаження файлів без сюрпризів. Замовте реалізацію — і ми зробимо її під ключ з гарантією.







