0% прочитано

Пряме завантаження у S3-сховище: чому великий файл не варто пропускати через PHP

Для великого файла PHP не обов’язково має бути посередником у передаванні кожного байта. Застосунок може перевірити право, визначити шлях і видати короткоживучий дозвіл, а браузер завантажить файл безпосередньо у S3-сумісне сховище.

10 жовтня 2026 р. 10 хв читанняLaradock

У попередній статті я розбирала тимчасові посилання на приватні файли для читання приватного об’єкта.

Завантаження можна побудувати за дуже схожим принципом, тільки напрямок даних буде протилежним.

Laravel перевіряє, чи дозволена операція, визначає шлях майбутнього об’єкта і створює короткоживучий дозвіл.

А сам великий файл браузер передає безпосередньо у S3-сумісне сховище.

Пряме завантаження не означає, що Laravel усувається зі сценарію. Laravel контролює дозвіл і місце запису, але PHP не передає сам вміст великого файла.

Звичайне завантаження пропускає весь файл через PHP

PHP стоїть на шляху кожного байта
Browser   |   | large file   vNginx   |   vPHP   |   | temporary upload   | request handling   | storage write   vS3-compatible storage

Для невеликого файла це цілком нормальний сценарій.

Але з великим відео PHP стає посередником для великого потоку даних, хоча прикладному коду зазвичай не потрібно читати кожен байт.

Запит довше займає worker, проходить через обмеження розміру HTTP-запиту, час виконання і тимчасове зберігання на сервері.

При прямому завантаженні PHP передає дозвіл, а не файл

Застосунок контролює операцію, але не передає великий файл
1. Browser -> Laravel   file metadata 2. Laravel   authorize   choose object path   create short-lived upload permission 3. Browser -> S3-compatible storage   large file

Laravel може створити тимчасове посилання саме для завантаження

Короткоживучий дозвіл на запис
[    'url' => $url,    'headers' => $headers,] = Storage::disk('s3')    ->temporaryUploadUrl(        $path,        now()->addMinutes(5),    );

temporaryUploadUrl() повертає адресу завантаження і заголовки, які клієнт має передати разом із запитом.

Це не постійний дозвіл на запис у бакет.

Посилання стосується конкретного шляху і має обмежений строк дії.

Шлях майбутнього файла визначає застосунок, а не браузер

Приклад шляху, створеного сервером
$path = sprintf(    'private/videos/%s.mp4',    (string) Str::uuid(),);

Я не дозволяю клієнту довільно вибрати будь-який ключ S3.

Саме застосунок вирішує, у яку область сховища можна записувати файл і як сформувати безпечне ім’я.

Це не дає клієнту перезаписати чужий об’єкт або вибрати шлях поза дозволеною областю.

Дозвіл на завантаження створюю лише після перевірки права

