In Laravel 13, how would a read-through disk let a design-portfolio site move its uploads from the public disk to S3 without downtime, and what does it leave undone?
answer
- driver read-through, primary and fallback
- read misses promote files to primary
- writes and listings hit primary only
- throw_on_promotion_failure defaults to false
- unread files still need a backfill
basics
~20 sA read-through disk (added in Laravel 13.26) reads the primary disk first, falls back to the old disk and copies the file forward. Writes and listings use the primary only, so unread files still need a backfill.
solid answer
~40 sIn Laravel 13.26 and later you define a disk with `'driver' => 'read-through'`, `'primary' => 's3'` and `'fallback' => 'public'`, and point the upload code at it. A read checks the primary; on a miss it reads the fallback and, because `copy` defaults to `true`, writes the file to the primary so the next read is served from S3. A failed promotion still returns the contents unless `throw_on_promotion_failure` is `true`. Writes go to the primary, existence checks look at both, deletes remove from both, and `url()` uses whichever disk currently holds the file. What it leaves undone: files nobody reads stay on the old disk, directory listings only see the primary, and each first read pays for a copy, so you still run a backfill before retiring the fallback.
code
php · 16 lines<?php
use Illuminate\Support\Facades\Storage;
// config/filesystems.php disk: driver read-through, primary s3, fallback public
$disk = Storage::disk('portfolio');
// Served from S3 if present; otherwise read from the public disk
// and copied to S3 before returning.
$bytes = $disk->get('portfolios/9/cover.jpg');
// New uploads go straight to the primary (S3).
$disk->put('portfolios/9/hero.jpg', $newBytes);
// Lists the primary only: unpromoted files are missing here.
$onS3 = $disk->allFiles('portfolios/9');go deeper
Recall that a read-through disk has a primary and a fallback, and that reading an old file copies it to the new disk.
Explain which operations go to the primary, which check both disks, and what the copy and throw_on_promotion_failure options change.
Plan the whole migration: point code at the read-through disk, backfill unread files, handle listings and stored paths, then retire the fallback safely.
Judge whether lazy promotion or a bulk copy with a cutover fits the data volume, cost and risk, and define when the old storage may be removed.
## The problem it solves A design-portfolio site starts with uploads on the `public` disk (`storage/app/public`, exposed by the `storage:link` symlink). When it moves to several servers it needs shared storage, so uploads must end up in an S3 bucket. The naive switch, changing `FILESYSTEM_DISK` or the disk name in code, breaks every existing image the moment it ships, because the old files are not in the bucket yet. A bulk copy first means a window where new uploads land on the old disk while the copy runs. Laravel 13.26 added the **read-through driver** for exactly this: a disk that wraps a **primary** (where files should end up) and a **fallback** (where they are now). ## Configuring it ```php 'portfolio' => [ 'driver' => 'read-through', 'primary' => 's3', 'fallback' => 'public', // 'copy' => true, // 'throw_on_promotion_failure' => false, ], ``` `primary` and `fallback` may be disk names or inline config arrays (built on demand). The manager rejects a missing `primary` or `fallback`, the same disk on both sides, or a disk that references itself, each with an `InvalidArgumentException`. ## How each operation behaves | Operation | Behaviour | |---|---| | read (`get`, `readStream`) | primary if the file is there; otherwise read the fallback and **promote** (copy) it to the primary | | write (`put`, `writeStream`) | primary only | | existence and metadata | either disk; no promotion | | delete | removes from the fallback if present, then from the primary | | directory listing (`files`, `allFiles`) | primary only | | `url()` / `temporaryUrl()` | the disk that currently holds the file | | `temporaryUploadUrl()` | primary | Two options tune promotion: - **`copy`** (default `true`) — set it to `false` to read from the fallback without copying, for example while you only want to measure what is still being read from the old disk. - **`throw_on_promotion_failure`** (default `false`) — by default a failed copy is swallowed and the read still returns the contents; set it to `true` to get an `UnableToReadFile` instead. A promoted stream read is buffered through `php://temp` before it is written to the primary and returned, so the first read of a large file costs a full download from the old disk plus an upload to the new one. ## A no-downtime migration, step by step 1. Install the S3 adapter and configure the `s3` disk. 2. Add the `portfolio` read-through disk and point upload and display code at it; new uploads now land in S3 immediately. 3. Let traffic promote the popular files as they are viewed. 4. Run a **backfill**: a queued command that walks the old disk and streams each file still missing from S3 across (for example with `copyToDisk`, added in 13.32), idempotently and in chunks. 5. Verify counts or checksums, then set `copy` to `false` or remove the fallback and switch the code to the plain `s3` disk. 6. Retire the old folder only after the fallback has stopped serving reads. ## What it leaves undone - **Unread files never move.** Old portfolios nobody opens stay on the fallback until the backfill copies them. - **Listings lie during the migration.** An admin screen built on `allFiles()` sees only the primary, so unpromoted files look missing. - **Stored paths must match.** The fallback and primary are addressed with the same relative path; if the old code stored absolute paths or disk-specific URLs, fix those rows first. - **Links can flip hosts.** `url()` returns an S3 link once a file is promoted and a `/storage/...` link before, so cached HTML may hold the old form for a while. - **First-read latency.** Promotion adds a write to the first request for each file. ## Watching the migration - Count how many files the backfill still finds only on the fallback, so you can see the old disk's share trend towards zero. - Keep the fallback's public URL working (for a `public` fallback, the `storage:link` symlink) until no link handed out earlier still points at it. - Budget for the first-read copies: a sudden burst of views on old portfolios turns into a burst of uploads to the bucket. ## Why it matters in an interview The feature is new, but the question underneath is old: how do you change storage backends while the site keeps serving files? The read-through disk answers the "serve while copying" half; a candidate who also names the backfill, the listing gap and the retirement check has done a real migration.
- What happens on a read if copying the fallback file to the primary fails?By default the read still succeeds: the adapter catches the filesystem exception and returns the contents from the fallback, so users see nothing. Setting `throw_on_promotion_failure` to `true` turns that into an `UnableToReadFile`, which is useful when you would rather notice a broken bucket than keep serving silently from the old disk.
- Why do you still need a backfill job with a read-through disk?Promotion only happens on reads. Files nobody requests during the migration stay on the fallback forever, and directory listings only show the primary. A backfill that walks the old disk and copies whatever is missing is the only way to know the fallback can be retired.
saying these in an interview costs you the question
- A read-through disk copies every fallback file to the primary when the app boots
- Writes to a read-through disk are sent to both the primary and the fallback
- allFiles() on a read-through disk merges the listings of both disks
- A failed promotion makes the read throw unless you disable it
- Once the read-through disk is configured the old disk can be deleted immediately