Работа с файлами: загрузка, Storage, диски local и S3

Рейтинг: 60.1% · 14 голосов
Курс по Laravel: маршруты, Eloquent, Blade, миграции, очереди и API. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
oleg_php
Сообщения: 25
Зарегистрирован: 14 май 2026, 08:06

Работа с файлами: загрузка, Storage, диски local и S3

Сообщение oleg_php »

АкадемияLaravel с нуляГлава 14 из 18
Оглавление курса (18)
  1. Знакомство с Laravel и установка окружения
  2. Маршруты и контроллеры
  3. Blade: шаблоны и вёрстка страниц
  4. Миграции и структура базы данных
  5. Eloquent ORM: модели и CRUD
  6. Связи в Eloquent: hasMany, belongsTo и другие
  7. Формы и валидация данных
  8. Аутентификация пользователей
  9. Middleware и защита маршрутов
  10. Очереди и фоновые задачи
  11. Отправка почты и уведомления
  12. Строим REST API на Laravel
  13. Авторизация: Gates и Policies
  14. Работа с файлами: загрузка, Storage, диски local и S3 (вы здесь)
  15. Тестирование: Pest и PHPUnit, фабрики, сидеры, RefreshDatabase
  16. Сервис-контейнер, провайдеры, свои artisan-команды и планировщик
  17. Деплой в продакшен и обзор современного фронтенда (Vite, Livewire, Inertia)
  18. События и слушатели, кеширование, логирование
В главе 13 мы научились решать, кому что можно. Теперь научим приложение принимать файлы: аватарки, счета в PDF, экспорты отчётов. За хранение в Laravel отвечает фасад Storage поверх библиотеки Flysystem. Идея простая: один API, а где физически лежат файлы (локальный диск или S3-совместимое облако), решает конфиг.

Как устроены диски:

Диски описаны в config/filesystems.php. Из коробки их три: local для приватных файлов (в Laravel 11+ его корень storage/app/private), public для всего, что можно отдавать вебом (storage/app/public), и s3. Диск по умолчанию задаёт переменная FILESYSTEM_DISK в .env.

Чтобы файлы с диска public открывались по адресу вида /storage/avatars/abc.jpg, нужен симлинк из public/storage на storage/app/public:

Код: Выделить всё

php artisan storage:link
Команда выполняется один раз на каждом окружении. Впишите её в деплой-скрипт сразу, об этом ещё вспомним в граблях.

Загрузка из формы:

Форма должна иметь enctype="multipart/form-data", иначе $request->file() вернёт null. Контроллер:

Код: Выделить всё

use Illuminate\Http\Request;
use Illuminate\Validation\Rules\File;

public function store(Request $request)
{
    $request->validate([
        'avatar' => ['required', File::image()->max('2mb')],
    ]);

    $path = $request->file('avatar')->store('avatars', 'public');

    $request->user()->update(['avatar_path' => $path]);

    return back()->with('status', 'Аватар обновлён');
}
store() сам генерирует уникальное хэш-имя и возвращает относительный путь вида avatars/9fK3...jpg. Именно его храним в базе, а URL для шаблона получаем через Storage::url($path). Правило File::image() начиная с Laravel 12 не пропускает SVG по умолчанию, и это правильно: внутри svg может жить скрипт. Если нужно своё имя файла, есть storeAs(), но имя от клиента туда подставлять нельзя, об этом ниже.

Приватные файлы:

Счета и договоры не должны лежать в public. Кладём их на local и отдаём через контроллер с проверкой прав, Policies из прошлой главы здесь и пригодятся:

Код: Выделить всё

use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;

public function download(Invoice $invoice)
{
    Gate::authorize('view', $invoice);

    return Storage::download($invoice->pdf_path, "invoice-{$invoice->number}.pdf");
}
Storage::download() сам выставит правильные заголовки и отдаст файл потоком, память на больших PDF не пострадает.

Подключаем S3:

Код: Выделить всё

composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Драйвер s3 работает не только с Amazon, а с любым S3-совместимым хранилищем: Yandex Object Storage, VK Cloud, Selectel, свой MinIO. Пример настроек для Яндекса:

Код: Выделить всё

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=YCAJE...
AWS_SECRET_ACCESS_KEY=YCM7...
AWS_DEFAULT_REGION=ru-central1
AWS_BUCKET=myapp-prod
AWS_ENDPOINT=https://storage.yandexcloud.net
AWS_USE_PATH_STYLE_ENDPOINT=false
Для MinIO укажите свой endpoint и AWS_USE_PATH_STYLE_ENDPOINT=true. Приватный файл из закрытого бакета наружу отдаём временной ссылкой:

Код: Выделить всё

