| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\s3\Aws\S3; |
| 4 |
|
| 5 |
use Dudlewebs\WPMCS\s3\Aws\Arn\ArnParser; |
| 6 |
use Dudlewebs\WPMCS\s3\Aws\Multipart\AbstractUploadManager; |
| 7 |
use Dudlewebs\WPMCS\s3\Aws\ResultInterface; |
| 8 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7; |
| 9 |
class MultipartCopy extends AbstractUploadManager |
| 10 |
{ |
| 11 |
use MultipartUploadingTrait; |
| 12 |
/** @var string|array */ |
| 13 |
private $source; |
| 14 |
/** @var string */ |
| 15 |
private $sourceVersionId; |
| 16 |
/** @var ResultInterface */ |
| 17 |
private $sourceMetadata; |
| 18 |
/** |
| 19 |
* Creates a multipart upload for copying an S3 object. |
| 20 |
* |
| 21 |
* The valid configuration options are as follows: |
| 22 |
* |
| 23 |
* - acl: (string) ACL to set on the object being upload. Objects are |
| 24 |
* private by default. |
| 25 |
* - before_complete: (callable) Callback to invoke before the |
| 26 |
* `CompleteMultipartUpload` operation. The callback should have a |
| 27 |
* function signature like `function (Aws\Command $command) {...}`. |
| 28 |
* - before_initiate: (callable) Callback to invoke before the |
| 29 |
* `CreateMultipartUpload` operation. The callback should have a function |
| 30 |
* signature like `function (Aws\Command $command) {...}`. |
| 31 |
* - before_upload: (callable) Callback to invoke before `UploadPartCopy` |
| 32 |
* operations. The callback should have a function signature like |
| 33 |
* `function (Aws\Command $command) {...}`. |
| 34 |
* - bucket: (string, required) Name of the bucket to which the object is |
| 35 |
* being uploaded. |
| 36 |
* - concurrency: (int, default=int(5)) Maximum number of concurrent |
| 37 |
* `UploadPart` operations allowed during the multipart upload. |
| 38 |
* - key: (string, required) Key to use for the object being uploaded. |
| 39 |
* - params: (array) An array of key/value parameters that will be applied |
| 40 |
* to each of the sub-commands run by the uploader as a base. |
| 41 |
* Auto-calculated options will override these parameters. If you need |
| 42 |
* more granularity over parameters to each sub-command, use the before_* |
| 43 |
* options detailed above to update the commands directly. |
| 44 |
* - part_size: (int, default=int(5242880)) Part size, in bytes, to use when |
| 45 |
* doing a multipart upload. This must between 5 MB and 5 GB, inclusive. |
| 46 |
* - state: (Aws\Multipart\UploadState) An object that represents the state |
| 47 |
* of the multipart upload and that is used to resume a previous upload. |
| 48 |
* When this option is provided, the `bucket`, `key`, and `part_size` |
| 49 |
* options are ignored. |
| 50 |
* - source_metadata: (Aws\ResultInterface) An object that represents the |
| 51 |
* result of executing a HeadObject command on the copy source. |
| 52 |
* - display_progress: (boolean) Set true to track status in 1/8th increments |
| 53 |
* for upload. |
| 54 |
* |
| 55 |
* @param S3ClientInterface $client Client used for the upload. |
| 56 |
* @param string|array $source Location of the data to be copied (in the |
| 57 |
* form /<bucket>/<key>). If the key contains a '?' |
| 58 |
* character, instead pass an array of source_key, |
| 59 |
* source_bucket, and source_version_id. |
| 60 |
* @param array $config Configuration used to perform the upload. |
| 61 |
*/ |
| 62 |
public function __construct(S3ClientInterface $client, $source, array $config = []) |
| 63 |
{ |
| 64 |
if (\is_array($source)) { |
| 65 |
$this->source = $source; |
| 66 |
} else { |
| 67 |
$this->source = $this->getInputSource($source); |
| 68 |
} |
| 69 |
parent::__construct($client, \array_change_key_case($config) + ['source_metadata' => null]); |
| 70 |
if ($this->displayProgress) { |
| 71 |
$this->getState()->setProgressThresholds($this->sourceMetadata["ContentLength"]); |
| 72 |
} |
| 73 |
} |
| 74 |
/** |
| 75 |
* An alias of the self::upload method. |
| 76 |
* |
| 77 |
* @see self::upload |
| 78 |
*/ |
| 79 |
public function copy() |
| 80 |
{ |
| 81 |
return $this->upload(); |
| 82 |
} |
| 83 |
protected function loadUploadWorkflowInfo() |
| 84 |
{ |
| 85 |
return ['command' => ['initiate' => 'CreateMultipartUpload', 'upload' => 'UploadPartCopy', 'complete' => 'CompleteMultipartUpload'], 'id' => ['bucket' => 'Bucket', 'key' => 'Key', 'upload_id' => 'UploadId'], 'part_num' => 'PartNumber']; |
| 86 |
} |
| 87 |
protected function getUploadCommands(callable $resultHandler) |
| 88 |
{ |
| 89 |
$parts = \ceil($this->getSourceSize() / $this->determinePartSize()); |
| 90 |
for ($partNumber = 1; $partNumber <= $parts; $partNumber++) { |
| 91 |
// If we haven't already uploaded this part, yield a new part. |
| 92 |
if (!$this->state->hasPartBeenUploaded($partNumber)) { |
| 93 |
$command = $this->client->getCommand($this->info['command']['upload'], $this->createPart($partNumber, $parts) + $this->getState()->getId()); |
| 94 |
$command->getHandlerList()->appendSign($resultHandler, 'mup'); |
| 95 |
(yield $command); |
| 96 |
} |
| 97 |
} |
| 98 |
} |
| 99 |
private function createPart($partNumber, $partsCount) |
| 100 |
{ |
| 101 |
$data = []; |
| 102 |
// Apply custom params to UploadPartCopy data |
| 103 |
$config = $this->getConfig(); |
| 104 |
$params = isset($config['params']) ? $config['params'] : []; |
| 105 |
foreach ($params as $k => $v) { |
| 106 |
$data[$k] = $v; |
| 107 |
} |
| 108 |
// The source parameter here is usually a string, but can be overloaded as an array |
| 109 |
// if the key contains a '?' character to specify where the query parameters start |
| 110 |
if (\is_array($this->source)) { |
| 111 |
$key = \str_replace('%2F', '/', \rawurlencode($this->source['source_key'])); |
| 112 |
$bucket = $this->source['source_bucket']; |
| 113 |
} else { |
| 114 |
list($bucket, $key) = \explode('/', \ltrim($this->source, '/'), 2); |
| 115 |
$key = \implode('/', \array_map('urlencode', \explode('/', \rawurldecode($key)))); |
| 116 |
} |
| 117 |
$uri = ArnParser::isArn($bucket) ? '' : '/'; |
| 118 |
$uri .= $bucket . '/' . $key; |
| 119 |
$data['CopySource'] = $uri; |
| 120 |
$data['PartNumber'] = $partNumber; |
| 121 |
if (!empty($this->sourceVersionId)) { |
| 122 |
$data['CopySource'] .= "?versionId=" . $this->sourceVersionId; |
| 123 |
} |
| 124 |
$defaultPartSize = $this->determinePartSize(); |
| 125 |
$startByte = $defaultPartSize * ($partNumber - 1); |
| 126 |
$data['ContentLength'] = $partNumber < $partsCount ? $defaultPartSize : $this->getSourceSize() - $defaultPartSize * ($partsCount - 1); |
| 127 |
$endByte = $startByte + $data['ContentLength'] - 1; |
| 128 |
$data['CopySourceRange'] = "bytes={$startByte}-{$endByte}"; |
| 129 |
return $data; |
| 130 |
} |
| 131 |
protected function extractETag(ResultInterface $result) |
| 132 |
{ |
| 133 |
return $result->search('CopyPartResult.ETag'); |
| 134 |
} |
| 135 |
protected function getSourceMimeType() |
| 136 |
{ |
| 137 |
return $this->getSourceMetadata()['ContentType']; |
| 138 |
} |
| 139 |
protected function getSourceSize() |
| 140 |
{ |
| 141 |
return $this->getSourceMetadata()['ContentLength']; |
| 142 |
} |
| 143 |
private function getSourceMetadata() |
| 144 |
{ |
| 145 |
if (empty($this->sourceMetadata)) { |
| 146 |
$this->sourceMetadata = $this->fetchSourceMetadata(); |
| 147 |
} |
| 148 |
return $this->sourceMetadata; |
| 149 |
} |
| 150 |
private function fetchSourceMetadata() |
| 151 |
{ |
| 152 |
if ($this->config['source_metadata'] instanceof ResultInterface) { |
| 153 |
return $this->config['source_metadata']; |
| 154 |
} |
| 155 |
//if the source variable was overloaded with an array, use the inputs for key and bucket |
| 156 |
if (\is_array($this->source)) { |
| 157 |
$headParams = ['Key' => $this->source['source_key'], 'Bucket' => $this->source['source_bucket']]; |
| 158 |
if (isset($this->source['source_version_id'])) { |
| 159 |
$this->sourceVersionId = $this->source['source_version_id']; |
| 160 |
$headParams['VersionId'] = $this->sourceVersionId; |
| 161 |
} |
| 162 |
//otherwise, use the default source parsing behavior |
| 163 |
} else { |
| 164 |
list($bucket, $key) = \explode('/', \ltrim($this->source, '/'), 2); |
| 165 |
$headParams = ['Bucket' => $bucket, 'Key' => $key]; |
| 166 |
if (\strpos($key, '?')) { |
| 167 |
list($key, $query) = \explode('?', $key, 2); |
| 168 |
$headParams['Key'] = $key; |
| 169 |
$query = Psr7\Query::parse($query, \false); |
| 170 |
if (isset($query['versionId'])) { |
| 171 |
$this->sourceVersionId = $query['versionId']; |
| 172 |
$headParams['VersionId'] = $this->sourceVersionId; |
| 173 |
} |
| 174 |
} |
| 175 |
} |
| 176 |
return $this->client->headObject($headParams); |
| 177 |
} |
| 178 |
/** |
| 179 |
* Get the url decoded input source, starting with a slash if it is not an |
| 180 |
* ARN to standardize the source location syntax. |
| 181 |
* |
| 182 |
* @param string $inputSource The source that was passed to the constructor |
| 183 |
* @return string The source, starting with a slash if it's not an arn |
| 184 |
*/ |
| 185 |
private function getInputSource($inputSource) |
| 186 |
{ |
| 187 |
$sourceBuilder = ArnParser::isArn($inputSource) ? '' : '/'; |
| 188 |
$sourceBuilder .= \ltrim(\rawurldecode($inputSource), '/'); |
| 189 |
return $sourceBuilder; |
| 190 |
} |
| 191 |
} |
| 192 |
|