params = $params; $this->client = $client; $this->params['Bucket'] = $bucket; $this->params['Key'] = $key; // If a string is passed, then assume that the download should stream to a file on disk if (is_string($target)) { if (!($target = fopen($target, 'a+'))) { throw new RuntimeException("Unable to open {$target} for writing"); } // Always append to the file fseek($target, 0, SEEK_END); } // Get the metadata and Content-MD5 of the object $this->target = EntityBody::factory($target); } /** * Get the bucket of the download * * @return string */ public function getBucket() { return $this->params['Bucket']; } /** * Get the key of the download * * @return string */ public function getKey() { return $this->params['Key']; } /** * Get the file to which the contents are downloaded * * @return string */ public function getFilename() { return $this->target->getUri(); } /** * Download the remainder of the object from Amazon S3 * * Performs a message integrity check if possible * * @return Model */ public function __invoke() { $command = $this->client->getCommand('HeadObject', $this->params); $this->meta = $command->execute(); if ($this->target->ftell() >= $this->meta['ContentLength']) { return false; } $this->meta['ContentMD5'] = (string) $command->getResponse()->getHeader('Content-MD5'); // Use a ReadLimitEntityBody so that rewinding the stream after an error does not cause the file pointer // to enter an inconsistent state with the data being downloaded $this->params['SaveAs'] = new ReadLimitEntityBody( $this->target, $this->meta['ContentLength'], $this->target->ftell() ); $result = $this->getRemaining(); $this->checkIntegrity(); return $result; } /** * Send the command to get the remainder of the object * * @return Model */ protected function getRemaining() { $current = $this->target->ftell(); $targetByte = $this->meta['ContentLength'] - 1; $this->params['Range'] = "bytes={$current}-{$targetByte}"; // Set the starting offset so that the body is never seeked to before this point in the event of a retry $this->params['SaveAs']->setOffset($current); $command = $this->client->getCommand('GetObject', $this->params); return $command->execute(); } /** * Performs an MD5 message integrity check if possible * * @throws UnexpectedValueException if the message does not validate */ protected function checkIntegrity() { if ($this->target->isReadable() && $expected = $this->meta['ContentMD5']) { $actual = $this->target->getContentMd5(); if ($actual != $expected) { throw new UnexpectedValueException( "Message integrity check failed. Expected {$expected} but got {$actual}." ); } } } }