From 14401efb0f6792415f15c92c1db07fe9e25ea466 Mon Sep 17 00:00:00 2001 From: Lukas Stabe Date: Wed, 22 Jan 2020 09:45:41 +0100 Subject: [PATCH] Enable fseek for files in S3 storage Signed-off-by: Lukas Stabe --- .../Files/ObjectStore/S3ObjectTrait.php | 27 +--- .../ObjectStore/S3SeekableReadStream.php | 139 ++++++++++++++++++ 2 files changed, 147 insertions(+), 19 deletions(-) create mode 100644 lib/private/Files/ObjectStore/S3SeekableReadStream.php diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 6188357521b..1e9d095b9eb 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -49,27 +49,16 @@ trait S3ObjectTrait { * @since 7.0.0 */ function readObject($urn) { - $client = $this->getConnection(); - $command = $client->getCommand('GetObject', [ - 'Bucket' => $this->bucket, - 'Key' => $urn + $context = stream_context_create([ + 's3seek' => [ + 'client' => $this->getConnection(), + 'bucket' => $this->bucket, + 'urn' => $urn, + ], ]); - $request = \Aws\serialize($command); - $headers = []; - foreach ($request->getHeaders() as $key => $values) { - foreach ($values as $value) { - $headers[] = "$key: $value"; - } - } - $opts = [ - 'http' => [ - 'protocol_version' => 1.1, - 'header' => $headers - ] - ]; - $context = stream_context_create($opts); - return fopen($request->getUri(), 'r', false, $context); + S3SeekableReadStream::registerIfNeeded(); + return fopen('s3seek://', 'r', false, $context); } /** diff --git a/lib/private/Files/ObjectStore/S3SeekableReadStream.php b/lib/private/Files/ObjectStore/S3SeekableReadStream.php new file mode 100644 index 00000000000..70855537034 --- /dev/null +++ b/lib/private/Files/ObjectStore/S3SeekableReadStream.php @@ -0,0 +1,139 @@ +. + * + */ + +namespace OC\Files\ObjectStore; + +/** + * A stream wrapper that uses http range requests to provide a seekable + * stream of a file in S3 storage. + */ +class S3SeekableReadStream { + private static $registered = false; + + /** + * Registers the stream wrapper using the `s3seek://` url scheme + * $return void + */ + public static function registerIfNeeded() { + if (!self::$registered) { + stream_wrapper_register( + 's3seek', + 'OC\Files\ObjectStore\S3SeekableReadStream' + ); + self::$registered = true; + } + } + + private $client; + private $bucket; + private $urn; + + private $current; + private $offset = 0; + + private function reconnect($range) { + if ($this->current != null) { + fclose($this->current); + } + + $command = $this->client->getCommand('GetObject', [ + 'Bucket' => $this->bucket, + 'Key' => $this->urn, + 'Range' => 'bytes=' . $range, + ]); + $request = \Aws\serialize($command); + $headers = []; + foreach ($request->getHeaders() as $key => $values) { + foreach ($values as $value) { + $headers[] = "$key: $value"; + } + } + $opts = [ + 'http' => [ + 'protocol_version' => 1.1, + 'header' => $headers, + ], + ]; + + $context = stream_context_create($opts); + $this->current = fopen($request->getUri(), 'r', false, $context); + + if ($this->current === false) {return false;} + + $responseHead = stream_get_meta_data($this->current)['wrapper_data']; + $contentRange = array_values(array_filter($responseHead, function ($v) { + return preg_match('#^content-range:#i', $v) === 1; + }))[0]; + + $content = trim(explode(':', $contentRange)[1]); + $range = trim(explode(' ', $content)[1]); + $begin = explode('-', $range)[0]; + $this->offset = intval($begin); + + return true; + } + + function stream_open($path, $mode, $options, &$opened_path) { + $o = stream_context_get_options($this->context)['s3seek']; + $this->bucket = $o['bucket']; + $this->urn = $o['urn']; + $this->client = $o['client']; + + return $this->reconnect('0-'); + } + + function stream_read($count) { + $ret = fread($this->current, $count); + $this->offset += strlen($ret); + return $ret; + } + + function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset === $this->offset) {return true;} + return $this->reconnect($offset . '-'); + case SEEK_CUR: + if ($offset === 0) {return true;} + return $this->reconnect(($this->offset + $offset) . '-'); + case SEEK_END: + return false; + } + return false; + } + + function stream_tell() { + return $this->offset; + } + + function stream_stat() { + return fstat($this->current); + } + + function stream_eof() { + return feof($this->current); + } + + function stream_close() { + fclose($this->current); + } +} -- 2.39.5