Skip to content
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

Progress Tracker for MultipartUploader & MultipartCopy #2699

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b2d70f6
multipart uploader method
cintiashamsu Apr 17, 2023
51f7574
update displayProgress method
cintiashamsu Apr 17, 2023
5cb12d5
removed comments
cintiashamsu Apr 17, 2023
ebd06c4
added ability to display progressBar for MultipartUpload and Multipar…
cintiashamsu Apr 19, 2023
6dc3597
Update UploadState.php
cintiashamsu Apr 19, 2023
7f05ff1
Update AbstractUploader.php
cintiashamsu Apr 19, 2023
657ab9a
MultipartUploader and MultipartCopy now sends total size directly to …
cintiashamsu Apr 19, 2023
89c9513
Update UploadStateTest.php
cintiashamsu Apr 24, 2023
d68597c
Added assert using expectOutputString and structure for data provider
cintiashamsu Apr 24, 2023
99738f8
added two tests
cintiashamsu Apr 26, 2023
604df58
Update UploadState.php
cintiashamsu Apr 26, 2023
12e328d
Update UploadStateTest.php
cintiashamsu Apr 28, 2023
e42ab91
Update UploadStateTest.php
cintiashamsu May 1, 2023
470627a
Combine progressThresholds and progressBar into one array
cintiashamsu May 1, 2023
8cfe7e9
Updated displayProgress to use progressBar
cintiashamsu May 1, 2023
9ade473
Update UploadState.php
cintiashamsu May 2, 2023
74e7938
added exceptions for non-int arguments
cintiashamsu May 3, 2023
5eded67
Added unit tests for failed upload and type checking
cintiashamsu May 3, 2023
735c9c4
Added config option
cintiashamsu May 5, 2023
cff396a
added config option to multipartUploader and multipartCopy
cintiashamsu May 10, 2023
776cc4d
added another parameter to markPartAsUploaded for possible Glacier su…
cintiashamsu May 15, 2023
465fc61
added descriptions for methods, cleaned up tests
cintiashamsu May 24, 2023
e2d4e81
PHPUnit Polyfills on UploadStateTest
cintiashamsu May 26, 2023
1a0a580
removed array_key_first to support php 5.5
cintiashamsu May 26, 2023
eee6a67
dev guide description added
cintiashamsu May 30, 2023
1156b75
added method setDisplayProgress, changelog
cintiashamsu Jun 9, 2023
da296ce
$displayProgress not declared dynamically
cintiashamsu Jun 9, 2023
9ea4f20
line-wrap in upload state
cintiashamsu Jun 10, 2023
102f26d
Merge branch 'aws:master' into new-feature
cintiashamsu Jun 10, 2023
367bc01
=== comparison operator
cintiashamsu Jun 27, 2023
d81c87b
Update MultipartUploaderTest.php
cintiashamsu Jun 27, 2023
39b1044
displayprogress set in multipartuploader/copy
cintiashamsu Jun 27, 2023
362ae07
Merge branch 'master' into new-feature
stobrien89 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Multipart/AbstractUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ protected function getNumberOfParts($partSize)
}
return null;
}
}
}
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
62 changes: 59 additions & 3 deletions src/Multipart/UploadState.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ class UploadState
const INITIATED = 1;
const COMPLETED = 2;

protected $progressBar = [
"Transfer initiated...\n| | 0.0%\n",
"|== | 12.5%\n",
"|===== | 25.0%\n",
"|======= | 37.5%\n",
"|========== | 50.0%\n",
"|============ | 62.5%\n",
"|=============== | 75.0%\n",
"|================= | 87.5%\n",
"|====================| 100.0%\nTransfer complete!\n"
];

/** @var array Params used to identity the upload. */
private $id;

Expand All @@ -25,6 +37,9 @@ class UploadState
/** @var int Identifies the status the upload. */
private $status = self::CREATED;

/** @var array Thresholds for progress of the upload. */
private $progressThresholds = [];

/**
* @param array $id Params used to identity the upload.
*/
Expand All @@ -45,7 +60,7 @@ public function getId()
}

