diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf25d6c..04ae654b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Unreleased] -- +-- + +## [1.4.0] - 2022-10-25 + +### Changed + +- [#605](https://github.com/owncloud/files_primary_s3/pull/605) - Allow configurable concurrent uploads + +### Fixed + +- [#618](https://github.com/owncloud/files_primary_s3/pull/618) - Fix stream download release + ## [1.3.0] - 2022-08-10 @@ -88,7 +99,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - First marketplace release -[Unreleased]: https://github.com/owncloud/files_primary_s3/compare/v1.3.0...master +[Unreleased]: https://github.com/owncloud/files_primary_s3/compare/v1.4.0...master +[1.4.0]: https://github.com/owncloud/files_primary_s3/compare/v1.3.1...v1.4.0 +[1.3.1]: https://github.com/owncloud/files_primary_s3/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/owncloud/files_primary_s3/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/owncloud/files_primary_s3/compare/v1.1.2...v1.2.0 [1.1.2]: https://github.com/owncloud/files_primary_s3/compare/v1.1.1...v1.1.2 diff --git a/appinfo/info.xml b/appinfo/info.xml index 12e8b5bf..42366b49 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -19,7 +19,7 @@ This extension is the successor of the [ownCloud Objectstore App](https://market https://github.com/owncloud/files_primary_s3/issues https://github.com/owncloud/files_primary_s3.git ownCloud GmbH - 1.3.0 + 1.4.0 https://doc.owncloud.com/server/latest/admin_manual/configuration/files/external_storage/s3_compatible_object_storage_as_primary.html diff --git a/lib/s3storage.php b/lib/s3storage.php index a3baef3f..6fc1cf4e 100644 --- a/lib/s3storage.php +++ b/lib/s3storage.php @@ -31,6 +31,7 @@ use Aws\S3\Exception\S3Exception; use Aws\S3\ObjectUploader; use Aws\S3\S3Client; +use GuzzleHttp\Handler\StreamHandler; use GuzzleHttp\Handler\CurlMultiHandler; use GuzzleHttp\Middleware; use OC\ServiceUnavailableException; @@ -49,6 +50,9 @@ class S3Storage implements IObjectStore, IVersionedObjectStorage { */ private $connection; + /** @var S3Client|null */ + private $downConnection; + /** * @var array */ @@ -88,57 +92,20 @@ protected function init(): void { } $config = $this->params['options']; if ($useGuzzle5) { - /* - * Note: phan runs in CI with the latest core, which has Guzzle7 or later. - * So various things that phan reports for this Guzzle5 code have to be suppressed. - */ - /* @phan-suppress-next-line PhanUndeclaredClassMethod */ - $client = new \GuzzleHttp\Client(['handler' => new \GuzzleHttp\Ring\Client\CurlMultiHandler()]); - /* @phan-suppress-next-line PhanDeprecatedFunction */ - $emitter = $client->getEmitter(); - /* @phan-suppress-next-line PhanUndeclaredTypeParameter */ - $emitter->on('before', static function (\GuzzleHttp\Event\BeforeEvent $event) { - /* @phan-suppress-next-line PhanUndeclaredClassMethod */ - $request = $event->getRequest(); - if ($request->getMethod() !== 'PUT') { - return; - } - $body = $request->getBody(); - if ($body !== null && $body->getSize() !== 0) { - return; - } - if ($request->hasHeader('Content-Length')) { - return; - } - // force content length header on empty body - $request->setHeader('Content-Length', '0'); - }); - $h = new \Aws\Handler\GuzzleV5\GuzzleHandler($client); + $h = $this->getHandlerV5(false); // curlMultiHandler + $dh = $this->getHandlerV5(true); // streamHandler for downloads } else { - // Create a handler stack that has all of the default middlewares attached - $handler = \GuzzleHttp\HandlerStack::create(new CurlMultiHandler()); - // Push the handler onto the handler stack - $handler->push(Middleware::mapRequest(function (RequestInterface $request) { - if ($request->getMethod() !== 'PUT') { - return $request; - } - $body = $request->getBody(); - if ($body !== null && $body->getSize() !== 0) { - return $request; - } - if ($request->hasHeader('Content-Length')) { - return $request; - } - // force content length header on empty body - return $request->withHeader('Content-Length', '0'); - })); - // Inject the handler into the client - $client = new \GuzzleHttp\Client(['handler' => $handler]); - $h = new GuzzleHandler($client); + $h = $this->getHandlerV7(false); // curlMultiHandler + $dh = $this->getHandlerV7(true); // streamHandler for downloads } $config['http_handler'] = $h; /* @phan-suppress-next-line PhanDeprecatedFunction */ $this->connection = S3Client::factory($config); + + // replace the http_handler for the download connection + $config['http_handler'] = $dh; + /* @phan-suppress-next-line PhanDeprecatedFunction */ + $this->downConnection = S3Client::factory($config); try { $this->connection->listBuckets(); } catch (S3Exception $exception) { @@ -156,6 +123,73 @@ protected function init(): void { } } + private function getHandlerV5($isStream) { + /* + * Note: phan runs in CI with the latest core, which has Guzzle7 or later. + * So various things that phan reports for this Guzzle5 code have to be suppressed. + */ + if ($isStream) { + /* @phan-suppress-next-line PhanUndeclaredClassMethod */ + $client = new \GuzzleHttp\Client(['handler' => new \GuzzleHttp\Ring\Client\StreamHandler()]); + } else { + /* @phan-suppress-next-line PhanUndeclaredClassMethod */ + $client = new \GuzzleHttp\Client(['handler' => new \GuzzleHttp\Ring\Client\CurlMultiHandler()]); + } + + /* @phan-suppress-next-line PhanDeprecatedFunction */ + $emitter = $client->getEmitter(); + /* @phan-suppress-next-line PhanUndeclaredTypeParameter */ + $beforeEventFunc = static function (\GuzzleHttp\Event\BeforeEvent $event) { + /* @phan-suppress-next-line PhanUndeclaredClassMethod */ + $request = $event->getRequest(); + if ($request->getMethod() !== 'PUT') { + return; + } + $body = $request->getBody(); + if ($body !== null && $body->getSize() !== 0) { + return; + } + if ($request->hasHeader('Content-Length')) { + return; + } + // force content length header on empty body + $request->setHeader('Content-Length', '0'); + }; + $emitter->on('before', $beforeEventFunc); + $h = new \Aws\Handler\GuzzleV5\GuzzleHandler($client); + return $h; + } + + private function getHandlerV7($isStream) { + // Create a handler stack that has all of the default middlewares attached + if ($isStream) { + $handler = \GuzzleHttp\HandlerStack::create(new StreamHandler()); + } else { + $handler = \GuzzleHttp\HandlerStack::create(new CurlMultiHandler()); + } + + $requestFunc = function (RequestInterface $request) { + if ($request->getMethod() !== 'PUT') { + return $request; + } + $body = $request->getBody(); + if ($body !== null && $body->getSize() !== 0) { + return $request; + } + if ($request->hasHeader('Content-Length')) { + return $request; + } + // force content length header on empty body + return $request->withHeader('Content-Length', '0'); + }; + // Push the handler onto the handler stack + $handler->push(Middleware::mapRequest($requestFunc)); + // Inject the handler into the client + $client = new \GuzzleHttp\Client(['handler' => $handler]); + $h = new GuzzleHandler($client); + return $h; + } + /** * {@inheritDoc} */ @@ -228,7 +262,7 @@ public function readObject($urn) { $this->init(); try { $context = stream_context_create([ - 's3' => ['seekable' => true] + 's3' => ['seekable' => true, 'client' => $this->downConnection] ]); return \fopen($this->getUrl($urn), 'rb', false, $context); } catch (AwsException $ex) { diff --git a/phpstan.neon b/phpstan.neon index 3a914e2b..74481873 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,7 @@ parameters: - '#Method OCA\\Files_Primary_S3\\Panels\\Admin::getPanel\(\) should return OCP\\AppFramework\\Http\\TemplateResponse|OCP\\Template but returns null.#' - '#Comparison operation ">=" between 7 and 7 is always true.#' - '#Call to an undefined method GuzzleHttp\\Client::getEmitter\(\).#' + - '#Instantiated class GuzzleHttp\\Ring\\Client\\StreamHandler not found.#' - '#Instantiated class GuzzleHttp\\Ring\\Client\\CurlMultiHandler not found.#' - '#Parameter \$event of anonymous function has invalid type GuzzleHttp\\Event\\BeforeEvent.#' - '#Call to method getRequest\(\) on an unknown class GuzzleHttp\\Event\\BeforeEvent.#' diff --git a/sonar-project.properties b/sonar-project.properties index 161c4dbc..3fb0f47c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ # Organization and project keys are displayed in the right sidebar of the project homepage sonar.organization=owncloud-1 sonar.projectKey=owncloud_files_primary_s3 -sonar.projectVersion=1.3.0 +sonar.projectVersion=1.4.0 sonar.host.url=https://sonarcloud.io # =====================================================