Варіант 1 - multipart/form-data через сервер API:
POST /api/documents
Content-Type: multipart/form-data; boundary=...
title=Договір
file=<бінарні дані>
$request->validate(['file' => ['required', 'file', 'mimes:pdf', 'max:10240']]);
$path = $request->file('file')->store('documents', 's3');
Просто, валідація й збереження в одному запиті. Але весь файл проходить через сервер застосунку: займає процес PHP на весь час завантаження, обмежений upload_max_filesize/post_max_size і тайм-аутами, а для великих файлів - ще й проксі (client_max_body_size у Nginx).
Варіант 2 - підписаний URL і пряме завантаження в сховище (S3, R2):
- клієнт просить дозвіл:
POST /api/uploads {"filename": "contract.pdf", "size": 52428800}; - сервер перевіряє права й параметри і повертає тимчасовий підписаний URL:
['url' => $url, 'headers' => $headers] = Storage::disk('s3')
->temporaryUploadUrl("uploads/{$id}.pdf", now()->plus(minutes: 5));
- клієнт завантажує файл напряму в сховище (
PUT $url); - клієнт повідомляє API:
POST /api/documents {"upload_id": "..."}- сервер перевіряє, що файл справді з'явився, його розмір і тип, і створює запис.
Переваги прямого завантаження: сервер застосунку не тримає з'єднання, немає лімітів PHP, сховище масштабується саме, можна завантажувати частинами (multipart upload S3) з продовженням після збою.
Що обов'язково для безпеки:
- короткий термін дії URL і фіксований шлях - клієнт не обирає, куди писати;
- перевірка після завантаження: розмір і тип - не довіряти тому, що клієнт заявив у кроці 1. Підозрілі файли - в карантин чи на антивірусну перевірку;
- очищення завантажених, але не підтверджених файлів (правило життєвого циклу бакета);
- приватний бакет і видача файлів теж через підписані URL.
Варіант 3 - base64 у JSON: зручно для дуже малих файлів (аватар-мініатюра), але +33% до розміру й весь файл у пам'яті - для решти погано.
Як обрати: невеликі файли (до кількох мегабайтів) - multipart; великі, багато одночасних завантажень, мобільні клієнти з нестабільною мережею - пряме завантаження.
Докладніше в документації: Laravel: тимчасові URL для завантаження