/**
* Set's the "upload_id", or 3rd part of the upload's ID. This typically
* Sets the "upload_id", or 3rd part of the upload's ID. This typically
* only needs to be done after initiating an upload.
*
* @param string $key The param key of the upload_id.
Expand Down Expand Up @@ -76,6 +91,47 @@ public function setPartSize($partSize)
$this->partSize = $partSize;
}

/**
* Sets the 1/8th thresholds array. $totalSize is only sent if
* 'track_upload' is true.
*
* @param $totalSize numeric Size of object to upload.
*
* @return array
*/
public function setProgressThresholds($totalSize)
SamRemis marked this conversation as resolved.
Show resolved Hide resolved
{
if(!is_numeric($totalSize)) {
throw new \InvalidArgumentException('The total size of the upload must be a number.');
}

$this->progressThresholds[0] = 0;
for ($i=1;$i<=8;$i++) {
$this->progressThresholds []= round($totalSize*($i/8));
}
return $this->progressThresholds;
}

/**
* Prints progress of upload.
*
* @param $totalUploaded numeric Size of upload so far.
*/
public function displayProgress($totalUploaded)
{
if(!is_numeric($totalUploaded)) {
throw new \InvalidArgumentException('The size of the bytes being uploaded must be a number.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do a line-wrap on this like:

throw new \InvalidArgumentException(
    'The size of the bytes being uploaded must be a number.'
);

}

while (!empty($this->progressThresholds)
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
&& !empty($this->progressBar)
&& $totalUploaded >= $this->progressThresholds[0])
{
echo array_shift($this->progressBar);
array_shift($this->progressThresholds);
}
}

/**
* Marks a part as being uploaded.
*
Expand Down Expand Up @@ -108,7 +164,6 @@ public function hasPartBeenUploaded($partNumber)
public function getUploadedParts()
{
ksort($this->uploadedParts);

return $this->uploadedParts;
}

Expand All @@ -118,6 +173,7 @@ public function getUploadedParts()
* @param int $status Status is an integer code defined by the constants
* CREATED, INITIATED, and COMPLETED on this class.
*/

cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
public function setStatus($status)
{
$this->status = $status;
Expand All @@ -142,4 +198,4 @@ public function isCompleted()
{
return $this->status === self::COMPLETED;
}
}
}
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions src/S3/MultipartCopy.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class MultipartCopy extends AbstractUploadManager
* options are ignored.
* - source_metadata: (Aws\ResultInterface) An object that represents the
* result of executing a HeadObject command on the copy source.
* - track_upload: (boolean) Set true to track status in 1/8th increments
* for upload.
*
* @param S3ClientInterface $client Client used for the upload.
* @param string|array $source Location of the data to be copied (in the
Expand All @@ -75,6 +77,12 @@ public function __construct(
$client,
array_change_key_case($config) + ['source_metadata' => null]
);

if (isset($config['track_upload']) && $config['track_upload']) {
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
$this->getState()->setProgressThresholds(
$this->sourceMetadata["ContentLength"]
);
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/S3/MultipartUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class MultipartUploader extends AbstractUploader
* of the multipart upload and that is used to resume a previous upload.
* When this option is provided, the `bucket`, `key`, and `part_size`
* options are ignored.
* - track_upload: (boolean) Set true to track status in 1/8th increments
* for upload.
*
* @param S3ClientInterface $client Client used for the upload.
* @param mixed $source Source of the data to upload.
Expand All @@ -70,6 +72,10 @@ public function __construct(
'key' => null,
'exception_class' => S3MultipartUploadException::class,
]);

if (isset($config['track_upload']) && $config['track_upload']) {
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
$this->getState()->setProgressThresholds($this->source->getSize());
}
}

protected function loadUploadWorkflowInfo()
Expand Down
7 changes: 7 additions & 0 deletions src/S3/MultipartUploadingTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