Спочатку перевірка права
$this->authorize(    'uploadVideo',    $resource,); $path = $pathFactory->forVideo(    $resource,); [    'url' => $url,    'headers' => $headers,] = Storage::disk('s3')    ->temporaryUploadUrl(        $path,        now()->addMinutes(5),

Сам факт входу в систему не означає, що користувач може завантажувати файл у будь-який контекст.

Перед створенням підписаного дозволу застосунок перевіряє саме цю операцію.

Після видачі URL сховище вже не питає Laravel про роль користувача. Воно перевіряє підпис.

До видачі дозволу перевіряю опис майбутнього файла

Перевірка заявлених параметрів
$data = $request->validate([    'name' => [        'required',        'string',        'max:255',    ],     'size' => [        'required',        'integer',        'min:1',        'max:524288000',    ],     'type' => [        'required',

До завантаження сервер ще не має самого файла, але клієнт може передати його назву, заявлений розмір і тип.

Ці дані допомагають відхилити очевидно недозволену операцію ще до створення підписаного URL.

Але даним браузера не можна довіряти як остаточному доказу. Після завантаження об’єкт потрібно перевірити ще раз.

Після цього браузер передає файл прямо у сховище

Великий файл більше не проходить через PHP
const response = await fetch(    upload.url,    {        method: 'PUT',        headers: upload.headers,        body: file,    },) if (!response.ok) {    throw new Error(        'Upload failed',    )}

У цьому запиті тіло з великим відео йде від браузера безпосередньо до S3-сумісного сховища.

PHP worker уже не чекає, доки сотні мегабайт пройдуть через застосунок.

Laravel був потрібен для рішення "чи можна завантажувати і куди", а не для пересилання самих байтів.

Пряме завантаження не означає публічний файл

Я продовжую використовувати приватну модель S3-сховища.

Тимчасовий дозвіл на запис одного об’єкта не означає, що цей об’єкт після завантаження має отримати постійний публічний URL.

Після запису файл залишається приватним, а для читання можна окремо створювати короткоживучий доступ.

Тимчасовий URL має давати вузьке право, а не доступ до всього бакета

Одна операція над одним об’єктом
Allowed: PUTprivate/videos/7fd2...mp4  Not granted: list bucketdelete another fileread another filewrite arbitrary pathpermanent credentials

Для прямого завантаження браузеру потрібні правильні CORS і адреса сховища

Серверна адреса не завжди є браузерною адресою
Laravel container: http://minio:9000        |        vworks  Browser: http://minio:9000        |        XDocker service name may not resolve

При прямому завантаженні браузер звертається безпосередньо до HTTP endpoint сховища, тому CORS стає частиною робочого сценарію.

Я дозволяю лише потрібні джерела, методи та заголовки.

Окремо перевіряю адресу: Laravel усередині Docker може бачити minio:9000, але браузер користувача не працює у внутрішній мережі Docker.

Успішний PUT ще не завершує прикладну операцію

Після запису потрібне підтвердження застосунку
Browser   |   | PUT succeeds   vS3  then  Browser   |   | upload completed   vLaravel   |

Сховище може успішно прийняти файл, але застосунок ще має дізнатися про завершення операції.

Я не створюю остаточний стан готового файла лише в момент видачі URL, бо користувач може так і не виконати завантаження.

Після успішного PUT клієнт повідомляє застосунок про завершення, а сервер перевіряє об’єкт.

Після завантаження перевіряю реальний об’єкт у сховищі

Перевірка після прямого завантаження
$disk = Storage::disk('s3'); if (! $disk->exists($path)) {    throw ValidationException::withMessages([        'file' =>            'Uploaded object was not found.',    ]);} $size = $disk->size(    $path,); $mimeType = $disk->mimeType(    $path,);

До видачі URL я мала лише заявлені клієнтом параметри.

Після завантаження вже можна перевірити, чи об’єкт існує, який має фактичний розмір і який тип визначає сховище.

Лише після цієї перевірки я переводжу прикладний запис у стан готового файла.

Між дозволом і завершенням існує проміжний стан

Видача URL ще не означає готовий файл
requested    |    vupload permission issued    |    vpending upload    |    +---- abandoned    |    vobject uploaded    |    vserver verification    |

Користувач може отримати дозвіл, закрити вкладку, втратити мережу або перервати завантаження.

Тому проміжний запис не повинен безкінечно виглядати як готовий файл.

Для таких станів потрібне обмеження строку життя або періодичне очищення.

Якщо у сховищі з’явилися об’єкти, які так і не були підтверджені застосунком, їх також потрібно мати можливість безпечно прибрати.

Під час заміни старий файл видаляю тільки після успішної зміни

Не видаляю робочий файл до підтвердження нового
Old file   |   | keep   v New direct upload   |   vverify new object   |   vupdate application state   |   vremove old object

Якщо це заміна вже існуючого файла, старий об’єкт не варто видаляти в момент видачі нового URL.

Нове завантаження ще може не відбутися.

Я спочатку підтверджую новий об’єкт, змінюю стан застосунку і лише після успішної зміни очищаю попередній файл.

Пряме завантаження не забирає відповідальність у Laravel

Ми прибираємо PHP з потоку даних, а не з правил системи
Laravel still owns: - authentication- authorization- allowed file purpose- object path- declared limits- short permission lifetime- final verification- database state- replacement and cleanup rules  Storage owns: - receiving file bytes

Не кожен файл потребує прямого завантаження

Я не використовую пряме завантаження лише тому, що воно технічно можливе.

Для маленьких аватарів або простих вкладень звичайне завантаження через PHP може залишатися значно простішим.

Найбільшу користь прямий сценарій дає там, де файли великі, завантаження довге або не хочеться тримати PHP між браузером і сховищем протягом усього передавання.

Що я перевіряю для прямого S3-завантаження

Мій короткий список:

  • Чи операція дозволена цьому користувачу?
  • Чи шлях об’єкта створює сервер?
  • Чи заявлений розмір і тип файла перевіряються до видачі дозволу?
  • Чи temporaryUploadUrl() має короткий строк дії?
  • Чи браузер передає всі підписані заголовки?
  • Чи CORS дозволяє лише потрібний сценарій?
  • Чи адреса сховища доступна браузеру, а не лише Docker-контейнеру?
  • Чи файл залишається приватним після завантаження?
  • Чи застосунок перевіряє реальний об’єкт після завершення?
  • Чи незавершені завантаження можна очистити?
  • Чи старий файл не видаляється до успішної заміни?

PHP має контролювати операцію, а не переносити кожен байт

Для великих файлів мені подобається розділяти керування і передавання даних.

Laravel автентифікує користувача, перевіряє право, визначає шлях, перевіряє заявлені параметри й видає короткоживучий дозвіл.

Після цього браузер передає великий файл безпосередньо в S3-сумісне сховище.

Коли завантаження завершене, Laravel знову включається: перевіряє реальний об’єкт і фіксує остаточний стан.

Так PHP залишається власником правил системи, але перестає бути транспортом для сотень мегабайт даних.

Пряме завантаження не обходить застосунок. Воно лише прибирає застосунок із того місця, де йому не обов’язково стояти між браузером і сховищем.

Laravel вирішує, чи можна завантажити файл і куди. Сам великий файл браузер може передати безпосередньо у сховище.