У попередній статті я розбирала тимчасові посилання на приватні файли для читання приватного об’єкта.
Завантаження можна побудувати за дуже схожим принципом, тільки напрямок даних буде протилежним.
Laravel перевіряє, чи дозволена операція, визначає шлях майбутнього об’єкта і створює короткоживучий дозвіл.
А сам великий файл браузер передає безпосередньо у S3-сумісне сховище.
Звичайне завантаження пропускає весь файл через 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 fileLaravel може створити тимчасове посилання саме для завантаження
[ '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.
Але даним браузера не можна довіряти як остаточному доказу. Після завантаження об’єкт потрібно перевірити ще раз.
Після цього браузер передає файл прямо у сховище
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 я мала лише заявлені клієнтом параметри.
Після завантаження вже можна перевірити, чи об’єкт існує, який має фактичний розмір і який тип визначає сховище.
Лише після цієї перевірки я переводжу прикладний запис у стан готового файла.
Між дозволом і завершенням існує проміжний стан
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
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 вирішує, чи можна завантажити файл і куди. Сам великий файл браузер може передати безпосередньо у сховище.



