$accountId, 'vaultName' => $vaultName, 'uploadId' => $uploadId, ]); foreach ($client->getPaginator('ListParts', $state->getId()) as $result) { // Get the part size from the first part in the first result. if (!$state->getPartSize()) { $state->setPartSize($result['PartSizeInBytes']); } // Mark all the parts returned by ListParts as uploaded. foreach ($result['Parts'] as $part) { list($rangeIndex, $rangeSize) = self::parseRange( $part['RangeInBytes'], $state->getPartSize() ); $state->markPartAsUploaded($rangeIndex, [ 'size' => $rangeSize, 'checksum' => $part['SHA256TreeHash'], ]); } } $state->setStatus(UploadState::INITIATED); return $state; } /** * Creates a multipart upload for a Glacier archive. * * The valid configuration options are as follows: * * - account_id: (string, default=string('-')) Account ID for the archive * being uploaded, if different from the account making the request. * - archive_description: (string) Description of the archive. * - before_complete: (callable) Callback to invoke before the * `CompleteMultipartUpload` operation. The callback should have a * function signature like `function (Aws\Command $command) {...}`. * - before_initiate: (callable) Callback to invoke before the * `InitiateMultipartUpload` operation. The callback should have a * function signature like `function (Aws\Command $command) {...}`. * - before_upload: (callable) Callback to invoke before any * `UploadMultipartPart` operations. The callback should have a function * signature like `function (Aws\Command $command) {...}`. * - concurrency: (int, default=int(3)) Maximum number of concurrent * `UploadMultipartPart` operations allowed during the multipart upload. * - part_size: (int, default=int(1048576)) Part size, in bytes, to use when * doing a multipart upload. This must between 1 MB and 4 GB, and must be * a power of 2 (in megabytes). * - prepare_data_source: (callable) Callback to invoke before starting the * multipart upload workflow. The callback should have a function * signature like `function () {...}`. * - state: (Aws\Multipart\UploadState) An object that represents the state * of the multipart upload and that is used to resume a previous upload. * When this options is provided, the `account_id`, `key`, and `part_size` * options are ignored. * - vault_name: (string, required) Vault name to use for the archive being * uploaded. * * @param GlacierClient $client Client used for the upload. * @param mixed $source Source of the data to upload. * @param array $config Configuration used to perform the upload. */ public function __construct(GlacierClient $client, $source, array $config = []) { parent::__construct($client, $source, $config + [ 'account_id' => '-', 'vault_name' => null, ]); } protected function loadUploadWorkflowInfo() { return [ 'command' => [ 'initiate' => 'InitiateMultipartUpload', 'upload' => 'UploadMultipartPart', 'complete' => 'CompleteMultipartUpload', ], 'id' => [ 'account_id' => 'accountId', 'vault_name' => 'vaultName', 'upload_id' => 'uploadId', ], 'part_num' => 'range', ]; } protected function determinePartSize() { // Make sure the part size is set. $partSize = $this->config['part_size'] ?: self::PART_MIN_SIZE; // Ensure that the part size is valid. if (!in_array($partSize, self::$validPartSizes)) { throw new \InvalidArgumentException('The part_size must be a power ' . 'of 2, in megabytes, such that 1 MB <= PART_SIZE <= 4 GB.'); } return $partSize; } protected function createPart($seekable, $number) { $data = []; $firstByte = $this->source->tell(); // Read from the source to create the body stream. This also // calculates the linear and tree hashes as the data is read. if ($seekable) { // Case 1: Stream is seekable, can make stream from new handle. $body = Psr7\Utils::tryFopen($this->source->getMetadata('uri'), 'r'); $body = $this->limitPartStream(Psr7\Utils::streamFor($body)); // Create another stream decorated with hashing streams and read // through it, so we can get the hash values for the part. $decoratedBody = $this->decorateWithHashes($body, $data); while (!$decoratedBody->eof()) $decoratedBody->read(1048576); // Seek the original source forward to the end of the range. $this->source->seek($this->source->tell() + $body->getSize()); } else { // Case 2: Stream is not seekable, must store part in temp stream. $source = $this->limitPartStream($this->source); $source = $this->decorateWithHashes($source, $data); $body = Psr7\Utils::streamFor(); Psr7\Utils::copyToStream($source, $body); } // Do not create a part if the body size is zero. if ($body->getSize() === 0) { return false; } $body->seek(0); $data['body'] = $body; $lastByte = $this->source->tell() - 1; $data['range'] = "bytes {$firstByte}-{$lastByte}/*"; return $data; } protected function handleResult(CommandInterface $command, ResultInterface $result) { list($rangeIndex, $rangeSize) = $this->parseRange( $command['range'], $this->state->getPartSize() ); $this->state->markPartAsUploaded($rangeIndex, [ 'size' => $rangeSize, 'checksum' => $command['checksum'] ]); } protected function getInitiateParams() { $params = ['partSize' => $this->state->getPartSize()]; if (isset($this->config['archive_description'])) { $params['archiveDescription'] = $this->config['archive_description']; } return $params; } protected function getCompleteParams() { $treeHash = new TreeHash(); $archiveSize = 0; foreach ($this->state->getUploadedParts() as $part) { $archiveSize += $part['size']; $treeHash->addChecksum($part['checksum']); } return [ 'archiveSize' => $archiveSize, 'checksum' => bin2hex($treeHash->complete()), ]; } /** * Decorates a stream with a tree AND linear sha256 hashing stream. * * @param Stream $stream Stream to decorate. * @param array $data Data bag that results are injected into. * * @return Stream */ private function decorateWithHashes(Stream $stream, array &$data) { // Make sure that a tree hash is calculated. $stream = new HashingStream($stream, new TreeHash(), function ($result) use (&$data) { $data['checksum'] = bin2hex($result); } ); // Make sure that a linear SHA256 hash is calculated. $stream = new HashingStream($stream, new PhpHash('sha256'), function ($result) use (&$data) { $data['ContentSHA256'] = bin2hex($result); } ); return $stream; } /** * Parses a Glacier range string into a size and part number. * * @param string $range Glacier range string (e.g., "bytes 5-5000/*") * @param int $partSize The chosen part size * * @return array */ private static function parseRange($range, $partSize) { // Strip away the prefix and suffix. if (strpos($range, 'bytes') !== false) { $range = substr($range, 6, -2); } // Split that range into it's parts. list($firstByte, $lastByte) = explode('-', $range); // Calculate and return range index and range size return [ intval($firstByte / $partSize) + 1, $lastByte - $firstByte + 1, ]; } }