使用Laravel从Web服务器流式传输Amazon S3对象

时间:2015-07-28 19:19:04

标签: laravel amazon-s3 laravel-5 flysystem

在使用laravel 5.1构建的网络应用程序中,用户可以上传我存储在Amazon S3中的一些敏感文件。后来我希望用户 WITH PERMISSION 下载此文件。由于我希望进行此身份验证检查,因此无法使用传统方法下载该文件,只需在S3中直接链接到文件即可。

我的方法:

  1. 当用户请求下载时,我的服务器会在本地下载文件,然后流式传输给用户。 问题:需要花费很长时间,因为文件太大了。

  2. 为用户提供预先签名的URL,以便直接从S3下载。 URL仅在5分钟内有效。 问题:如果该网址已共享,则任何人都可以在5分钟内下载该网址。

  3. According to this article,将数据直接从S3流式传输到客户端。这看起来很有希望,但我不知道如何实现这一点。

  4. 根据这篇文章,我需要:

    1. 注册流包装 - 这是我的第一个问题,因为我不知道如何获取S3Client对象,因为laravel使用了flysystem,我不知道是什么方法调用来获取此对象。也许我需要在我的composer.json中单独包含S3包?
    2. 禁用输出缓冲 - 我是否需要在laravel中执行此操作或laravel是否已经处理过它?
    3. 我相信其他开发者之前已经看过这样的问题,并希望得到一些帮助指针。如果有人已经使用laravel Response::download($pathToFile, $name, $headers)直接从S3流向客户端,那么我很想听听你的方法。

1 个答案:

答案 0 :(得分:8)

From discussion in comments, I have arrived at some key points that I would like to share.

Pre-Signed URLs

As @ceejayoz pointed out, pre-signed URLs are not a bad idea because:

  1. I can keep time as low as 10 seconds which is perfect for any redirects and to start download, but not enough for the link to be shared.
  2. My previous understanding was that the download has to finish in the given time. So if the link expires in 10 seconds, the download has to happen before that. But @ceejayoz pointed that is not the case. The download which have started is allowed to finish.
  3. With cloud front, I can also restrict on the IP address, to add more security.


IAM Roles

He also pointed out another not so great method - to create temporary IAM users. This is a maintenance nightmare if not done correctly, so only do if you know what you are doing.


Stream From S3

This is the method that I have chosen for now. Maybe later I will move to the first method.

Warning: If you stream, then your server is still the middleman and all the data will go via your server. So if it fails, or is slow, your download will be slow.

My first question was how to register stream wrapper:

Since I am using Laravel and laravel uses flysystem for S3 management, there was no easy way for me to get the S3Client. Hence I added additional package AWS SDK for Laravel in my composer.json

"aws/aws-sdk-php-laravel" : "~3.0"

Then I wrote my code as follows:

class FileDelivery extends Command implements SelfHandling
{
    private $client;
    private $remoteFile;
    private $bucket;

    public function __construct($remoteFile)
    {
        $this->client = AWS::createClient('s3');
        $this->client->registerStreamWrapper();
        $this->bucket = 'mybucket';
        $this->remoteFile = $remoteFile;
    }

    public function handle()
    {
        try
        {
            // First get the meta-data of the object.
            $headers = $this->client->headObject(array(
                'Bucket' => $this->bucket,
                'Key' => $this->remoteFile
            ));

            $headers = $headers['@metadata'];
            if($headers['statusCode'] !== 200)
            {
                throw new S3Exception();
            }
        }
        catch(S3Exception $e)
        {
            return 404;
        }

        // return appropriate headers before the stream starts.
        http_response_code($headers['statusCode']);
        header("Last-Modified: {$headers['headers']['last-modified']}");
        header("ETag: {$headers['headers']['etag']}");
        header("Content-Type: {$headers['headers']['content-type']}");
        header("Content-Length: {$headers['headers']['content-length']}");
        header("Content-Disposition: attachment; filename=\"{$this->filename}\"");

        // Since file sizes can be too large,
        // buffers can suffer because they cannot store huge amounts of data.
        // Thus we disable buffering before stream starts.
        // We also flush anything pending in buffer.
        if(ob_get_level())
        {
            ob_end_flush();
        }
        flush();

        // Start the stream.
        readfile("s3://{$this->bucket}/{$this->remoteFile}");
    }
}

My second question was Do I need to Disable output buffering in laravel?

The answer IMHO is yes. The buffering lets the data flushed immediately from the buffer, resulting in lower memory consumption. Since we are not using any laravel function to offload the data to client, this is not done by laravel and hence needs to be done by us.