$url = Storage::disk('s3')->temporaryUrl(
    $invoice->pdf_path,
    now()->addMinutes(15)
);
Главный профит абстракции: код загрузки и скачивания выше не меняется вообще. Поменяли FILESYSTEM_DISK, и те же store() и download() работают с облаком. С Laravel 11 temporaryUrl() умеет и local-диск, так что даже подписанные ссылки переживают переезд.

Типичные грабли:

Имя файла от клиента. getClientOriginalName() может содержать ../, пробелы, кириллицу и вообще что угодно. В файловую систему оно попадать не должно: хэш-имя от store(), а оригинал, если нужен для отображения, в отдельную колонку БД.

Лимиты PHP и nginx. Правило max('2mb') бессильно, если upload_max_filesize=2M в php.ini обрезал запрос раньше. Проверяйте upload_max_filesize, post_max_size и client_max_body_size у nginx. Симптом: валидация ругается, что файл обязателен, хотя его отправляли, или голая 413.

Забытый storage:link. Локально картинки есть, на проде 404. Особенно коварно при деплое в свежесозданную папку релиза: симлинк остался в старой.

file_get_contents() на гигабайтном экспорте съест память воркера. Для больших файлов есть Storage::putFile(), readStream() и writeStream(), они работают потоками.

exists() на S3 это отдельный HTTP-запрос к хранилищу. Дёргать его в цикле по тысяче записей медленно, а у части провайдеров ещё и платно.

Абсолютные пути в БД. Храните относительный путь внутри диска. Запись вида /var/www/app/storage/... превратит переезд на S3 в ручную миграцию по строкам.

Итог:

Storage даёт единый API над local и S3: публичное раздаём через симлинк, приватное через контроллер или временную ссылку, в БД живут только относительные пути. В следующей главе займёмся тестированием, и там Storage::fake() покажет, зачем была нужна вся эта абстракция: тесты загрузки файлов без единой записи на реальный диск.
👍2 ❤️3 🔥 😄 🤔1
✔ Лучший ответ сформирован автоматически — petcheneg
oleg_php писал(а):Правило max('2mb') бессильно, если upload_max_filesize=2M в php.ini обрезал запрос раньше. вот это боль. убил полдня на проде: валидация орёт required, а файл реально уходил с фронта. оказалось nginx молча отдавал 413, а js её глотал. теперь client_max_body_size проверяю первым делом, до всякого дебага laravel
Перейти к ответу →
Аватара пользователя
petcheneg
Сообщения: 1
Зарегистрирован: 16 май 2026, 09:26

Re: Работа с файлами: загрузка, Storage, диски local и S3

Сообщение petcheneg »

✔ Лучший ответ — сформирован автоматически
oleg_php писал(а):Правило max('2mb') бессильно, если upload_max_filesize=2M в php.ini обрезал запрос раньше.
вот это боль. убил полдня на проде: валидация орёт required, а файл реально уходил с фронта. оказалось nginx молча отдавал 413, а js её глотал. теперь client_max_body_size проверяю первым делом, до всякого дебага laravel
👍2 ❤️1 🔥 😄 🤔
Аватара пользователя
pyandy
Сообщения: 1
Зарегистрирован: 01 июн 2026, 04:07

Re: Работа с файлами: загрузка, Storage, диски local и S3

Сообщение pyandy »

на яндекс облаке заводится с полпинка, подтверждаю. только регион ru-central1 не забудьте, иначе ловите SignatureDoesNotMatch и идёте гуглить на час. и ключи делайте сервисному аккаунту с ролью storage.editor, admin туда вешать не надо
👍1 ❤️ 🔥 😄 🤔1
Аватара пользователя
roselin
Сообщения: 2
Зарегистрирован: 08 июн 2026, 07:47

Re: Работа с файлами: загрузка, Storage, диски local и S3

Сообщение roselin »

а что делать на шаред-хостинге, где symlink запрещён? клиентский проект лежит на обычном хостинге за 300р, storage:link падает с ошибкой
👍2 ❤️ 🔥 😄 🤔
Аватара пользователя
weekendbteam
Сообщения: 2
Зарегистрирован: 15 май 2026, 18:09

Re: Работа с файлами: загрузка, Storage, диски local и S3

Сообщение weekendbteam »

svelte42 писал(а):а что делать на шаред-хостинге, где symlink запрещён?
варианта два: либо отдавать всё через роут и response()->file() (медленнее, зато работает везде), либо поменять root у диска public прямо на папку внутри public_path() в filesystems.php. но вообще 2026 год, vps стоит как два кофе, съезжайте с шареда
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Авторизация: Gates и Policies
Следующая глава →
Тестирование: Pest и PHPUnit, фабрики, сидеры, RefreshDatabase

Все главы курса «Laravel с нуля»

Поделиться темой: ✈ Telegram VK

Вернуться в «Laravel с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость