Skip to content

Commit

Permalink
Support for async uploads with Client::uploadAsync()
Browse files Browse the repository at this point in the history
  • Loading branch information
rafael-at-bunny committed Jul 26, 2024
1 parent a2df99f commit 15b1f93
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 40 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 28 additions & 21 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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<array-key, mixed>, 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
Expand Down
64 changes: 45 additions & 19 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};
}
}

0 comments on commit 15b1f93

Please sign in to comment.