diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2f3f0..f987316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Support for async uploads with `Client::uploadAsync()`; + ## [3.3.1] - 2024-07-23 ### Changed diff --git a/README.md b/README.md index 245bbcb..6414c9e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ The checksum can be disabled using the `$withChecksum` parameter: $client->upload('/path/to/local/file.txt', 'remote/path/hello-world.txt', false); ``` +> [!NOTE] +> Async uploads are supported with `$client->uploadAsync()`. It will return a `GuzzleHttp\Promise\PromiseInterface`. + --- ### Downloading objects diff --git a/src/Client.php b/src/Client.php index 892e281..c8e24bc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,6 +5,7 @@ namespace Bunny\Storage; use GuzzleHttp\Client as HttpClient; +use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\Utils as PromiseUtils; use Psr\Http\Message\ResponseInterface; @@ -92,10 +93,22 @@ public function putContents(string $path, string $contents, bool $withChecksum = $headers['Checksum'] = strtoupper(hash('sha256', $contents)); } - $this->makeUploadRequest($path, ['headers' => $headers, 'body' => $contents]); + $promise = $this->makeUploadRequest($path, ['headers' => $headers, 'body' => $contents]); + $promise->wait(); } public function upload(string $localPath, string $path, bool $withChecksum = true): void + { + $promise = $this->uploadWithOptions($localPath, $path, $withChecksum); + $promise->wait(); + } + + public function uploadAsync(string $localPath, string $path, bool $withChecksum = true): PromiseInterface + { + return $this->uploadWithOptions($localPath, $path, $withChecksum); + } + + private function uploadWithOptions(string $localPath, string $path, bool $withChecksum): PromiseInterface { $fileStream = fopen($localPath, 'r'); if (false === $fileStream) { @@ -110,37 +123,31 @@ public function upload(string $localPath, string $path, bool $withChecksum = tru } } - $this->makeUploadRequest($path, ['headers' => $headers, 'body' => $fileStream]); + return $this->makeUploadRequest($path, ['headers' => $headers, 'body' => $fileStream]); } /** * @param array{headers: array, body: mixed} $options */ - private function makeUploadRequest(string $path, array $options): void + private function makeUploadRequest(string $path, array $options): PromiseInterface { - $response = $this->httpClient->request('PUT', $this->normalizePath($path), $options); - - if (401 === $response->getStatusCode()) { - throw new AuthenticationException($this->storageZoneName, $this->apiAccessKey); - } + $response = $this->httpClient->requestAsync('PUT', $this->normalizePath($path), $options); - if (400 === $response->getStatusCode()) { - /** @var bool|array{Message: string}|null $json */ - $json = json_decode($response->getBody()->getContents(), true); - $message = 'Checksum and file contents mismatched'; - - if (isset($json['Message']) && is_array($json) && is_string($json['Message'])) { - $message = (string) $json['Message']; + return $response->then(function (ResponseInterface $response) { + if (401 === $response->getStatusCode()) { + throw new AuthenticationException($this->storageZoneName, $this->apiAccessKey); } - throw new Exception($message); - } + if (400 === $response->getStatusCode()) { + throw new Exception('Checksum and file contents mismatched'); + } - if (201 === $response->getStatusCode()) { - return; - } + if (201 === $response->getStatusCode()) { + return; + } - throw new Exception('Could not upload file'); + throw new Exception('Could not upload file'); + }); } public function getContents(string $path): string diff --git a/tests/ClientTest.php b/tests/ClientTest.php index ba7b131..5d87435 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -41,34 +41,37 @@ public static function deleteDataProvider(): array */ public function testUpload(string $file, bool $withChecksum, ?string $expectedChecksum) { - $options = function (array $options) use ($expectedChecksum): bool { - if (!is_resource($options['body'])) { - return false; - } + $thenPromise = $this->createMock(\GuzzleHttp\Promise\Promise::class); + $thenPromise->expects($this->once())->method('wait'); - if (null === $expectedChecksum) { - return !isset($options['headers']['Checksum']); - } + $httpPromise = $this->createMock(\GuzzleHttp\Promise\Promise::class); + $httpPromise->expects($this->once())->method('then')->willReturn($thenPromise); - if (!isset($options['headers']['Checksum'])) { - return false; - } + $httpClient = $this->createMock(\GuzzleHttp\Client::class); + $httpClient->expects($this->once())->method('requestAsync')->with('PUT', 'test/'.$file, $this->callback($this->uploadOptionsCallback($expectedChecksum)))->willReturn($httpPromise); - if ($expectedChecksum !== $options['headers']['Checksum']) { - return false; - } + $client = new Client('abc1234d', 'test', Region::FALKENSTEIN, $httpClient); + $client->upload(__DIR__.'/_files/'.$file, $file, $withChecksum); + } - return true; - }; + /** + * @dataProvider uploadDataProvider + */ + public function testUploadAsync(string $file, bool $withChecksum, ?string $expectedChecksum) + { + $thenPromise = $this->createMock(\GuzzleHttp\Promise\Promise::class); + $thenPromise->expects($this->never())->method('wait'); - $response = $this->createMock(\Psr\Http\Message\ResponseInterface::class); - $response->expects($this->atLeastOnce())->method('getStatusCode')->willReturn(201); + $httpPromise = $this->createMock(\GuzzleHttp\Promise\Promise::class); + $httpPromise->expects($this->once())->method('then')->willReturn($thenPromise); $httpClient = $this->createMock(\GuzzleHttp\Client::class); - $httpClient->expects($this->once())->method('request')->with('PUT', 'test/'.$file, $this->callback($options))->willReturn($response); + $httpClient->expects($this->once())->method('requestAsync')->with('PUT', 'test/'.$file, $this->callback($this->uploadOptionsCallback($expectedChecksum)))->willReturn($httpPromise); $client = new Client('abc1234d', 'test', Region::FALKENSTEIN, $httpClient); - $client->upload(__DIR__.'/_files/'.$file, $file, $withChecksum); + $result = $client->uploadAsync(__DIR__.'/_files/'.$file, $file, $withChecksum); + + $this->assertSame($thenPromise, $result); } public static function uploadDataProvider(): array @@ -146,4 +149,27 @@ public static function infoDatesDataProvider(): array [true, '', '', '01/02/03 04:05:06', '01/02/03 04:05:06'], ]; } + + private function uploadOptionsCallback(?string $expectedChecksum) + { + return function (array $options) use ($expectedChecksum): bool { + if (!is_resource($options['body'])) { + return false; + } + + if (null === $expectedChecksum) { + return !isset($options['headers']['Checksum']); + } + + if (!isset($options['headers']['Checksum'])) { + return false; + } + + if ($expectedChecksum !== $options['headers']['Checksum']) { + return false; + } + + return true; + }; + } }