trait MultipartUploadingTrait
{
private $uploadedBytes = 0;

/**
* Creates an UploadState object for a multipart upload by querying the
* service for the specified upload's information.
Expand Down Expand Up @@ -55,6 +57,11 @@ protected function handleResult(CommandInterface $command, ResultInterface $resu
'PartNumber' => $command['PartNumber'],
'ETag' => $this->extractETag($result),
]);

// Updates counter for uploaded bytes.
$this->uploadedBytes += $command["ContentLength"];
// Sends uploaded bytes to progress tracker.
$this->getState()->displayProgress($this->uploadedBytes);
cintiashamsu marked this conversation as resolved.
Show resolved Hide resolved
}

abstract protected function extractETag(ResultInterface $result);
Expand Down
158 changes: 156 additions & 2 deletions tests/Multipart/UploadStateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
namespace Aws\Test\Multipart;

use Aws\Multipart\UploadState;
use PHPUnit\Framework\TestCase;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;

/**
* @covers Aws\Multipart\UploadState
Expand Down Expand Up @@ -70,4 +70,158 @@ public function testSerializationWorks()
$this->assertTrue($newState->isInitiated());
$this->assertArrayHasKey('foo', $newState->getId());
}
}

public function testEmptyUploadStateOutputWithConfigFalse()
{
$state = new UploadState([]);
$state->displayProgress(13);
$this->expectOutputString('');
}

/**
* @dataProvider getDisplayProgressCases
*/
public function testDisplayProgressPrintsProgress(
$totalSize,
$totalUploaded,
$progressBar
) {
$state = new UploadState([]);
$state->setProgressThresholds($totalSize);
$state->displayProgress($totalUploaded);

$this->expectOutputString($progressBar);
}

public function getDisplayProgressCases()
{
$progressBar = ["Transfer initiated...\n| | 0.0%\n",
"|== | 12.5%\n",
"|===== | 25.0%\n",
"|======= | 37.5%\n",
"|========== | 50.0%\n",
"|============ | 62.5%\n",
"|=============== | 75.0%\n",
"|================= | 87.5%\n",
"|====================| 100.0%\nTransfer complete!\n"];
return [
[100000, 0, $progressBar[0]],
[100000, 12499, $progressBar[0]],
[100000, 12500, "{$progressBar[0]}{$progressBar[1]}"],
[100000, 24999, "{$progressBar[0]}{$progressBar[1]}"],
[100000, 25000, "{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}"],
[100000, 37499, "{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}"],
[
100000,
37500,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}"
],
[
100000,
49999,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}"
],
[
100000,
50000,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}"
],
[
100000,
62499,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}"
],
[
100000,
62500,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}"
],
[
100000,
74999,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}"
],
[
100000,
75000,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}{$progressBar[6]}"
],
[
100000,
87499,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}{$progressBar[6]}"
],
[
100000,
87500,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}{$progressBar[6]}{$progressBar[7]}"
],
[
100000,
99999,
"{$progressBar[0]}{$progressBar[1]}{$progressBar[2]}{$progressBar[3]}{$progressBar[4]}" .
"{$progressBar[5]}{$progressBar[6]}{$progressBar[7]}"
],
[100000, 100000, implode($progressBar)]
];
}

/**
* @dataProvider getThresholdCases
*/
public function testUploadThresholds($totalSize)
{
$state = new UploadState([]);
$threshold = $state->setProgressThresholds($totalSize);

$this->assertIsArray($threshold);
$this->assertCount(9, $threshold);
}

public function getThresholdCases()
{
return [
[0],
[100000],
[100001]
];
}

/**
* @dataProvider getInvalidIntCases
*/
public function testSetProgressThresholdsThrowsException($totalSize)
{
$state = new UploadState([]);
$this->expectExceptionMessage('The total size of the upload must be a number.');
$this->expectException(\InvalidArgumentException::class);

$state->setProgressThresholds($totalSize);
}

/**
* @dataProvider getInvalidIntCases
*/
public function testDisplayProgressThrowsException($totalUploaded)
{
$state = new UploadState([]);
$this->expectExceptionMessage('The size of the bytes being uploaded must be a number.');
$this->expectException(\InvalidArgumentException::class);

$state->displayProgress($totalUploaded);
}

public function getInvalidIntCases()
{
return [
[''],
[null],
['aws']
];
}
}
45 changes: 45 additions & 0 deletions tests/S3/MultipartUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,49 @@ public function testAppliesAmbiguousSuccessParsing()
);
$uploader->upload();
}

public function testFailedUploadPrintsPartialProgressBar()
{
$partialBar = [ "Transfer initiated...\n| | 0.0%\n",
"|== | 12.5%\n",
"|===== | 25.0%\n"];
$this->expectOutputString("{$partialBar[0]}{$partialBar[1]}{$partialBar[2]}");

$this->expectExceptionMessage("An exception occurred while uploading parts to a multipart upload");
$this->expectException(\Aws\S3\Exception\S3MultipartUploadException::class);
$counter = 0;

$httpHandler = function ($request, array $options) use (&$counter) {
if ($counter < 4) {
$body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><OperationNameResponse><UploadId>baz</UploadId></OperationNameResponse>";
} else {
$body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\n";
}
$counter++;

return Promise\Create::promiseFor(
new Psr7\Response(200, [], $body)
);
};

$s3 = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'http_handler' => $httpHandler
]);

$data = str_repeat('.', 50 * self::MB);
$source = Psr7\Utils::streamFor($data);

$uploader = new MultipartUploader(
$s3,
$source,
[
'bucket' => 'test-bucket',
'key' => 'test-key',
'track_upload' => 'true'
]
);
$uploader->upload();
}
}