if (false === $rest->error && 204 !== $rest->code) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::deleteBucket({$bucket}): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
* Create input info array for putObject()
* @param string $file Input file
* @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own)
public function inputFile($file, $md5sum = true) {
if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
$this->__triggerError('UpdraftPlus_S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__);
return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ?
(is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : '', 'sha256sum' => hash_file('sha256', $file));
* Create input array info for putObject() with a resource
* @param string $resource Input resource to read from
* @param integer $bufferSize Input byte size
* @param string $md5sum MD5 hash to send (optional)
public function inputResource(&$resource, $bufferSize, $md5sum = '') {
if (!is_resource($resource) || $bufferSize < 0) {
$this->__triggerError('UpdraftPlus_S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
$input = array('size' => $bufferSize, 'md5sum' => $md5sum);
$input['fp'] =& $resource;
* Initiate a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html)
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param array $requestHeaders Array of request headers or content type as a string
* @param string $storageClass Storage class constant
public function initiateMultipartUpload ($bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
$rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest->setParameter('uploads','');
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
if (is_array($requestHeaders))
foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
$rest->setAmzHeader('x-amz-storage-class', $storageClass);
$rest->setAmzHeader('x-amz-acl', $acl);
foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
// Carry out the HTTP operation
if (false === $rest->response->error && 200 !== $rest->response->code) {
$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->response->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::initiateMultipartUpload(): [%s] %s",
$rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
} elseif (isset($rest->response->body)) {
// DreamObjects already returns a SimpleXMLElement here. Not sure how that works.
if (is_a($rest->response->body, 'SimpleXMLElement')) {
$body = $rest->response->body;
$body = new SimpleXMLElement($rest->response->body);
return (string) $body->UploadId;
// It is a programming error if we reach this line
* Upload a part of a multi-part set (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadUploadPart.html)
* The chunk is read into memory, so make sure that you have enough (or patch this function to work another way!)
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $uploadId uploadId returned previously from initiateMultipartUpload
* @param integer $partNumber sequential part number to upload
* @param string $filePath file to upload content from
* @param integer $partSize number of bytes in each part (though final part may have fewer) - pass the same value each time (for this particular upload) - default 5Mb (which is Amazon's minimum)
* @return string (ETag) | false
public function uploadPart ($bucket, $uri, $uploadId, $filePath, $partNumber, $partSize = 5242880) {
$rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest->setParameter('partNumber', $partNumber);
$rest->setParameter('uploadId', $uploadId);
$fileOffset = ($partNumber - 1 ) * $partSize;
// Download the smallest of the remaining bytes and the part size
$fileBytes = min(filesize($filePath) - $fileOffset, $partSize);
if ($fileBytes < 0) $fileBytes = 0;
$rest->setHeader('Content-Type', 'application/octet-stream');
if ($handle = fopen($filePath, "rb")) {
if ($fileOffset >0) fseek($handle, $fileOffset);
while ($fileBytes>0 && $read = fread($handle, max($fileBytes, 131072))) {
$fileBytes = $fileBytes - strlen($read);
$bytes_read += strlen($read);
$rest->data = $rest->data . $read;
$rest->setHeader('Content-MD5', base64_encode(md5($rest->data, true)));
$rest->size = $bytes_read;
$rest = $rest->getResponse();
if (false === $rest->error && 200 !== $rest->code) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::uploadPart(): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
return $rest->headers['hash'];
* Complete a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadComplete.html)
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $uploadId uploadId returned previously from initiateMultipartUpload
* @param array $parts an ordered list of eTags of previously uploaded parts from uploadPart
public function completeMultipartUpload($bucket, $uri, $uploadId, $parts) {
$rest = new UpdraftPlus_S3Request('POST', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest->setParameter('uploadId', $uploadId);
$xml = "<CompleteMultipartUpload>\n";
foreach ($parts as $etag) {
$xml .= "<Part><PartNumber>$partno</PartNumber><ETag>$etag</ETag></Part>\n";
$xml .= "</CompleteMultipartUpload>";
$rest->size = strlen($rest->data);
$rest->setHeader('Content-Type', 'application/xml');
$rest = $rest->getResponse();
if (false === $rest->error && 200 !== $rest->code) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
// Special case: when the error means "you've already done that". Turn it into success. See in: https://trello.com/c/6jJoiCG5
if ('InternalError' == $rest->error['code'] && 'This multipart completion is already in progress' == $rest->error['message']) {
$this->__triggerError(sprintf("UpdraftPlus_S3::completeMultipartUpload(): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
* Abort a multi-part upload (http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadAbort.html)
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $uploadId uploadId returned previously from initiateMultipartUpload
public function abortMultipartUpload ($bucket, $uri, $uploadId) {
$rest = new UpdraftPlus_S3Request('DELETE', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest->setParameter('uploadId', $uploadId);
$rest = $rest->getResponse();
if (false === $rest->error && 204 !== $rest->code) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::abortMultipartUpload(): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
* @param mixed $input Input data
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param array $requestHeaders Array of request headers or content type as a string
* @param string $storageClass Storage class constant
public function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
if ($input === false) return false;
$rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
if (!is_array($input)) $input = array(
'data' => $input, 'size' => strlen($input),
'md5sum' => base64_encode(md5($input, true)),
'sha256sum' => hash('sha256', $input)
$rest->fp =& $input['fp'];
elseif (isset($input['file']) && is_file($input['file']))
$rest->fp = @fopen($input['file'], 'rb');
elseif (isset($input['data']))
$rest->data = $input['data'];
// Content-Length (required)
if (isset($input['size']) && $input['size'] >= 0) {
$rest->size = $input['size'];
if (isset($input['file']))
$rest->size = filesize($input['file']);
elseif (isset($input['data']))
$rest->size = strlen($input['data']);
// Custom request headers (Content-Type, Content-Disposition, Content-Encoding)
if (is_array($requestHeaders))
foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
elseif (is_string($requestHeaders)) // Support for legacy contentType parameter
$input['type'] = $requestHeaders;
if (!isset($input['type'])) {
if (isset($requestHeaders['Content-Type']))
$input['type'] =& $requestHeaders['Content-Type'];
elseif (isset($input['file']))
$input['type'] = $this->__getMimeType($input['file']);
$input['type'] = 'application/octet-stream';
if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class
$rest->setAmzHeader('x-amz-storage-class', $storageClass);
if (!empty($this->_serverSideEncryption)) {
$rest->setAmzHeader('x-amz-server-side-encryption', $this->_serverSideEncryption);
// We need to post with Content-Length and Content-Type, MD5 is optional
if ($rest->size >= 0 && (false !== $rest->fp || false !== $rest->data)) {
$rest->setHeader('Content-Type', $input['type']);
if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']);
if (isset($input['sha256sum'])) $rest->setAmzHeader('x-amz-content-sha256', $input['sha256sum']);
$rest->setAmzHeader('x-amz-acl', $acl);
foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
$rest->response->error = array('code' => 0, 'message' => 'Missing input parameters');
if (false === $rest->response->error && 200 !== $rest->response->code) {
$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->response->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::putObject(): [%s] %s",
$rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
* Put an object from a file (legacy function)
* @param string $file Input file path
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param string $contentType Content type
* @param string $storageClass
public function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null, $storageClass = self::STORAGE_CLASS_STANDARD) {
return $this->putObject($this->inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType, $storageClass);
* Put an object from a string (legacy function)
* @param string $string Input data
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param string $contentType Content type
public function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') {
return $this->putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param mixed $saveTo Filename or resource to write to
* @param mixed $resume - if $saveTo is a resource, then this is either false or the value for a Range: header; otherwise, a boolean, indicating whether to resume if possible.
public function getObject($bucket, $uri, $saveTo = false, $resume = false) {
$rest = new UpdraftPlus_S3Request('GET', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
if (is_resource($saveTo)) {
if (!is_bool($resume)) $rest->setHeader('Range', $resume);
if ($resume && file_exists($saveTo)) {
if (false !== ($rest->fp = @fopen($saveTo, 'ab'))) {
$rest->setHeader('Range', "bytes=".filesize($saveTo).'-');
$rest->file = realpath($saveTo);
$rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
if (false !== ($rest->fp = @fopen($saveTo, 'wb')))
$rest->file = realpath($saveTo);
$rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo);
if (false === $rest->response->error) $rest->getResponse();
if (false === $rest->response->error && ( !$resume && 200 != $rest->response->code) || ( $resume && 206 != $rest->response->code && 200 != $rest->response->code))
$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->response->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::getObject({$bucket}, {$uri}): [%s] %s",
$rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__);
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param boolean $returnInfo Return response information
public function getObjectInfo($bucket, $uri, $returnInfo = true) {
$rest = new UpdraftPlus_S3Request('HEAD', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest = $rest->getResponse();
if (false === $rest->error && (200 !== $rest->code && 404 !== $rest->code))
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::getObjectInfo({$bucket}, {$uri}): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
return (200 == $rest->code) ? ($returnInfo ? $rest->headers : true) : false;
* @param string $bucket Source bucket name
* @param string $uri Source object URI
* @param string $bucket Destination bucket name
* @param string $uri Destination object URI
* @param string $acl ACL constant
* @param array $metaHeaders Optional array of x-amz-meta-* headers
* @param array $requestHeaders Optional array of request headers (content type, disposition, etc.)
* @param string $storageClass Storage class constant
public function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) {
$rest = new UpdraftPlus_S3Request('PUT', $bucket, $uri, $this->endpoint, $this->use_dns_bucket_name, $this);
$rest->setHeader('Content-Length', 0);
foreach ($requestHeaders as $h => $v) $rest->setHeader($h, $v);
foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v);
if (self::STORAGE_CLASS_STANDARD !== $storageClass) // Storage class
$rest->setAmzHeader('x-amz-storage-class', $storageClass);
$rest->setAmzHeader('x-amz-acl', $acl);
$rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri)));
if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0)
$rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE');
$rest = $rest->getResponse();
if (false === $rest->error && 200 !== $rest->code) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
if (false !== $rest->error) {
$this->__triggerError(sprintf("UpdraftPlus_S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s",
$rest->error['code'], $rest->error['message']), __FILE__, __LINE__);
return isset($rest->body->LastModified, $rest->body->ETag) ? array(
'time' => strtotime((string)$rest->body->LastModified),
'hash' => substr((string)$rest->body->ETag, 1, -1)
* Set logging for a bucket
* @param string $bucket Bucket name
* @param string $targetBucket Target bucket (where logs are stored)
* @param string $targetPrefix Log prefix (e,g; domain.com-)
public function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) {
// The S3 log delivery group has to be added to the target bucket's ACP
if (null !== $targetBucket && false !== ($acp = $this->getAccessControlPolicy($targetBucket, ''))) {
// Only add permissions to the target bucket when they do not exist
foreach ($acp['acl'] as $acl)
if ('Group' == $acl['type'] && 'http://acs.amazonaws.com/groups/s3/LogDelivery' == $acl['uri']) {
if ($acl['permission'] == 'WRITE') $aclWriteSet = true;
elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true;
if (!$aclWriteSet) $acp['acl'][] = array(
'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE'
if (!$aclReadSet) $acp['acl'][] = array(
'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP'
if (!$aclReadSet || !$aclWriteSet) $this->setAccessControlPolicy($targetBucket, '', $acp);
$bucketLoggingStatus = $dom->createElement('BucketLoggingStatus');
$bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/');
if (null !== $targetBucket) {
if (null == $targetPrefix) $targetPrefix = $bucket . '-';
$loggingEnabled = $dom->createElement('LoggingEnabled');
$loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket));
$loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix));
// TODO: Add TargetGrants?
$bucketLoggingStatus->appendChild($loggingEnabled);
$dom->appendChild($bucketLoggingStatus);