Laravel
S3
AWS
File-Download
PHP

Laravel Download from S3 To Local

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Downloading a file from Amazon S3 to local storage in Laravel is usually a server-to-server copy, not a browser download. The cleanest implementation depends on file size: for small files you can read the content directly, while for larger files you should stream from the S3 disk to the local disk to avoid loading the whole file into memory.

Configure The S3 Disk First

Laravel's filesystem layer wraps S3 through a storage disk. Once the s3 disk is configured in config/filesystems.php and the AWS credentials are present, the code path is straightforward.

A typical environment setup looks like this:

env
1AWS_ACCESS_KEY_ID=your-key
2AWS_SECRET_ACCESS_KEY=your-secret
3AWS_DEFAULT_REGION=us-east-1
4AWS_BUCKET=your-bucket
5AWS_URL=
6AWS_ENDPOINT=
7AWS_USE_PATH_STYLE_ENDPOINT=false

In application code, you usually interact through Storage::disk('s3').

Small Files: Read Then Write

For small files, the easiest approach is to read the object into memory and write it to the local disk.

php
1<?php
2
3use Illuminate\Support\Facades\Storage;
4
5$s3Path = 'reports/daily.csv';
6$localPath = 'downloads/daily.csv';
7
8$content = Storage::disk('s3')->get($s3Path);
9Storage::disk('local')->put($localPath, $content);

This is concise and works well when the file is modest in size. The drawback is obvious: the entire file content is held in PHP memory.

Large Files: Stream From S3 To Local

For larger files, use streams. Laravel's filesystem API supports reading a stream from one disk and writing it to another.

php
1<?php
2
3use Illuminate\Support\Facades\Storage;
4
5$s3Path = 'exports/big-report.zip';
6$localPath = 'downloads/big-report.zip';
7
8$stream = Storage::disk('s3')->readStream($s3Path);
9
10if ($stream === false) {
11    throw new RuntimeException('Unable to open S3 stream.');
12}
13
14try {
15    Storage::disk('local')->writeStream($localPath, $stream);
16} finally {
17    if (is_resource($stream)) {
18        fclose($stream);
19    }
20}

This pattern is usually the right answer for backups, large media files, exports, and batch jobs. Memory stays predictable because the file is copied as a stream.

Download To A Specific Absolute Path

Sometimes you do not want Laravel's local disk root. You want an exact path on the server, such as a temporary directory.

php
1<?php
2
3use Illuminate\Support\Facades\Storage;
4
5$s3Path = 'images/logo.png';
6$absoluteLocalPath = storage_path('app/tmp/logo.png');
7
8$stream = Storage::disk('s3')->readStream($s3Path);
9
10if ($stream === false) {
11    throw new RuntimeException('Unable to open S3 stream.');
12}
13
14try {
15    $target = fopen($absoluteLocalPath, 'w');
16    stream_copy_to_stream($stream, $target);
17    fclose($target);
18} finally {
19    if (is_resource($stream)) {
20        fclose($stream);
21    }
22}

This is useful when another library expects a real file path instead of a Laravel disk path.

Do Not Confuse This With Browser Downloads

Laravel also has patterns such as Storage::disk('s3')->download(...) or returning a response download. Those are for sending a file to the client browser. They do not copy the object into local server storage.

That distinction matters:

  • S3 to local disk on the server: use get, readStream, put, or writeStream
  • S3 to end user browser: use a response-based download flow

Add Error Handling And Existence Checks

In production code, check that the source object exists and handle failures explicitly.

php
1<?php
2
3use Illuminate\Support\Facades\Storage;
4
5$s3 = Storage::disk('s3');
6$local = Storage::disk('local');
7$s3Path = 'reports/monthly.csv';
8$localPath = 'downloads/monthly.csv';
9
10if (! $s3->exists($s3Path)) {
11    throw new RuntimeException('Source file does not exist in S3.');
12}
13
14$content = $s3->get($s3Path);
15$local->put($localPath, $content);

For queued jobs or scheduled syncs, log both the source path and destination path so failed copies are easier to diagnose.

When Queues Make Sense

If the copy is large or part of a batch import pipeline, put the work on a queue instead of handling it inline during a web request. The filesystem code stays the same, but the user experience improves because the request does not sit open waiting for a multi-second or multi-minute transfer.

Common Pitfalls

  • Using get() for large files and exhausting PHP memory. Use streams instead.
  • Confusing a server-side copy with a browser download response.
  • Forgetting to close streams after writeStream or stream_copy_to_stream.
  • Writing to a local path that does not exist or is not writable by the application.
  • Assuming S3 read permissions are enough when the local destination directory is not writable.

Summary

  • In Laravel, S3-to-local copy is usually done through the filesystem disks.
  • Use get() and put() for small files.
  • Use readStream() and writeStream() for large files.
  • Response downloads are for browsers, not for server-side file copies.
  • Add existence checks, error handling, and queues when the copy is part of production workflow.

Course illustration
Course illustration

All Rights Reserved.