friendica-addons/s3_storage/vendor/akeeba/s3/src/Connector.php

962 lines
27 KiB
PHP

<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4;
// Protection against direct access
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotDeleteFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetBucket;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotListBuckets;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotOpenFileForWrite;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error;
defined('AKEEBAENGINE') or die();
class Connector
{
/**
* Amazon S3 configuration object
*
* @var Configuration
*/
private $configuration = null;
/**
* Connector constructor.
*
* @param Configuration $configuration The configuration object to use
*/
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
}
/**
* Put an object to Amazon S3, i.e. upload a file. If the object already exists it will be overwritten.
*
* @param Input $input Input object
* @param string $bucket Bucket name. If you're using v4 signatures it MUST be on the region defined.
* @param string $uri Object URI. Think of it as the absolute path of the file in the bucket.
* @param string $acl ACL constant, by default the object is private (visible only to the uploading
* user)
* @param array $requestHeaders Array of request headers
*
* @return void
*
* @throws CannotPutFile If the upload is not possible
*/
public function putObject(Input $input, string $bucket, string $uri, string $acl = Acl::ACL_PRIVATE, array $requestHeaders = []): void
{
$request = new Request('PUT', $bucket, $uri, $this->configuration);
$request->setInput($input);
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
if (count($requestHeaders))
{
foreach ($requestHeaders as $h => $v)
{
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
{
$request->setAmzHeader(strtolower($h), $v);
}
else
{
$request->setHeader($h, $v);
}
}
}
if (isset($requestHeaders['Content-Type']))
{
$input->setType($requestHeaders['Content-Type']);
}
if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference()))))
{
throw new CannotPutFile('Missing input parameters', 0);
}
// We need to post with Content-Length and Content-Type, MD5 is optional
$request->setHeader('Content-Type', $input->getType());
$request->setHeader('Content-Length', $input->getSize());
if ($input->getMd5sum())
{
$request->setHeader('Content-MD5', $input->getMd5sum());
}
$request->setAmzHeader('x-amz-acl', $acl);
$response = $request->getResponse();
if ($response->code !== 200)
{
if (!$response->error->isError())
{
throw new CannotPutFile("Unexpected HTTP status {$response->code}", $response->code);
}
if (is_object($response->body) && ($response->body instanceof \SimpleXMLElement) && (strpos($input->getSize(), ',') === false))
{
// For some reason, trying to single part upload files on some hosts comes back with an inexplicable
// error from Amazon that we need to set Content-Length:5242880,5242880 instead of
// Content-Length:5242880 which is AGAINST Amazon's documentation. In this case we pass the header
// 'workaround-braindead-error-from-amazon' and retry. Uh, OK?
if (isset($response->body->CanonicalRequest))
{
$amazonsCanonicalRequest = (string) $response->body->CanonicalRequest;
$lines = explode("\n", $amazonsCanonicalRequest);
foreach ($lines as $line)
{
if (substr($line, 0, 15) != 'content-length:')
{
continue;
}
[$junk, $stupidAmazonDefinedContentLength] = explode(":", $line);
if (strpos($stupidAmazonDefinedContentLength, ',') !== false)
{
if (!isset($requestHeaders['workaround-braindead-error-from-amazon']))
{
$requestHeaders['workaround-braindead-error-from-amazon'] = 'you can\'t fix stupid';
$this->putObject($input, $bucket, $uri, $acl, $requestHeaders);
return;
}
}
}
}
}
}
if ($response->error->isError())
{
throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
);
}
}
/**
* Get (download) an object
*
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string|resource|null $saveTo Filename or resource to write to
* @param int|null $from Start of the download range, null to download the entire object
* @param int|null $to End of the download range, null to download the entire object
*
* @return string|null No return if $saveTo is specified; data as string otherwise
*
*/
public function getObject(string $bucket, string $uri, $saveTo = null, ?int $from = null, ?int $to = null): ?string
{
$request = new Request('GET', $bucket, $uri, $this->configuration);
$fp = null;
if (!is_resource($saveTo) && is_string($saveTo))
{
$fp = @fopen($saveTo, 'wb');
if ($fp === false)
{
throw new CannotOpenFileForWrite($saveTo);
}
}
if (is_resource($saveTo))
{
$fp = $saveTo;
}
if (is_resource($fp))
{
$request->setFp($fp);
}
// Set the range header
if ((!empty($from) && !empty($to)) || (!is_null($from) && !empty($to)))
{
$request->setHeader('Range', "bytes=$from-$to");
}
$response = $request->getResponse();
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)),
$response->error->getCode()
);
}
if (!is_resource($fp))
{
return $response->body;
}
return null;
}
/**
* Delete an object
*
* @param string $bucket Bucket name
* @param string $uri Object URI
*
* @return void
*/
public function deleteObject(string $bucket, string $uri): void
{
$request = new Request('DELETE', $bucket, $uri, $this->configuration);
$response = $request->getResponse();
if (!$response->error->isError() && ($response->code !== 204))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotDeleteFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s",
$response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
}
/**
* Get a query string authenticated URL
*
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param int|null $lifetime Lifetime in seconds
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
*
* @return string
*/
public function getAuthenticatedURL(string $bucket, string $uri, ?int $lifetime = null, bool $https = false): string
{
// Get a request from the URI and bucket
$questionmarkPos = strpos($uri, '?');
$query = '';
if ($questionmarkPos !== false)
{
$query = substr($uri, $questionmarkPos + 1);
$uri = substr($uri, 0, $questionmarkPos);
}
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !!!! DO NOT TOUCH THIS CODE. YOU WILL BREAK PRE-SIGNED URLS WITH v4 SIGNATURES. !!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* The following two lines seem weird and possibly extraneous at first glance. However, they are VERY important.
* If you remove them pre-signed URLs for v4 signatures will break! That's because pre-signed URLs with v4
* signatures follow different rules than with v2 signatures.
*
* Authenticated (pre-signed) URLs are always made against the generic S3 region endpoint, not the bucket's
* virtual-hosting-style domain name. The bucket is always the first component of the path.
*
* For example, given a bucket called foobar and an object baz.txt in it we are pre-signing the URL
* https://s3-eu-west-1.amazonaws.com/foobar/baz.txt, not
* https://foobar.s3-eu-west-1.amazonaws.com/foobar/baz.txt (as we'd be doing with v2 signatures).
*
* The problem is that the Request object needs to be created before we can convey the intent (regular request
* or generation of a pre-signed URL). As a result its constructor creates the (immutable) request URI solely
* based on whether the Configuration object's getUseLegacyPathStyle() returns false or not.
*
* Since we want to request URI to contain the bucket name we need to tell the Request object's constructor that
* we are creating a Request object for path-style access, i.e. the useLegacyPathStyle flag in the Configuration
* object is true. Naturally, the default behavior being virtual-hosting-style access to buckets, this flag is
* most likely **false**.
*
* Therefore we need to clone the Configuration object, set the flag to true and create a Request object using
* the falsified Configuration object.
*
* Note that v2 signatures are not affected. In v2 we are always appending the bucket name to the path, despite
* the fact that we include the bucket name in the domain name.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !!!! DO NOT TOUCH THIS CODE. YOU WILL BREAK PRE-SIGNED URLS WITH v4 SIGNATURES. !!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
$newConfig = clone $this->configuration;
$newConfig->setUseLegacyPathStyle(true);
// Create the request object.
$uri = str_replace('%2F', '/', rawurlencode($uri));
$request = new Request('GET', $bucket, $uri, $newConfig);
if ($query)
{
parse_str($query, $parameters);
if (count($parameters))
{
foreach ($parameters as $k => $v)
{
$request->setParameter($k, $v);
}
}
}
// Get the signed URI from the Request object
return $request->getAuthenticatedURL($lifetime, $https);
}
/**
* Get the location (region) of a bucket. You need this to use the V4 API on that bucket!
*
* @param string $bucket Bucket name
*
* @return string
*/
public function getBucketLocation(string $bucket): string
{
$request = new Request('GET', $bucket, '', $this->configuration);
$request->setParameter('location', null);
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
$result = 'us-east-1';
if ($response->hasBody())
{
$result = (string) $response->body;
}
switch ($result)
{
// "EU" is an alias for 'eu-west-1', however the canonical location name you MUST use is 'eu-west-1'
case 'EU':
case 'eu':
$result = 'eu-west-1';
break;
// If the bucket location is 'us-east-1' you get an empty string. @#$%^&*()!!
case '':
$result = 'us-east-1';
break;
}
return $result;
}
/**
* Get the contents of a bucket
*
* If maxKeys is null this method will loop through truncated result sets
*
* @param string $bucket Bucket name
* @param string|null $prefix Prefix (directory)
* @param string|null $marker Marker (last file listed)
* @param int|null $maxKeys Maximum number of keys ("files" and "directories") to return
* @param string $delimiter Delimiter, typically "/"
* @param bool $returnCommonPrefixes Set to true to return CommonPrefixes
*
* @return array
*/
public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
{
$request = new Request('GET', $bucket, '', $this->configuration);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
if (!empty($marker))
{
$request->setParameter('marker', $marker);
}
if (!empty($maxKeys))
{
$request->setParameter('max-keys', $maxKeys);
}
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
$results = [];
$nextMarker = null;
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->IsTruncated) &&
((string) $response->body->IsTruncated == 'false')
)
{
return $results;
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$nextMarker = (string) $response->body->NextMarker;
}
// Is it a truncated result?
$isTruncated = ($nextMarker !== null) && ((string) $response->body->IsTruncated == 'true');
// Is this a truncated result and no maxKeys specified?
$isTruncatedAndNoMaxKeys = ($maxKeys == null) && $isTruncated;
// Is this a truncated result with less keys than the specified maxKeys; and common prefixes found but not returned to the caller?
$isTruncatedAndNeedsContinue = ($maxKeys != null) && $isTruncated && (count($results) < $maxKeys);
// Loop through truncated results if maxKeys isn't specified
if ($isTruncatedAndNoMaxKeys || $isTruncatedAndNeedsContinue)
{
do
{
$request = new Request('GET', $bucket, '', $this->configuration);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
$request->setParameter('marker', $nextMarker);
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
try
{
$response = $request->getResponse();
}
catch (\Exception $e)
{
break;
}
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$nextMarker = (string) $response->body->NextMarker;
}
$continueCondition = false;
if ($isTruncatedAndNoMaxKeys)
{
$continueCondition = !$response->error->isError() && $isTruncated;
}
if ($isTruncatedAndNeedsContinue)
{
$continueCondition = !$response->error->isError() && $isTruncated && (count($results) < $maxKeys);
}
} while ($continueCondition);
}
if (!is_null($maxKeys))
{
$results = array_splice($results, 0, $maxKeys);
}
return $results;
}
/**
* Get a list of buckets
*
* @param bool $detailed Returns detailed bucket list when true
*
* @return array
*/
public function listBuckets(bool $detailed = false): array
{
// When listing buckets with the AWSv4 signature method we MUST set the region to us-east-1. Don't ask...
$configuration = clone $this->configuration;
$configuration->setRegion('us-east-1');
$request = new Request('GET', '', '', $configuration);
$response = $request->getResponse();
if (!$response->error->isError() && (($response->code !== 200)))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotListBuckets(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
$results = [];
if (!isset($response->body->Buckets))
{
return $results;
}
if ($detailed)
{
if (isset($response->body->Owner, $response->body->Owner->ID, $response->body->Owner->DisplayName))
{
$results['owner'] = [
'id' => (string) $response->body->Owner->ID,
'name' => (string) $response->body->Owner->DisplayName,
];
}
$results['buckets'] = [];
foreach ($response->body->Buckets->Bucket as $b)
{
$results['buckets'][] = [
'name' => (string) $b->Name,
'time' => strtotime((string) $b->CreationDate),
];
}
}
else
{
foreach ($response->body->Buckets->Bucket as $b)
{
$results[] = (string) $b->Name;
}
}
return $results;
}
/**
* Start a multipart upload of an object
*
* @param Input $input Input data
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $acl ACL constant
* @param array $requestHeaders Array of request headers
*
* @return string The upload session ID (UploadId)
*/
public function startMultipart(Input $input, string $bucket, string $uri, string $acl = Acl::ACL_PRIVATE, array $requestHeaders = []): string
{
$request = new Request('POST', $bucket, $uri, $this->configuration);
$request->setParameter('uploads', '');
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
if (is_array($requestHeaders))
{
foreach ($requestHeaders as $h => $v)
{
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
{
$request->setAmzHeader(strtolower($h), $v);
}
else
{
$request->setHeader($h, $v);
}
}
}
$request->setAmzHeader('x-amz-acl', $acl);
if (isset($requestHeaders['Content-Type']))
{
$input->setType($requestHeaders['Content-Type']);
}
$request->setHeader('Content-Type', $input->getType());
$response = $request->getResponse();
if (!$response->error->isError() && ($response->code !== 200))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
);
}
return (string) $response->body->UploadId;
}
/**
* Uploads a part of a multipart object upload
*
* @param Input $input Input data. You MUST specify the UploadID and PartNumber
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param array $requestHeaders Array of request headers or content type as a string
* @param int $chunkSize Size of each upload chunk, in bytes. It cannot be less than 5242880 bytes (5Mb)
*
* @return null|string The ETag of the upload part of null if we have ran out of parts to upload
*/
public function uploadMultipart(Input $input, string $bucket, string $uri, array $requestHeaders = [], int $chunkSize = 5242880): ?string
{
if ($chunkSize < 5242880)
{
$chunkSize = 5242880;
}
// We need a valid UploadID and PartNumber
$UploadID = $input->getUploadID();
$PartNumber = $input->getPartNumber();
if (empty($UploadID))
{
throw new CannotPutFile(
__METHOD__ . '(): No UploadID specified'
);
}
if (empty($PartNumber))
{
throw new CannotPutFile(
__METHOD__ . '(): No PartNumber specified'
);
}
$UploadID = urlencode($UploadID);
$PartNumber = (int) $PartNumber;
$request = new Request('PUT', $bucket, $uri, $this->configuration);
$request->setParameter('partNumber', $PartNumber);
$request->setParameter('uploadId', $UploadID);
$request->setInput($input);
// Full data length
$totalSize = $input->getSize();
// No Content-Type for multipart uploads
$input->setType(null);
// Calculate part offset
$partOffset = $chunkSize * ($PartNumber - 1);
if ($partOffset > $totalSize)
{
// This is to signify that we ran out of parts ;)
return null;
}
// How many parts are there?
$totalParts = floor($totalSize / $chunkSize);
if ($totalParts * $chunkSize < $totalSize)
{
$totalParts++;
}
// Calculate Content-Length
$size = $chunkSize;
if ($PartNumber >= $totalParts)
{
$size = $totalSize - ($PartNumber - 1) * $chunkSize;
}
if ($size <= 0)
{
// This is to signify that we ran out of parts ;)
return null;
}
$input->setSize($size);
switch ($input->getInputType())
{
case Input::INPUT_DATA:
$input->setData(substr($input->getData(), ($PartNumber - 1) * $chunkSize, $input->getSize()));
break;
case Input::INPUT_FILE:
case Input::INPUT_RESOURCE:
$fp = $input->getFp();
fseek($fp, ($PartNumber - 1) * $chunkSize);
break;
}
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
if (is_array($requestHeaders))
{
foreach ($requestHeaders as $h => $v)
{
if (strtolower(substr($h, 0, 6)) == 'x-amz-')
{
$request->setAmzHeader(strtolower($h), $v);
}
else
{
$request->setHeader($h, $v);
}
}
}
$request->setHeader('Content-Length', $input->getSize());
if ($input->getInputType() === Input::INPUT_DATA)
{
$request->setHeader('Content-Type', "application/x-www-form-urlencoded");
}
$response = $request->getResponse();
if ($response->code !== 200)
{
if (!$response->error->isError())
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if (is_object($response->body) && ($response->body instanceof \SimpleXMLElement) && (strpos($input->getSize(), ',') === false))
{
// For some moronic reason, trying to multipart upload files on some hosts comes back with a crazy
// error from Amazon that we need to set Content-Length:5242880,5242880 instead of
// Content-Length:5242880 which is AGAINST Amazon's documentation. In this case we pass the header
// 'workaround-broken-content-length' and retry. Whatever.
if (isset($response->body->CanonicalRequest))
{
$amazonsCanonicalRequest = (string) $response->body->CanonicalRequest;
$lines = explode("\n", $amazonsCanonicalRequest);
foreach ($lines as $line)
{
if (substr($line, 0, 15) != 'content-length:')
{
continue;
}
[$junk, $stupidAmazonDefinedContentLength] = explode(":", $line);
if (strpos($stupidAmazonDefinedContentLength, ',') !== false)
{
if (!isset($requestHeaders['workaround-broken-content-length']))
{
$requestHeaders['workaround-broken-content-length'] = true;
// This is required to reset the input size to its default value. If you don't do that
// only one part will ever be uploaded. Oops!
$input->setSize(-1);
return $this->uploadMultipart($input, $bucket, $uri, $requestHeaders, $chunkSize);
}
}
}
}
}
throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
);
}
// Return the ETag header
return $response->headers['hash'];
}
/**
* Finalizes the multi-part upload. The $input object should contain two keys, etags an array of ETags of the
* uploaded parts and UploadID the multipart upload ID.
*
* @param Input $input The array of input elements
* @param string $bucket The bucket where the object is being stored
* @param string $uri The key (path) to the object
*
* @return void
*/
public function finalizeMultipart(Input $input, string $bucket, string $uri): void
{
$etags = $input->getEtags();
$UploadID = $input->getUploadID();
if (empty($etags))
{
throw new CannotPutFile(
__METHOD__ . '(): No ETags array specified'
);
}
if (empty($UploadID))
{
throw new CannotPutFile(
__METHOD__ . '(): No UploadID specified'
);
}
// Create the message
$message = "<CompleteMultipartUpload>\n";
$part = 0;
foreach ($etags as $etag)
{
$part++;
$message .= "\t<Part>\n\t\t<PartNumber>$part</PartNumber>\n\t\t<ETag>\"$etag\"</ETag>\n\t</Part>\n";
}
$message .= "</CompleteMultipartUpload>";
// Get a request query
$reqInput = Input::createFromData($message);
$request = new Request('POST', $bucket, $uri, $this->configuration);
$request->setParameter('uploadId', $UploadID);
$request->setInput($reqInput);
// Do post
$request->setHeader('Content-Type', 'application/xml'); // Even though the Amazon API doc doesn't mention it, it's required... :(
$response = $request->getResponse();
if (!$response->error->isError() && ($response->code != 200))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
if ($response->error->getCode() == 'RequestTimeout')
{
return;
}
throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true))
);
}
}
/**
* Returns the configuration object
*
* @return Configuration
*/
public function getConfiguration(): Configuration
{
return $this->configuration;
}
}