Перенесення мільйонів файлів з одного сховища в інше — це той етап міграції, на який зазвичай не закладають бюджет. Масштабний aws s3 sync між провайдерами триває днями, коштує чималих грошей за трафік і копіює все підряд: від застарілих аватарів до забутих експортів. Альтернатива — написання коду, який вручну перевіряє два диски, — засмічує проєкт конструкціями Storage::disk('old'), які потім важко видалити.
Laravel 13.26 представляє драйвер read-through, який бере це «жонлювання» дисками на себе. Ви вказуєте основний (primary) та резервний (fallback) диски: система спочатку шукає файл на основному, а якщо його там немає — звертається до старого сховища і автоматично копіює файл на нове під час читання. «Гарячі» файли мігрують самі при першому запиті, а «холодні» залишаються на місці, доки ви не вирішите, що з ними робити. Драйвер додав @taylorotwell у PR #61140.
# Налаштування диска
Диск read-through створюється на основі двох інших дисків у config/filesystems.php. Просто вкажіть їхні назви:
'disks' => [
'r2' => [
'driver' => 's3',
// Cloudflare R2 credentials...
],
'legacy-s3' => [
'driver' => 's3',
// бакет, з якого ви переходите...
],
'assets' => [
'driver' => 'read-through',
'primary' => 'r2',
'fallback' => 'legacy-s3',
],
],
У коді додатка ви використовуєте Storage::disk('assets') як будь-який інший диск. У цьому і полягає сенс: контролери та джоби навіть не знають про існування двох бакетів. Параметри primary та fallback також приймають масиви з конфігурацією, якщо ви не хочете реєструвати диски окремо. Менеджер валідує пару під час ініціалізації, тому відсутність диска або циклічне посилання на самого себе викличе InvalidArgumentException ще до початку роботи.
# Як розподіляються операції
Варто розібратися з правилами маршрутизації, перш ніж запускати це у продакшн:
- Читання (
get(),readStream()) перевіряє основний диск, а потім резервний. Якщо файл знайдено на резервному, він копіюється на основний, після чого повертається контент. Потокове читання використовуєphp://temp, щоб не завантажувати файл повністю в пам'ять. - Запис, видалення, переміщення та копіювання працюють тільки з
primary. - Список файлів (
files()) підтягується лише зprimary, тому ви побачите лише ті об'єкти, що вже були перенесені або створені після перемикання. - Перевірка наявності та метадані (
exists(),size(),mimeType(),lastModified(),url(),temporaryUrl()) звертаються до того диска, де лежить файл, не ініціюючи копіювання.
Два з цих правил мають свої нюанси. Списки файлів відображають лише основний диск, тому будь-яка логіка, що перебирає директорію, не побачить файлів на fallback. А видалення стосується лише primary: якщо ви видалите файл, який все ще є на fallback, він «воскресне» при наступному читанні. Під час міграції це зазвичай не проблема, оскільки старий диск згодом буде видалено, але поведінка, коли delete() повертає true, а наступний exists() теж true, може здивувати.
Перенесення файлів працює за принципом «найкращих зусиль». Якщо копіювання на основний диск не вдалося, читання з резервного все одно спрацює, а помилка буде пригнічена. Логіка в тому, що переповнений новий диск не має зупиняти роздачу файлів. Якщо ви хочете знати про такі помилки негайно, встановіть 'throw_on_promotion_failure' => true.
# Читання без копіювання
Іноді потрібно використовувати два диски без міграції. @jimbojsb у PR #61155 додав опцію copy саме для таких випадків:
'assets' => [
'driver' => 'read-through',
'primary' => 'local-assets',
'fallback' => 'production-s3',
'copy' => false,
],
З copy => false дані з резервного диска просто віддаються користувачу без копіювання. Це зручно для локальної розробки з дампом бази даних із продакшну: записи посилаються на файли, які існують лише в хмарі. Таке налаштування дозволяє відображати їх локально, не забиваючи пам'ять ноутбука. Також це підходить для першої фази міграції, щоб перевірити стабільність читання перед початком масового копіювання.
# План міграції
Підсумовуючи, переїзд зі старого S3 на R2 виглядає так:
- Створюєте новий бакет і додаєте його конфігурацію поряд зі старим.
- Змінюєте диск, який уже використовує ваш додаток (наприклад,
assets), на паруread-through: новий бакет якprimary, старий якfallback. Тепер нові завантаження йдуть у R2, а старі файли мігрують самі при зверненні. - Коли основний трафік пройде через систему, більшість затребуваних файлів уже буде на новому місці. Решту можна перенести разовою синхронізацією або фоновим завданням.
- Замінюєте конфіг
read-throughна звичайний диск, що вказує на новий бакет, і видаляєте старий.
Між 2 та 4 кроками немає моменту ризикованого перемикання всього трафіку відразу. Відкат — це просто зміна конфігурації, адже дані на старому бакеті залишалися цілісними протягом усього процесу.
# Що ще почитати
- Нотатки до релізу Laravel 13.26, де цей драйвер з’явився разом із debounced listeners та
Queue::forward(). - Використання AWS S3 у Laravel — база, на якій побудовані ці приклади.