Skip to content

Instantly share code, notes, and snippets.

@progress44
Created April 26, 2020 09:47
Show Gist options
  • Save progress44/f5693c99d3d8cdfda260144bf5bc2781 to your computer and use it in GitHub Desktop.
Save progress44/f5693c99d3d8cdfda260144bf5bc2781 to your computer and use it in GitHub Desktop.

Revisions

  1. progress44 created this gist Apr 26, 2020.
    178 changes: 178 additions & 0 deletions S3FileStream.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,178 @@
    <?php

    namespace App\Http\Responses;

    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Storage;

    class S3FileStream
    {
    /**
    * @var \League\Flysystem\AwsS3v3\AwsS3Adapter
    */
    private $adapter;

    /**
    * @var \Aws\S3\S3Client
    */
    private $client;

    /**
    * @var file end byte
    */
    private $end;

    /**
    * @var string
    */
    private $filePath;

    /**
    * @var bool storing if request is a range (or a full file)
    */
    private $isRange = false;

    /**
    * @var length of bytes requested
    */
    private $length;

    /**
    * @var
    */
    private $return_headers = [];

    /**
    * @var file size
    */
    private $size;

    /**
    * @var start byte
    */
    private $start;

    /**
    * S3FileStream constructor.
    * @param string $filePath
    * @param string $adapter
    */
    public function __construct(string $filePath, string $adapter = 's3')
    {
    $this->filePath = $filePath;
    $this->filesystem = Storage::disk($adapter)->getDriver();
    $this->adapter = Storage::disk($adapter)->getAdapter();
    $this->client = $this->adapter->getClient();
    }

    /**
    * Output file to client
    */
    public function output()
    {
    return $this->setHeaders()->stream();
    }

    /**
    * Output headers to client
    * @return $this
    */
    protected function setHeaders()
    {
    $object = $this->client->headObject([
    'Bucket' => $this->adapter->getBucket(),
    'Key' => $this->filePath,
    ]);

    $this->start = 0;
    $this->size = $object['ContentLength'];
    $this->end = $this->size - 1;
    //Set headers
    $this->return_headers = [];
    $this->return_headers['Last-Modified'] = $object['LastModified'];
    $this->return_headers['Accept-Ranges'] = 'bytes';
    //$this->return_headers['Content-Type'] = $object['ContentType'];
    $this->return_headers['Content-Type'] = "video/x-m4v";
    $this->return_headers['Content-Disposition'] = 'inline; filename=' . basename($this->filePath);

    if (!is_null(request()->server('HTTP_RANGE'))) {
    $c_start = $this->start;
    $c_end = $this->end;

    [$_, $range] = explode('=', request()->server('HTTP_RANGE'), 2);
    if (strpos($range, ',') !== false) {
    headers('Content-Range: bytes ' . $this->start . '-' . $this->end . '/' . $this->size);
    return response('416 Requested Range Not Satisfiable', 416);
    }
    if ($range == '-') {
    $c_start = $this->size - substr($range, 1);
    } else {
    $range = explode('-', $range);
    $c_start = $range[0];

    $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
    }
    $c_end = ($c_end > $this->end) ? $this->end : $c_end;
    if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
    headers('Content-Range: bytes ' . $this->start . '-' . $this->end . '/' . $this->size);
    return response('416 Requested Range Not Satisfiable', 416);
    }
    $this->start = $c_start;
    $this->end = $c_end;
    $this->length = $this->end - $this->start + 1;
    $this->return_headers['Content-Length'] = $this->length;
    $this->return_headers['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
    $this->isRange = true;
    } else {
    $this->length = $this->size;
    $this->return_headers['Content-Length'] = $this->length;
    unset($this->return_headers['Content-Range']);
    $this->isRange = false;
    }

    // Safari workaround
    $this->return_headers['If-None-Match'] = 'webkit-no-cache';
    $this->return_headers['Content-Length'] = $this->length;

    return $this;
    }

    /**
    * Stream file to client
    * @throws \Exception
    */
    protected function stream()
    {
    $this->client->registerStreamWrapper();
    // Create a stream context to allow seeking
    $context = stream_context_create([
    's3' => [
    'seekable' => true,
    ],
    ]);
    // Open a stream in read-only mode
    if (!($stream = fopen("s3://{$this->adapter->getBucket()}/{$this->filePath}", 'rb', false, $context))) {
    throw new \Exception('Could not open stream for reading export [' . $this->filePath . ']');
    }
    if (isset($this->start)) {
    fseek($stream, $this->start, SEEK_SET);
    }

    $remaining_bytes = $this->length ?? $this->size;
    $chunk_size = 1024;

    $video = response()->stream(
    function () use ($stream, $remaining_bytes, $chunk_size) {
    while (!feof($stream) && $remaining_bytes > 0) {
    echo fread($stream, $chunk_size);
    $remaining_bytes -= $chunk_size;
    flush();
    }
    fclose($stream);
    },
    ($this->isRange ? 206 : 200),
    $this->return_headers
    );
    return $video;
    }
    }