1) { // @codeCoverageIgnoreStart throw new RuntimeException('You cannot create a single upload that is larger than 4 GB.'); // @codeCoverageIgnoreEnd } return $generator->getUploadPart(1); } /** * @param EntityBodyInterface $body The upload body * @param int $partSize The size of parts to split the upload into. Default is the 4GB max * * @throws InvalidArgumentException when the part size is invalid (i.e. not a power of 2 of 1MB) * @throws InvalidArgumentException when the body is not seekable (must be able to rewind after calculating hashes) * @throws InvalidArgumentException when the archive size is less than one byte */ public function __construct(EntityBodyInterface $body, $partSize) { $this->partSize = $partSize; // Make sure the part size is valid $validPartSizes = array_map(function ($value) {return pow(2, $value) * Size::MB;}, range(0, 12)); if (!in_array($this->partSize, $validPartSizes)) { throw new InvalidArgumentException('The part size must be a megabyte multiplied by a power of 2 and no ' . 'greater than 4 gigabytes.'); } // Validate body if (!$body->isSeekable()) { throw new InvalidArgumentException('The upload body must be seekable.'); } $this->generateUploadParts($body); // Validate archive size if ($this->archiveSize < 1) { throw new InvalidArgumentException('The archive size must be at least 1 byte.'); } } /** * Returns a single upload part from the calculated uploads by part number. By default it returns the first, which * is useful behavior if there is only one upload. * * @param int $partNumber The numerical index of the upload * * @return UploadPart * @throws OutOfBoundsException if the index of the upload doesn't exist */ public function getUploadPart($partNumber) { $partNumber = (int) $partNumber; // Get the upload at the index if it exists if (isset($this->uploadParts[$partNumber - 1])) { return $this->uploadParts[$partNumber - 1]; } else { throw new OutOfBoundsException("An upload part with part number {$partNumber} at index did not exist."); } } /** * @return array */ public function getAllParts() { return $this->uploadParts; } /** * @return array */ public function getArchiveSize() { return $this->archiveSize; } /** * @return string */ public function getRootChecksum() { if (!$this->rootChecksum) { $this->rootChecksum = TreeHash::fromChecksums(array_map(function (UploadPart $part) { return $part->getChecksum(); }, $this->uploadParts))->getHash(); } return $this->rootChecksum; } /** * @return string */ public function getPartSize() { return $this->partSize; } /** * {@inheritdoc} */ public function serialize() { return serialize(array( 'uploadParts' => $this->uploadParts, 'archiveSize' => $this->archiveSize, 'partSize' => $this->partSize )); } /** * {@inheritdoc} */ public function unserialize($serialized) { // Unserialize data $data = unserialize($serialized); // Set properties foreach (array('uploadParts', 'archiveSize', 'partSize') as $property) { if (isset($data[$property])) { $this->{$property} = $data[$property]; } else { throw new RuntimeException(sprintf('Cannot unserialize the %s class. The %s property is missing.', __CLASS__, $property )); } } } /** * {@inheritdoc} */ public function getIterator() { return new \ArrayIterator($this->uploadParts); } /** * {@inheritdoc} */ public function count() { return count($this->uploadParts); } /** * Performs the work of reading the body stream, creating tree hashes, and creating UploadPartContext objects * * @param EntityBodyInterface $body The body to create parts from */ protected function generateUploadParts(EntityBodyInterface $body) { // Rewind the body stream $body->seek(0); // Initialize variables for tracking data for upload $uploadContext = new UploadPartContext($this->partSize, $body->ftell()); // Read the data from the streamed body in 1MB chunks $data = $this->readPart($body); while (strlen($data) > 0) { // Add data to the hashes and size calculations $uploadContext->addData($data); // If the upload part is complete, generate an upload object and reset the currently tracked upload data if ($uploadContext->isFull()) { $this->updateTotals($uploadContext->generatePart()); $uploadContext = new UploadPartContext($this->partSize, $body->ftell()); } $data = $this->readPart($body); } // Handle any leftover data if (!$uploadContext->isEmpty()) { $this->updateTotals($uploadContext->generatePart()); } // Rewind the body stream $body->seek(0); } /** * Updated the upload helper running totals and tree hash with the data from a complete upload part * * @param UploadPart $part The newly completed upload part * * @throws OverflowException if the maximum number of allowed upload parts is exceeded */ protected function updateTotals(UploadPart $part) { // Throw an exception if there are more parts than total allowed if ($part->getPartNumber() > self::MAX_NUM_PARTS) { // @codeCoverageIgnoreStart throw new OverflowException('An archive must be uploaded in ' . self::MAX_NUM_PARTS . ' parts or less.'); // @codeCoverageIgnoreEnd } $this->uploadParts[] = $part; $this->archiveSize += $part->getSize(); } private function readPart(EntityBodyInterface $body, $max = Size::MB) { return $body->read(min($this->partSize, $max)); } }