-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: s3 transfer manager v2 #3079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yenfryherrerafeliz
wants to merge
51
commits into
aws:master
Choose a base branch
from
yenfryherrerafeliz:feat_s3_transfer_manager_v2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
3093732
feat: s3 transfer manager v2
yenfryherrerafeliz 5950c31
chore: add tests cases and refactor
yenfryherrerafeliz 1c82ab5
chore: add multipart download listener tests
yenfryherrerafeliz 237fc7b
chore: refactor multipart downloaders and add tests
yenfryherrerafeliz c28c165
feat: add download directory and refactor code
yenfryherrerafeliz 34321b2
chore: add upload and refactor code
yenfryherrerafeliz 5289f7c
feat: add upload directory feature
yenfryherrerafeliz c6c0780
feat: multipart upload and some refactor
yenfryherrerafeliz 034b50d
chore: short namespace
yenfryherrerafeliz bc15ac3
chore: refactor and address feedback
yenfryherrerafeliz e681d10
chore: fix test cases
yenfryherrerafeliz 1f094cd
chore: remove unused implementation
yenfryherrerafeliz 464b498
chore: remove invalid test
yenfryherrerafeliz 09e493f
fix: add nullable type
yenfryherrerafeliz f10522b
chore: add more tests
yenfryherrerafeliz a55e1b3
chore: add upload unit tests and refactor
yenfryherrerafeliz b271897
chore: address naming feedback and test failures
yenfryherrerafeliz f4f1c88
chore: address minor styling issues
yenfryherrerafeliz d987aff
chore: add download tests
yenfryherrerafeliz 6d000f1
chore: add integ tests
yenfryherrerafeliz 27570d0
chore: add integ test
yenfryherrerafeliz 1dde7fc
chore: address PR feedback
yenfryherrerafeliz 060e0e1
chore: fix and refactor
yenfryherrerafeliz ee0fefb
chore: fix TransferListener import
yenfryherrerafeliz 9f639f1
chore: add test case
yenfryherrerafeliz bdce369
chore: address PR feedback
yenfryherrerafeliz cd2133a
fix: prevent calling twice downloadFailed
yenfryherrerafeliz 0426e11
chore: fix exception throwing
yenfryherrerafeliz a548ce2
feat: consider checksum mode from command
yenfryherrerafeliz 5a25ad8
chore: tests and minor fixes
yenfryherrerafeliz 64fc3f6
tests: Add integ test for abort
yenfryherrerafeliz 908b4a0
chore: update integ test
yenfryherrerafeliz 1e643e5
feat: update to use modeled inputs
yenfryherrerafeliz 48a2822
chore: multipart download updates
yenfryherrerafeliz 02b43f0
chore: s3 transfer manager updates
yenfryherrerafeliz 153227b
chore: minor update
yenfryherrerafeliz 31dbd19
chore: add empty lines at the end
yenfryherrerafeliz aeed758
chore: remove unused implementations
yenfryherrerafeliz 26d2469
fix: object key should be normalized
yenfryherrerafeliz 10928ca
fix: minor logic and test fix
yenfryherrerafeliz 632ece9
fix: fix s3 delimiter test
yenfryherrerafeliz 6efcc0a
fix: wrong data provider name used
yenfryherrerafeliz b0c5f75
chore: addressed some styling
yenfryherrerafeliz 44f6ff4
chore: update argument name
yenfryherrerafeliz 83ccd9b
chore: make config optional
yenfryherrerafeliz 039a67b
chore: minor refactor and fix
yenfryherrerafeliz 50715d1
chore: make parameter optional
yenfryherrerafeliz 3034ad8
chore: make model classes final
yenfryherrerafeliz cfe4ab5
chore: make classes final and refactor tests
yenfryherrerafeliz f4c42e0
chore: fix and reformat integ test
yenfryherrerafeliz 9fb03f1
chore: address some styling suggestions
yenfryherrerafeliz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace Aws\S3\S3Transfer; | ||
|
||
use Psr\Http\Message\StreamInterface; | ||
|
||
class DownloadResponse | ||
{ | ||
public function __construct( | ||
private readonly StreamInterface $content, | ||
private readonly array $metadata = [] | ||
) {} | ||
|
||
public function getContent(): StreamInterface | ||
{ | ||
return $this->content; | ||
} | ||
|
||
public function getMetadata(): array | ||
{ | ||
return $this->metadata; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace Aws\S3\S3Transfer\Exceptions; | ||
|
||
class S3TransferException extends \RuntimeException | ||
stobrien89 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
} |
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,359 @@ | ||
<?php | ||
|
||
namespace Aws\S3\S3Transfer; | ||
|
||
use Aws\CommandInterface; | ||
use Aws\ResultInterface; | ||
use Aws\S3\S3ClientInterface; | ||
use Aws\S3\S3Transfer\Progress\TransferProgressSnapshot; | ||
use GuzzleHttp\Promise\Coroutine; | ||
use GuzzleHttp\Promise\Create; | ||
use GuzzleHttp\Promise\PromiseInterface; | ||
use GuzzleHttp\Promise\PromisorInterface; | ||
use GuzzleHttp\Psr7\Utils; | ||
use Psr\Http\Message\StreamInterface; | ||
|
||
abstract class MultipartDownloader implements PromisorInterface | ||
{ | ||
public const GET_OBJECT_COMMAND = "GetObject"; | ||
public const PART_GET_MULTIPART_DOWNLOADER = "partGet"; | ||
public const RANGE_GET_MULTIPART_DOWNLOADER = "rangeGet"; | ||
|
||
/** @var array */ | ||
protected array $requestArgs; | ||
|
||
/** @var int */ | ||
protected int $currentPartNo; | ||
|
||
/** @var int */ | ||
protected int $objectPartsCount; | ||
|
||
/** @var int */ | ||
protected int $objectSizeInBytes; | ||
|
||
/** @var string */ | ||
protected string $eTag; | ||
|
||
/** @var StreamInterface */ | ||
private StreamInterface $stream; | ||
|
||
/** @var TransferListenerNotifier | null */ | ||
private readonly ?TransferListenerNotifier $listenerNotifier; | ||
|
||
/** Tracking Members */ | ||
private ?TransferProgressSnapshot $currentSnapshot; | ||
|
||
/** | ||
* @param S3ClientInterface $s3Client | ||
* @param array $requestArgs | ||
* @param array $config | ||
* - minimum_part_size: The minimum part size for a multipart download | ||
* using range get. This option MUST be set when using range get. | ||
* @param int $currentPartNo | ||
* @param int $objectPartsCount | ||
* @param int $objectSizeInBytes | ||
* @param string $eTag | ||
* @param StreamInterface|null $stream | ||
* @param TransferProgressSnapshot|null $currentSnapshot | ||
* @param TransferListenerNotifier|null $listenerNotifier | ||
*/ | ||
public function __construct( | ||
protected readonly S3ClientInterface $s3Client, | ||
array $requestArgs, | ||
protected readonly array $config = [], | ||
int $currentPartNo = 0, | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
int $objectPartsCount = 0, | ||
int $objectSizeInBytes = 0, | ||
string $eTag = "", | ||
?StreamInterface $stream = null, | ||
?TransferProgressSnapshot $currentSnapshot = null, | ||
?TransferListenerNotifier $listenerNotifier = null, | ||
) { | ||
$this->requestArgs = $requestArgs; | ||
$this->currentPartNo = $currentPartNo; | ||
$this->objectPartsCount = $objectPartsCount; | ||
$this->objectSizeInBytes = $objectSizeInBytes; | ||
$this->eTag = $eTag; | ||
if ($stream === null) { | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$this->stream = Utils::streamFor( | ||
fopen('php://temp', 'w+') | ||
); | ||
} else { | ||
$this->stream = $stream; | ||
} | ||
$this->currentSnapshot = $currentSnapshot; | ||
$this->listenerNotifier = $listenerNotifier; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getCurrentPartNo(): int | ||
{ | ||
return $this->currentPartNo; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getObjectPartsCount(): int | ||
{ | ||
return $this->objectPartsCount; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getObjectSizeInBytes(): int | ||
{ | ||
return $this->objectSizeInBytes; | ||
} | ||
|
||
/** | ||
* @return TransferProgressSnapshot | ||
*/ | ||
public function getCurrentSnapshot(): TransferProgressSnapshot | ||
{ | ||
return $this->currentSnapshot; | ||
} | ||
|
||
/** | ||
* Returns that resolves a multipart download operation, | ||
* or to a rejection in case of any failures. | ||
* | ||
* @return PromiseInterface | ||
*/ | ||
public function promise(): PromiseInterface | ||
{ | ||
return Coroutine::of(function () { | ||
$this->downloadInitiated($this->requestArgs); | ||
try { | ||
yield $this->s3Client->executeAsync($this->nextCommand()) | ||
->then(function (ResultInterface $result) { | ||
// Calculate object size and parts count. | ||
$this->computeObjectDimensions($result); | ||
// Trigger first part completed | ||
$this->partDownloadCompleted($result); | ||
})->otherwise(function ($reason) { | ||
$this->partDownloadFailed($reason); | ||
|
||
throw $reason; | ||
}); | ||
} catch (\Throwable $e) { | ||
$this->downloadFailed($e); | ||
// TODO: yield transfer exception modeled with a transfer failed response. | ||
yield Create::rejectionFor($e); | ||
} | ||
|
||
while ($this->currentPartNo < $this->objectPartsCount) { | ||
try { | ||
yield $this->s3Client->executeAsync($this->nextCommand()) | ||
stobrien89 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
->then(function ($result) { | ||
$this->partDownloadCompleted($result); | ||
|
||
return $result; | ||
})->otherwise(function ($reason) { | ||
$this->partDownloadFailed($reason); | ||
|
||
return $reason; | ||
}); | ||
} catch (\Throwable $reason) { | ||
$this->downloadFailed($reason); | ||
// TODO: yield transfer exception modeled with a transfer failed response. | ||
yield Create::rejectionFor($reason); | ||
} | ||
|
||
} | ||
|
||
// Transfer completed | ||
$this->downloadComplete(); | ||
|
||
// TODO: yield the stream wrapped in a modeled transfer success response. | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
yield Create::promiseFor(new DownloadResponse( | ||
$this->stream, | ||
[] | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
)); | ||
}); | ||
} | ||
|
||
/** | ||
* Returns the next command for fetching the next object part. | ||
* | ||
* @return CommandInterface | ||
*/ | ||
abstract protected function nextCommand() : CommandInterface; | ||
|
||
/** | ||
* Compute the object dimensions, such as size and parts count. | ||
* | ||
* @param ResultInterface $result | ||
* | ||
* @return void | ||
*/ | ||
abstract protected function computeObjectDimensions(ResultInterface $result): void; | ||
|
||
/** | ||
* Calculates the object size dynamically. | ||
* | ||
* @param $sizeSource | ||
* | ||
* @return int | ||
*/ | ||
protected function computeObjectSize($sizeSource): int | ||
{ | ||
if (is_int($sizeSource)) { | ||
return (int) $sizeSource; | ||
} | ||
|
||
if (empty($sizeSource)) { | ||
return 0; | ||
} | ||
|
||
// For extracting the object size from the ContentRange header value. | ||
if (preg_match("/\/(\d+)$/", $sizeSource, $matches)) { | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return $matches[1]; | ||
} | ||
|
||
throw new \RuntimeException('Invalid source size format'); | ||
} | ||
|
||
/** | ||
* Main purpose of this method is to propagate | ||
* the download-initiated event to listeners, but | ||
* also it does some computation regarding internal states | ||
* that need to be maintained. | ||
* | ||
* @param array $commandArgs | ||
* | ||
* @return void | ||
*/ | ||
private function downloadInitiated(array $commandArgs): void | ||
{ | ||
if ($this->currentSnapshot === null) { | ||
$this->currentSnapshot = new TransferProgressSnapshot( | ||
$commandArgs['Key'], | ||
0, | ||
$this->objectSizeInBytes | ||
); | ||
} else { | ||
$this->currentSnapshot = new TransferProgressSnapshot( | ||
$this->currentSnapshot->getIdentifier(), | ||
$this->currentSnapshot->getTransferredBytes(), | ||
$this->currentSnapshot->getTotalBytes(), | ||
$this->currentSnapshot->getResponse() | ||
); | ||
} | ||
|
||
$this->listenerNotifier?->transferInitiated([ | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'request_args' => $commandArgs, | ||
'progress_snapshot' => $this->currentSnapshot, | ||
]); | ||
} | ||
|
||
/** | ||
* Propagates download-failed event to listeners. | ||
* | ||
* @param \Throwable $reason | ||
* | ||
* @return void | ||
*/ | ||
private function downloadFailed(\Throwable $reason): void | ||
{ | ||
$this->listenerNotifier?->transferFail([ | ||
'request_args' => $this->requestArgs, | ||
'progress_snapshot' => $this->currentSnapshot, | ||
'reason' => $reason, | ||
]); | ||
} | ||
|
||
/** | ||
* Propagates part-download-completed to listeners. | ||
* It also does some computation in order to maintain internal states. | ||
* In this specific method we move each part content into an accumulative | ||
* stream, which is meant to hold the full object content once the download | ||
* is completed. | ||
* | ||
* @param ResultInterface $result | ||
* | ||
* @return void | ||
*/ | ||
private function partDownloadCompleted( | ||
ResultInterface $result | ||
): void | ||
{ | ||
$partDownloadBytes = $result['ContentLength']; | ||
if (isset($result['ETag'])) { | ||
$this->eTag = $result['ETag']; | ||
} | ||
Utils::copyToStream($result['Body'], $this->stream); | ||
$newSnapshot = new TransferProgressSnapshot( | ||
$this->currentSnapshot->getIdentifier(), | ||
$this->currentSnapshot->getTransferredBytes() + $partDownloadBytes, | ||
$this->objectSizeInBytes, | ||
$result->toArray() | ||
); | ||
$this->currentSnapshot = $newSnapshot; | ||
$this->listenerNotifier?->bytesTransferred([ | ||
'request_args' => $this->requestArgs, | ||
'progress_snapshot' => $this->currentSnapshot, | ||
]); | ||
} | ||
|
||
/** | ||
* Propagates part-download-failed event to listeners. | ||
* | ||
* @param \Throwable $reason | ||
* | ||
* @return void | ||
*/ | ||
private function partDownloadFailed( | ||
\Throwable $reason, | ||
): void | ||
{ | ||
$this->downloadFailed($reason); | ||
} | ||
|
||
/** | ||
* Propagates object-download-completed event to listeners. | ||
* It also resets the pointer of the stream to the first position, | ||
* so that the stream is ready to be consumed once returned. | ||
* | ||
* @return void | ||
*/ | ||
private function downloadComplete(): void | ||
{ | ||
$this->stream->rewind(); | ||
$newSnapshot = new TransferProgressSnapshot( | ||
$this->currentSnapshot->getIdentifier(), | ||
$this->currentSnapshot->getTransferredBytes(), | ||
$this->objectSizeInBytes, | ||
$this->currentSnapshot->getResponse() | ||
); | ||
$this->currentSnapshot = $newSnapshot; | ||
$this->listenerNotifier?->transferComplete([ | ||
'request_args' => $this->requestArgs, | ||
'progress_snapshot' => $this->currentSnapshot, | ||
]); | ||
} | ||
|
||
/** | ||
* @param mixed $multipartDownloadType | ||
* | ||
* @return string | ||
*/ | ||
public static function chooseDownloaderClassName( | ||
yenfryherrerafeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
string $multipartDownloadType | ||
): string | ||
{ | ||
return match ($multipartDownloadType) { | ||
MultipartDownloader::PART_GET_MULTIPART_DOWNLOADER => 'Aws\S3\S3Transfer\PartGetMultipartDownloader', | ||
MultipartDownloader::RANGE_GET_MULTIPART_DOWNLOADER => 'Aws\S3\S3Transfer\RangeGetMultipartDownloader', | ||
default => throw new \InvalidArgumentException( | ||
"The config value for `multipart_download_type` must be one of:\n" | ||
. "\t* " . MultipartDownloader::PART_GET_MULTIPART_DOWNLOADER | ||
."\n" | ||
. "\t* " . MultipartDownloader::RANGE_GET_MULTIPART_DOWNLOADER | ||
) | ||
}; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.