Skip to content

Instantly share code, notes, and snippets.

@fideloper
Last active January 17, 2024 18:41
Show Gist options
  • Star 73 You must be signed in to star a gist
  • Fork 20 You must be signed in to fork a gist
  • Save fideloper/6ada632650d8677ba23963ab4eae6b48 to your computer and use it in GitHub Desktop.
Save fideloper/6ada632650d8677ba23963ab4eae6b48 to your computer and use it in GitHub Desktop.
Stream file from S3 to browser, assume Laravel Filesystem usage
<?php
/*************************************************************************
* Get File Information
*/
// Assuming these come from some data source in your app
$s3FileKey = 's3/key/path/to/file.ext';
$fileName = 'file.ext';
// Create temporary download link and redirect
$adapter = Storage::disk('s3')->getAdapter();
$client = $adapter->getClient();
$client->registerStreamWrapper();
$object = $client->headObject([
'Bucket' => $adapter->getBucket(),
'Key' => /*$adapter->getPathPrefix() . */$s3FileKey,
]);
/*************************************************************************
* Set headers to allow browser to force a download
*/
header('Last-Modified: '.$object['LastModified']);
// header('Etag: '.$object['ETag']); # We are not implementing validation caching here, but we could!
header('Accept-Ranges: '.$object['AcceptRanges']);
header('Content-Length: '.$object['ContentLength']);
header('Content-Type: '.$object['ContentType']);
header('Content-Disposition: attachment; filename='.$fileName);
/*************************************************************************
* Stream file to the browser
*/
// Open a stream in read-only mode
if (!($stream = fopen("s3://{$adapter->getBucket()}/{$s3FileKey}", 'r'))) {
throw new \Exception('Could not open stream for reading file: ['.$s3FileKey.']');
}
// Check if the stream has more data to read
while (!feof($stream)) {
// Read 1024 bytes from the stream
echo fread($stream, 1024);
}
// Be sure to close the stream resource when you're done with it
fclose($stream);
@fideloper
Copy link
Author

Reference:

Docs on HeadObject S3 API call:

http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#headobject

Example streaming file and explanation on "$client->registerStreamWrapper()" method:

https://aws.amazon.com/blogs/developer/amazon-s3-php-stream-wrapper/

Force file download via Content-Disposition header:

https://stackoverflow.com/questions/8485886/force-file-download-with-php-using-header

@kingsloi
Copy link

Hey @fideloper, I bought your Scaling Laravel course and slowly but surely implementing all that good stuff into my app! I've just implemented your S3FileStream class, but not 100% sure how to test (or specially how to mock S3). Can you share any ideas your have re: testing against that class?

@skmetaly
Copy link

skmetaly commented Apr 18, 2018

hey @fideloper . There's an extra paranthesis at the end of

header('Content-Disposition: attachment; filename='.$fileName));

causing a syntax error

Fideloper Update: Fixed that, thanks!

@robfrancken
Copy link

@fideloper thanks so much, saved me hours.

@nwaughachukwuma
Copy link

@fideloper thanks for this

@ob1y2k
Copy link

ob1y2k commented Sep 30, 2020

this is such a good script, works with Lumen (https://gist.github.com/digitalkreativ/17cd94db914e6cb21b5dd9e675dd9abe) , while S3FileStream.php does not (many classes missing)

tnx for sharing

@lovecoding-git
Copy link

lovecoding-git commented Nov 3, 2020

Why is this being downloaded for me instead of being displayed?

@rodrigopedra
Copy link

@lovecoding-git comment this line:

header('Content-Disposition: attachment; filename='.$fileName);

This header tells the browser to force download instead of just displaying when the it knows the content type.

@CBox
Copy link

CBox commented Nov 28, 2020

Why not to use Storage::disk('s3')->readStream($path), it's not the same?

@justsanjit
Copy link

justsanjit commented Jan 2, 2022

Now you can just use

Storage::disk('s3')->download($path);

@reppair
Copy link

reppair commented Sep 4, 2023

@justsanjit no you can't, this will first download the entire file from s3, then serve it back as a response, it seems. What I am sure of, once you send the get request it will wait and wait for response, then give you the file at once (if it doesn't timeout). The idea here is to force the browser to start a download and stream content from s3, trough the server, to the client as the client is first asked where to store the file, then observe the download progress like downloading any other public file from the server.

@justsanjit
Copy link

@reppair
Copy link

reppair commented Sep 4, 2023

Laravel Framework 10.20.0

Thing is, the s3 files are private. Can't be public. I need to authorize the downloads on the server and stream them back. I've been digging into this for hours now, nothing seems to do an actual stream from s3, trough the server back to the browser apart from this:

$size = Storage::disk('s3')->size($path);

$fileUrl = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(3));

$headers = [
    'Content-Type' => 'audio/wav',
    'Content-Length' => $size,
];

return response()->streamDownload(function () use ($fileUrl, $filename, $size) {
    if (! ($stream = fopen($fileUrl, 'r'))) {
        throw new \Exception("'Could not open stream for reading file: ['.$filename.']'");
    }

    while (! feof($stream)) {
        echo fread($stream, 1024);
    }

    fclose($stream);
}, $filename, $headers);

As you can see, the problem here is, I am creating a publicly accessible URL to the file. Which I want to avoid.

@reppair
Copy link

reppair commented Sep 5, 2023

Here is how I've done it without creating temporary public URL with the S3 custom stream wrapper as outline here.

Edit: Later realized I've done basically the same as show in this gist, just overlooked it at first. 😸

$size = Storage::disk('s3')->size($path);

$client = Storage::disk('s3')->getClient();

$client->registerStreamWrapper();

$buket = config('filesystems.disks.s3.bucket');

$fileUrl = "s3://$buket/$path";

$headers = [
    'Content-Type' => 'audio/wav',
    'Content-Length' => $size,
];

return response()->streamDownload(function () use ($fileUrl, $filename) {
    if (! ($stream = fopen($fileUrl, 'r'))) {
        throw new \Exception("'Could not open stream for reading file: ['.$filename.']'");
    }

    while (! feof($stream)) {
        echo fread($stream, 1024);
    }

    fclose($stream);
}, $filename, $headers);

@localpath
Copy link

I think this does a streamed response right?

return Storage::disk('s3.protected')->response($document->url);

or download response

return Storage::disk('s3.protected')->download($document->url);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment