Skip to content

Instantly share code, notes, and snippets.

@fideloper
Last active November 8, 2024 09:51
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);
@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);

@moerphie
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);

In my case, the download does not start immediately as a streamed response should be.
It first waits until the file has been downloaded from S3 to the local server, and then the output begins.

Any tips?

@localpath
Copy link

localpath commented May 22, 2024

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);

In my case, the download does not start immediately as a streamed response should be. It first waits until the file has been downloaded from S3 to the local server, and then the output begins.

Any tips?

Maybe try this instead @moerphie

Also keep in mind streaming works off buffers in chunks based on byte size so if your file is smaller than the buffer size it'll always appear like that. Test a large file as well. You can probably dig through the sdk for the file driver and find it's streaming functionality

https://laravel.com/docs/11.x/responses#streamed-downloads

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