Skip to content
LaravelBlog

livewire/livewire: Reverted: same-disk move optimization in `storeAs()` (PR #10390)

LaravelBlogBot
LaravelBlogBot

Contributed by joshhanley

Part of the livewire v4.4.7 release

Categories: Fix (3), Upgrade notes (11)

Pull request #10390 reverts #9772 ("[4.x] Move temporary file if disks are same"), an optimization that made Livewire's TemporaryUploadedFile::storeAs() move — rather than copy — a temporary upload whenever the source and destination disks matched. That optimization changed behavior away from Laravel's own UploadedFile::storeAs() contract, which always copies the file and never touches the source. Once the temporary file had been moved, any later code reading from it — getSize(), getMimeType(), and so on — would throw. This revert removes the optimization entirely and restores the always-copy behavior on every disk. If you are upgrading from v3.8.10, the net effect is that storeAs() once again behaves exactly as it did before the optimization ever existed.

What changed

The heart of the revert is in src/Features/SupportFileUploads/TemporaryUploadedFile.php. The same-disk fast path introduced by #9772 is gone:

// Before (#9772): same disk and no extra options — move instead of copy.
if ($this->disk === $disk && empty($options)) {
    Storage::disk($disk)->move($this->path, $newPath);
} else {
    Storage::disk($disk)->put(
        $newPath, $this->storage->readStream($this->path), $options
    );
}

// After (this revert): always copy, on every disk.
Storage::disk($disk)->put(
    $newPath, $this->storage->readStream($this->path), $options
);

With the branch removed, every storeAs() call streams the temporary file into the destination disk, leaving the temporary upload in place. The PR also deletes the tests that asserted the move behavior (test_same_disk_store_moves_file_instead_of_copying and test_different_disk_store_copies_file) along with the FileUploadSameDiskComponent fixture, so the test suite reflects the restored contract.

Why the optimization broke the contract

Laravel's UploadedFile::storeAs() copies the file to the target disk and leaves the source untouched — calling code is allowed to keep reading the original afterwards. Livewire's temporary uploaded file wraps that API and must honor the same contract. The move optimization violated it: after a same-disk storeAs(), the temporary file was gone, and any subsequent read threw an exception (reported in #10238).

The kind of code that broke:

class AvatarUpload extends Component
{
    use WithFileUploads;

    public $photo;

    public function save()
    {
        $path = $this->photo->storeAs('avatars', 'avatar.png', 'public');

        // Reading metadata from the temporary upload after storing
        // no longer throws — storeAs() never consumes the source.
        logger()->info('avatar uploaded', [
            'path' => $path,
            'size' => $this->photo->getSize(),   // threw under #9772
            'mime' => $this->photo->getMimeType(),
        ]);
    }
}

Why the proposed fix wasn't good enough

PR #10309 proposed repairing the optimization by falling back to the file's cached .json metadata whenever those accessors were called on a moved file. That sounds reasonable, but that metadata sidecar is deliberately never written for S3 uploads — doing so would require an extra signed upload for every single file. The fallback would therefore only work on non-S3 disks. The result would be code that works locally in development and still breaks in production for anyone using S3. That is worse than the current, consistent failure, because it would not be caught before deploying.

No moveAs() — for now

Adding an explicit moveAs()-style method was considered as a way to preserve the performance win without breaking the contract. But Laravel has no precedent for such an API, and inventing one inside Livewire's file-upload layer was judged to be too much. After a lot of thought and digging, the straight revert was chosen instead.

Upgrade impact from v3.8.10

  • Coming straight from v3.8.10: the optimization only ever shipped on the 4.x branch via #9772, so you likely never encountered it. After this revert, storeAs() matches the behavior you already knew — no action required.
  • If you upgraded to an affected 4.x release and worked around the bug (for example, by reading getSize() or getMimeType() before storing, or avoiding follow-up reads entirely), those workarounds are no longer necessary. The temporary upload survives storeAs(), so you can read its metadata afterwards again.
  • Performance: same-disk stores copy again, so large files pay the extra I/O that #9772 briefly eliminated. If you genuinely need move semantics for a specific upload, you can call Storage::disk($disk)->move($from, $to) yourself — just be aware you will be consuming the temp file yourself and stepping outside the guaranteed contract.

Takeaways

  • TemporaryUploadedFile::storeAs() copies on every disk again; it never consumes the temporary upload.
  • Reading getSize(), getMimeType(), and similar accessors after storing works once more, fixing the regression reported in #10238.
  • The same-disk move optimization (#9772) is fully reverted, including its tests; no moveAs()-style API was added.
  • A metadata-fallback fix (#10309) was rejected because the cached .json sidecar is never written for S3, which would have made failures inconsistent between local development and production.

Sources

More from this release

Related Articles