'',
'Date' => '',
'Content-MD5' => '',
'Content-Type' => '',
);
public $fp = false; // Use HTTP PUT?
public $size = 0; // PUT file size.
public $data = false; // PUT post fields.
public $response; // S3 request respone.
/**
* Constructor - if you're not using the class statically.
* @param string $accessKey Access key
* @param string $secretKey Secret key
* @param bool $useSSL Enable SSL
* @param string $endpoint Amazon URI
*/
public function __construct($accessKey = null, $secretKey = null, $useSSL = false, $endpoint = 's3.amazonaws.com', $region = '') {
if ($accessKey !== null && $secretKey !== null) {
$this->setAuth($accessKey, $secretKey);
}
$this->useSSL = $useSSL;
$this->setSSL(false, false);
$this->endpoint = $endpoint;
$this->region = $region;
}
/**
* Set the service endpoint.
* @param string $host Hostname
*/
public function setEndpoint($host) {
$this->endpoint = $host;
}
/**
* Set the service region.
* @param string $region
*/
public function setRegion($region) {
$this->region = $region;
}
/**
* Get the service region.
* @return string $region
*/
public function getRegion() {
$region = $this->region;
// parse region from endpoint if not specific
if (empty($region)) {
if (preg_match("/s3[.-](?:website-|dualstack\.)?(.+)\.amazonaws\.com/i", $this->endpoint, $match) !== 0 && strtolower($match[1]) !== 'external-1') {
$region = $match[1];
}
}
return empty($region) ? 'us-east-1' : $region;
}
/**
* Set AWS access key and secret key.
* @param string $accessKey Access key
* @param string $secretKey Secret key
*/
public function setAuth($accessKey, $secretKey) {
$this->__accessKey = $accessKey;
$this->__secretKey = $secretKey;
}
/**
* Check if AWS keys have been set.
* @return bool
*/
public function hasAuth() {
return $this->__accessKey !== null && $this->__secretKey !== null;
}
/**
* Set SSL on or off.
* @param bool $enabled SSL enabled
* @param bool $validate SSL certificate validation
*/
public function setSSL($enabled, $validate = true) {
$this->useSSL = $enabled;
$this->useSSLValidation = $validate;
}
/**
* Set SSL client certificates (experimental).
* @param string $sslCert SSL client certificate
* @param string $sslKey SSL client key
* @param string $sslCACert SSL CA cert (only required if you are having problems with your system CA cert)
*/
public function setSSLAuth($sslCert = null, $sslKey = null, $sslCACert = null) {
$this->sslCert = $sslCert;
$this->sslKey = $sslKey;
$this->sslCACert = $sslCACert;
}
/**
* Set the error mode to exceptions.
* @param bool $enabled Enable exceptions
*/
public function setExceptions($enabled = true) {
$this->useExceptions = $enabled;
}
/**
* 设置head请求是否可用,不可用时替换为get请求
* head请求通常是可用的,但在使用nginx转发,开启缓存且无特别指定时(proxy_cache_methods GET HEAD;),head会被转为get导致签名异常
* https://serverfault.com/questions/530763/nginx-proxy-cache-key-and-head-get-request
*/
public function setHeadValid($enabled = true){
$this->headValid = $enabled;
}
/**
* Set AWS time correction offset (use carefully).
* This can be used when an inaccurate system time is generating
* invalid request signatures. It should only be used as a last
* resort when the system time cannot be changed.
*
* @param string $offset Time offset (set to zero to use AWS server time)
*/
public function setTimeCorrectionOffset($offset = 0) {
if ($offset == 0) {
$rest = $this->s3Request('HEAD');
$rest = $rest->getResponse();
$awstime = $rest->headers['date'];
$systime = time();
$offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime);
}
$this->__timeOffset = $offset;
}
/**
* Set signing key.
* @param string $keyPairId AWS Key Pair ID
* @param string $signingKey Private Key
* @param bool $isFile Load private key from file, set to false to load string
*
* @return bool
*/
public function setSigningKey($keyPairId, $signingKey, $isFile = true) {
$this->__signingKeyPairId = $keyPairId;
if (($this->__signingKeyResource = openssl_pkey_get_private($isFile ?
file_get_contents($signingKey) : $signingKey)) !== false) {
return true;
}
$this->__triggerError('S3->setSigningKey(): Unable to open load private key: ' . $signingKey, __FILE__, __LINE__);
return false;
}
/**
* Set Signature Version.
* @param string $version of signature ('v4' or 'v2')
*/
public function setSignatureVersion($version = 'v2') {
$this->signVer = $version;
}
/**
* Free signing key from memory, MUST be called if you are using setSigningKey().
*/
public function freeSigningKey() {
if ($this->__signingKeyResource !== false) {
openssl_free_key($this->__signingKeyResource);
}
}
/**
* Set progress function.
* @param function $func Progress function
*/
public function setProgressFunction($func = null) {
$this->progressFunction = $func;
}
/**
* Internal error handler.
* @internal Internal error handler
* @param string $message Error message
* @param string $file Filename
* @param int $line Line number
* @param int $code Error code
*/
private function __triggerError($message, $file, $line, $code = 0) {
if ($this->useExceptions) {
throw new S3Exception($message, $file, $line, $code);
} else {
trigger_error($message, E_USER_WARNING);
}
}
/**
* Get response error message.
*/
private function __errorMessage($body) {
if (!$body || !is_array($body)) return '';
$body = array_change_key_case($body, CASE_LOWER);
static $lang = null;
if (!$lang) $lang = I18n::getType();
if ($lang != 'zh-CN') return _get($body, 'message', '');
$data = array(
'AccessDenied' => '拒绝访问,请检查配置参数及资源权限。',
'InvalidArgument' => '参数格式错误。',
'InternalError' => '内部服务器错误。',
'InvalidAccessKeyId' => '无效的AccessKeyId。',
'InvalidBucketName' => '无效的Bucket名称。',
'InvalidObjectName' => '无效的文件名称。',
'NoSuchBucket' => '指定的Bucket不存在。',
'NoSuchKey' => '指定的文件不存在。',
'SignatureDoesNotMatch' => '签名错误,请检查密码等参数是否正确。',
);
if (isset($body['code'])) {
$code = strtolower($body['code']);
$data = array_change_key_case($data, CASE_LOWER);
if (isset($data[$code])) return $data[$code];
}
$message = _get($body, 'message', '');
if (!$message) return '';
if (stripos($message, 'timed out')) return '连接超时,检查网络及节点地址是否正常。';
return str_replace('Could not resolve host', '无法连接主机', $message);
}
/**
* Process CURL response
* @param type $rest
* @param type $function
* @param type $noBody
* @param type $params
* @param type $code
* @return boolean
*/
private function __execReponse($rest, $function, $noBody = 0, $params = array(), $code = 200) {
$body = false;
if (isset($rest->body)) {
if (is_string($rest->body)) {
$body = xml2json($rest->body);
} else {
$body = json_decode(json_encode($rest->body), true);
}
}
if ($rest->error === false && $rest->code !== $code) {
$message = $this->__errorMessage($body);
if (!$message) $message = 'Unexpected HTTP status.['.$rest->code.']';
$rest->error = array('code' => $rest->code, 'message' => $message);
}
if ($rest->error !== false) {
$param = implode(',', $params);
$message = $this->__errorMessage($rest->error);
$this->__triggerError('S3->'.$function.'('.$param.') ['.$rest->error['code'].'] '.$message, __FILE__, __LINE__);
return false;
}
return $noBody ? true : $body;
}
/**
* Get a list of buckets.
* @param bool $detailed Returns detailed bucket list when true
* @return array | false
*/
public function listBuckets($detailed = false) {
$rest = $this->s3Request('GET', '', '', $this->endpoint);
$rest = $rest->getResponse();
if (!$body = $this->__execReponse($rest, __FUNCTION__))
return false;
$results = array();
if (!isset($body['Buckets'])) {
return $results;
}
if(isset($body['Buckets']['Bucket']['Name'])){
$body['Buckets']['Bucket'] = array($body['Buckets']['Bucket']);
}
if (!$detailed) {
foreach ($body['Buckets']['Bucket'] as $bkt) {
$results[] = $bkt['Name'];
}
return $results;
}
// 详细信息
if (isset($body['Owner'], $body['Owner']['ID'])) {
$results['owner'] = array(
'id' => $body['Owner']['ID'],
);
if (isset($body['Owner']['DisplayName'])) {
$results['owner']['name'] = $body['Owner']['DisplayName'];
}
}
$results['buckets'] = array();
foreach ($body['Buckets']['Bucket'] as $bkt) {
$results['buckets'][] = array(
'name' => $bkt['Name'],
'time' => strtotime($bkt['CreationDate'])
);
}
return $results;
}
/**
* Get contents for a bucket.
* If maxKeys is null this method will loop through truncated result sets
* @param string $bucket Bucket name
* @param string $prefix Prefix
* @param string $marker Marker (last file listed)
* @param string $maxKeys Max keys (maximum number of keys to return)
* @param string $delimiter Delimiter
* @param bool $returnCommonPrefixes Set to true to return CommonPrefixes
*
* @return array | false
*/
public function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) {
$results = array('listObject' => array(), 'listPrefix' => array(), 'nextMarker' => null);
$listObject = $listPrefix = array();
$nextMarker = null;
do{
check_abort();
$rest = $this->s3Request('GET', $bucket, '', $this->endpoint);
if ($maxKeys == 0) {
$maxKeys = null;
}
// $rest->setParameter('list-type', 2); // 与marker冲突
if ($prefix !== null && $prefix !== '') {
$rest->setParameter('prefix', $prefix);
}
if ($marker !== null && $marker !== '') {
$rest->setParameter('marker', $marker);
}
if ($maxKeys !== null && $maxKeys !== '') {
$rest->setParameter('max-keys', $maxKeys);
}
if ($delimiter !== null && $delimiter !== '') {
$rest->setParameter('delimiter', $delimiter);
} elseif (!empty($this->defDelimiter)) {
$rest->setParameter('delimiter', $this->defDelimiter);
}
if ($nextMarker !== null && $nextMarker !== '') {
$rest->setParameter('marker', $nextMarker);
}
$response = $rest->getResponse();
if (!$body = $this->__execReponse($response, __FUNCTION__))
return false;
if(isset($body['Contents'])){
if(isset($body['Contents']['Key'])){
$body['Contents'] = array($body['Contents']);
}
foreach ($body['Contents'] as $c){
$listObject[$c['Key']] = array(
'name' => $c['Key'],
'time' => strtotime($c['LastModified']),
'size' => (int) $c['Size'],
'hash' => trim($c['ETag'], '"'),
);
$nextMarker = $c['Key'];
}
}
if ($returnCommonPrefixes && isset($body['CommonPrefixes'])) {
if(isset($body['CommonPrefixes']['Prefix'])){
$body['CommonPrefixes'] = array($body['CommonPrefixes']);
}
foreach ($body['CommonPrefixes'] as $c) {
$listPrefix[$c['Prefix']] = array('name' => $c['Prefix']);
}
}
if (isset($body['NextMarker'])) {
$nextMarker = $body['NextMarker'];
}
}while(($maxKeys == null && $body && $nextMarker != null && $body['IsTruncated'] == 'true'));
$results['listObject'] = array_values($listObject);
$results['listPrefix'] = array_values($listPrefix);
$results['nextMarker'] = $nextMarker;
return $results;
}
/**
* Put a bucket.
* @param string $bucket Bucket name
* @param constant $acl ACL flag
* @param string $location Set as "EU" to create buckets hosted in Europe
* @return bool
*/
public function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) {
$rest = $this->s3Request('PUT', $bucket, '', $this->endpoint);
$rest->setAmzHeader('x-amz-acl', $acl);
if ($location === false) {
$location = $this->getRegion();
}
if ($location !== false && $location !== 'us-east-1') {
$dom = new DOMDocument();
$createBucketConfiguration = $dom->createElement('CreateBucketConfiguration');
$locationConstraint = $dom->createElement('LocationConstraint', $location);
$createBucketConfiguration->appendChild($locationConstraint);
$dom->appendChild($createBucketConfiguration);
$rest->data = $dom->saveXML();
$rest->size = strlen($rest->data);
$rest->setHeader('Content-Type', 'application/xml');
}
$rest = $rest->getResponse();
return $this->__execReponse($rest, __FUNCTION__, 1, array($bucket, $acl, $location));
}
/**
* Delete an empty bucket.
* @param string $bucket Bucket name
* @return bool
*/
public function deleteBucket($bucket) {
$rest = $this->s3Request('DELETE', $bucket, '', $this->endpoint);
$rest = $rest->getResponse();
$code = $rest->code == '200' ? 200 : 204;
return $this->__execReponse($rest, __FUNCTION__, 1, array($bucket), $code);
}
/**
* get location(region) of bucket
* @param [type] $bucket
* @return void
*/
public function getBucketRegion($bucket) {
$rest = $this->s3Request('GET', $bucket, '', $this->endpoint);
$rest->setParameter('location', '');
$rest = $rest->getResponse();
$body = $this->__execReponse($rest, __FUNCTION__, 0, array($bucket));
// if ($body === false) return false;
return isset($body[0]) ? $body[0] : 'us-east-1'; // LocationConstraint
}
/**
* get cors of bucket
* @param [type] $bucket
* @return void
*/
public function getBucketCors($bucket) {
$rest = $this->s3Request('GET', $bucket, '', $this->endpoint);
$rest->setParameter('cors', '');
$rest = $rest->getResponse();
if (!$body = $this->__execReponse($rest, __FUNCTION__, 0, array($bucket)))
return false;
return isset($body['CORSRule']) ? $body['CORSRule'] : false;
}
/**
* set cors of bucket
* @param [type] $bucket
* @return void
*/
public function setBucketCors($bucket) {
$xmlStr = ""
. ""
. "*"
. "GET"
. "PUT"
. "POST"
. "DELETE"
. "HEAD"
. "600"
. "ETag"
. "*"
. ""
. "";
$xml = new SimpleXMLElement($xmlStr);
$body = $xml->asXML();
$rest = $this->s3Request('PUT', $bucket, '', $this->endpoint);
$rest->setHeader('Content-Type', 'application/xml');
$rest->setHeader('Content-MD5', $this->__base64(md5($body)));
$rest->setHeader('Content-Length', strlen($body));
$rest->setParameter('cors', '');
$rest->setBody($body);
$rest = $rest->getResponse();
return $this->__execReponse($rest, __FUNCTION__, 1);
}
/**
* 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)
* @return array | false
*/
public function inputFile($file, $md5sum = true) {
if (!file_exists($file) || !is_file($file) || !is_readable($file)) {
$this->__triggerError('S3->inputFile(): Unable to open input file: ' . $file, __FILE__, __LINE__);
return false;
}
clearstatcache(false, $file);
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 int $bufferSize Input byte size
* @param string $md5sum MD5 hash to send (optional)
* @return array | false
*/
public function inputResource(&$resource, $bufferSize = false, $md5sum = '') {
if (!is_resource($resource) || (int) $bufferSize < 0) {
$this->__triggerError('S3->inputResource(): Invalid resource or buffer size', __FILE__, __LINE__);
return false;
}
// Try to figure out the bytesize
if ($bufferSize === false) {
if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) {
$this->__triggerError('S3->inputResource(): Unable to obtain resource size', __FILE__, __LINE__);
return false;
}
fseek($resource, 0);
}
$input = array('size' => $bufferSize, 'md5sum' => $md5sum);
$input['fp'] = &$resource;
return $input;
}
/**
* Put an object.
* @param mixed $input Input data
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param constant $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 constant $storageClass Storage class constant
* @param constant $serverSideEncryption Server-side encryption
* @return bool/array
*/
public function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) {
if ($input === false) {
return false;
}
$rest = $this->s3Request('PUT', $bucket, $uri, $this->endpoint);
if (!is_array($input)) {
$input = array(
'data' => $input,
'size' => strlen($input),
'md5sum' => base64_encode(md5($input, true)),
'sha256sum' => hash('sha256', $input),
);
}
// Data
if (isset($input['fp'])) {
$rest->fp = &$input['fp'];
} elseif (isset($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'];
} else {
if (isset($input['file'])) {
clearstatcache(false, $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) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
} elseif (is_string($requestHeaders)) { // Support for legacy contentType parameter
$input['type'] = $requestHeaders;
}
// Content-Type
if (!isset($input['type'])) {
if (isset($requestHeaders['Content-Type'])) {
$input['type'] = &$requestHeaders['Content-Type'];
} elseif (isset($input['file'])) {
$input['type'] = $this->__getMIMEType($input['file']);
} else {
$input['type'] = 'application/octet-stream';
}
}
if ($storageClass !== self::STORAGE_CLASS_STANDARD) { // Storage class
$rest->setAmzHeader('x-amz-storage-class', $storageClass);
}
if ($serverSideEncryption !== self::SSE_NONE) { // Server-side encryption
$rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption);
}
// We need to post with Content-Length and Content-Type, MD5 is optional
if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) {
$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 = $rest->getResponse();
if(!$this->__execReponse($rest, __FUNCTION__, 1)){
return false;
}
return $rest->headers;
}
return false;
}
/**
* 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 constant $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param string $contentType Content type
* @return bool
*/
public function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) {
// TODO jos返回结果中size=0,不能直接使用其结果,其他待确认
return $this->putObject($this->inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType);
}
/**
* Put an object from a string (legacy function).
* @param string $string Input data
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param constant $acl ACL constant
* @param array $metaHeaders Array of x-amz-meta-* headers
* @param string $contentType Content type
* @return bool
*/
public function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) {
return $this->putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType);
}
/**
* Get uploadId [Multipart upload/copy].
* @param type $bucket
* @param type $uri
* @return bool
*/
public function getUploadId($bucket, $uri, $metaHeaders = array(), $requestHeaders = array()) {
$rest = $this->s3Request('POST', $bucket, $uri, $this->endpoint);
$rest->setParameter('uploads', '');
foreach ($requestHeaders as $h => $v) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
foreach ($metaHeaders as $h => $v) {
$rest->setAmzHeader('x-amz-meta-' . $h, $v);
}
$rest = $rest->getResponse();
if (!$body = $this->__execReponse($rest, __FUNCTION__, 0, array($bucket, $uri)))
return false;
return isset($body['UploadId']) ? $body['UploadId'] : false;
}
/**
* Chunk copy.
* @param type $srcBucket
* @param type $file
* @param type $bucket
* @param type $uri
* @return type
*/
public function multiCopyObject($srcBucket, $file, $bucket, $uri, $metaHeaders = array(), $requestHeaders = array()) {
$uploadId = $this->getUploadId($bucket, $uri, $metaHeaders, $requestHeaders);
if (!$uploadId) return false;
$info = $this->getObjectInfo($srcBucket, $file);
if (!$info) return false;
$fileSize = $info['size'];
$uploadPosition = 0;
$pieces = $this->__generateParts($fileSize, self::MIN_PART_SIZE);
$partList = array();
foreach ($pieces as $i => $piece) {
$fromPos = $uploadPosition + (int) $piece[self::AMZ_SEEK_TO];
$toPos = (int) $piece[self::AMZ_LENGTH] + $fromPos - 1;
$requestHeaders = array(
'x-amz-copy-source' => sprintf('/%s/%s', $srcBucket, rawurlencode($file)),
'x-amz-copy-source-range' => "bytes={$fromPos}-{$toPos}",
);
$tryCnt = 0;
do {
$tryCnt++;
$etag = $this->uploadPart($bucket, $uri, ($i + 1), $uploadId, $requestHeaders);
} while (!$etag && $tryCnt < self::UPLOAD_RETRY);
if (!$etag) return false;
$partList[] = array('PartNumber' => ($i + 1), 'ETag' => $etag);
}
if (!empty($partList)) {
$tryCnt = 0;
do {
$tryCnt++;
$complete = $this->completeMultiUpload($bucket, $uri, $uploadId, $partList);
} while (!$complete && $tryCnt < self::UPLOAD_RETRY);
return $complete;
}
return false;
}
/**
* Chunk upload.
* @param type $file
* @param type $bucket
* @param type $uri
* @param type $metaHeaders
* @param array $requestHeaders
* @return bool|string
*/
public function multiUploadObject($file, $bucket, $uri, $metaHeaders = array(), $requestHeaders = array()) {
$uploadId = $this->getUploadId($bucket, $uri, $metaHeaders, $requestHeaders);
if (!$uploadId) return false;
$fileSize = filesize($file);
$pieces = $this->__generateParts($fileSize, self::MIN_PART_SIZE);
$partList = array();
foreach ($pieces as $i => $piece) {
$chunkData = array(
'file' => $file,
'offset' => (int) $piece[self::AMZ_SEEK_TO],
'length' => (int) $piece[self::AMZ_LENGTH]
);
$requestHeaders = array(
'Content-Type' => 'application/octet-stream',
'Content-Length' => $chunkData['length'],
);
$tryCnt = 0;
do {
$tryCnt++;
$etag = $this->uploadPart($bucket, $uri, ($i + 1), $uploadId, $requestHeaders, $chunkData);
} while (!$etag && $tryCnt < self::UPLOAD_RETRY);
if (!$etag) return false;
$partList[] = array('PartNumber' => ($i + 1), 'ETag' => $etag);
}
if (!empty($partList)) {
$tryCnt = 0;
do {
$tryCnt++;
$complete = $this->completeMultiUpload($bucket, $uri, $uploadId, $partList);
} while (!$complete && $tryCnt < self::UPLOAD_RETRY);
return $complete;
}
return false;
}
/**
* Complete multipart upload object.
* @param type $bucket
* @param type $uri
* @param type $uploadId
* @param type $data
* @param type $metaHeaders
* @param type $requestHeaders
* @return bool
*/
public function completeMultiUpload($bucket, $uri, $uploadId, $data, $metaHeaders = array(), $requestHeaders = array()) {
$xmlStr = "";
foreach ($data as $part) {
$xmlStr .= ''
. "{$part['PartNumber']}"
. "{$part['ETag']}"
. '';
}
$xmlStr .= '';
$xml = new SimpleXMLElement($xmlStr);
$body = $xml->asXML();
$rest = $this->s3Request('POST', $bucket, $uri, $this->endpoint);
$rest->setHeader('Content-Type', 'application/octet-stream');
$rest->setHeader('Content-MD5', $this->__base64(md5($body)));
$rest->setHeader('Content-Length', strlen($body));
$rest->setParameter('uploadId', $uploadId);
$rest->setBody($body);
foreach ($requestHeaders as $h => $v) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
foreach ($metaHeaders as $h => $v) {
$rest->setAmzHeader('x-amz-meta-' . $h, $v);
}
$rest = $rest->getResponse();
return $this->__execReponse($rest, __FUNCTION__, 1);
}
/**
* Upload part.
* @param type $bucket
* @param type $uri
* @param type $partNumber
* @param type $uploadId
* @param type $requestHeaders
* @param type $data
* @return bool|\S3Request
*/
public function uploadPart($bucket, $uri, $partNumber, $uploadId, $requestHeaders = array(), $data = array()) {
$rest = $this->s3Request('PUT', $bucket, $uri, $this->endpoint);
if (isset($data['offset'])) {
if($this->signVer == 'v4'){
$chunk = $this->__getFileData($data['file'], $data['offset'], ($data['offset'] + $data['length'] - 1));
$rest->setBody($chunk);
}else{
if (!$rest->fp = @fopen($data['file'], 'rb')) {
return false;
}
fseek($rest->fp, $data['offset']);
$rest->size = $data['length'];
}
}
$rest->setParameter('partNumber', $partNumber);
$rest->setParameter('uploadId', $uploadId);
foreach ($requestHeaders as $h => $v) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
$rest = $rest->getResponse();
// upload part
if (!empty($data)) {
if(!$this->__execReponse($rest, __FUNCTION__, 1, array($bucket, $uri))){
return false;
}
return !empty($rest->headers['hash']) ? $rest->headers['hash'] : false;
}
// upload part copy
if (!$body = $this->__execReponse($rest, __FUNCTION__, 0, array($bucket, $uri)))
return false;
return !empty($body['ETag']) ? trim($body['ETag'], '"') : false;
}
/**
* Get upload part list
* @param type $bucket
* @param type $uri
* @param type $uploadId
* @return boolean
*/
public function listParts($bucket, $uri, $uploadId) {
$rest = $this->s3Request('GET', $bucket, $uri, $this->endpoint);
$rest->setParameter('uploadId', $uploadId);
$response = $rest->getResponse();
if (!$body = $this->__execReponse($response, __FUNCTION__, 0, array($bucket, $uri)))
return false;
$list = array();
if (isset($body['Part']['PartNumber'])) {
$body['Part'] = array($body['Part']);
}
foreach ($body['Part'] as $part) {
$list[] = array(
'PartNumber' => $part['PartNumber'],
'ETag' => trim($part['ETag'], '"'),
);
}
return $list;
}
/**
* Get upload parts.
* @param type $file_size
* @param type $partSize
* @return type
*/
private function __generateParts($file_size, $partSize = 10485760) {
$i = 0;
$size_count = $file_size;
$values = array();
if ($file_size / $partSize > self::MAX_PART_NUM) {
$partSize = ($size_count - $size_count % (self::MAX_PART_NUM - 1)) / (self::MAX_PART_NUM - 1);
$partSize = ceil($partSize/1024/1024)*1024*1024; // 取整
} else {
$partSize = $this->__computePartSize($partSize);
}
while ($size_count > 0) {
$size_count -= $partSize;
$values[] = array(
self::AMZ_SEEK_TO => ($partSize * $i),
self::AMZ_LENGTH => (($size_count > 0) ? $partSize : ($size_count + $partSize)),
);
$i++;
}
return $values;
}
/**
* Get part size.
* @param type $partSize
* @return type
*/
private function __computePartSize($partSize) {
$partSize = (int) $partSize;
if ($partSize <= self::MIN_PART_SIZE) {
$partSize = self::MIN_PART_SIZE;
} elseif ($partSize > self::MAX_PART_SIZE) {
$partSize = self::MAX_PART_SIZE;
}
return $partSize;
}
/**
* Get file data
* @param type $filename
* @param type $from_pos
* @param type $to_pos
* @return string
*/
private function __getFileData($filename, $from_pos = null, $to_pos = null) {
if (!file_exists($filename) || false === $fh = fopen($filename, 'rb')) {
return '';
}
if($from_pos === null || $to_pos === null){
return @file_get_contents($filename);
}
$total_length = $to_pos - $from_pos + 1;
$buffer = 8192;
$left_length = $total_length;
$data = '';
fseek($fh, $from_pos);
while (!feof($fh)) {
$read_length = $left_length >= $buffer ? $buffer : $left_length;
if ($read_length <= 0) {
break;
}
$data .= fread($fh, $read_length);
$left_length = $left_length - $read_length;
}
fclose($fh);
return $data;
}
/**
* Get an object.
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param mixed $saveTo Filename or resource to write to
* @return mixed
*/
public function getObject($bucket, $uri, $requestHeaders = array(), $saveTo = false) {
$rest = $this->s3Request('GET', $bucket, $uri, $this->endpoint);
foreach ($requestHeaders as $h => $v) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
if ($saveTo !== false) {
if (is_resource($saveTo)) {
$rest->fp = &$saveTo;
} elseif (($rest->fp = @fopen($saveTo, 'wb')) !== false) {
$rest->file = realpath($saveTo);
} else {
$rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: ' . $saveTo);
}
}
if ($rest->response->error === false) {
$rest->getResponse();
}
if ($rest->response->error === false && $rest->response->code !== 200 && $rest->response->code !== 206) {
$rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status');
}
if ($rest->response->error !== false) {
$this->__triggerError("S3->getObject({$bucket}, {$uri}): [".$rest->response->error['code'].'] '.$rest->response->error['message'],__FILE__,__LINE__);
return false;
}
return isset($rest->response->body) ? $rest->response->body : '';
}
/**
* Get object information.
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param bool $returnInfo Return response information
* @return mixed | false
*/
public function getObjectInfo($bucket, $uri, $returnInfo = true) {
$rest = $this->s3Request('HEAD', $bucket, $uri, $this->endpoint);
$rest = $rest->getResponse();
if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) {
$rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status');
}
if ($rest->error !== false) {
// url包含%xxx时会报错;
$this->__triggerError("S3->getObjectInfo({$bucket}, {$uri}): [".$rest->error['code'].'] '.$rest->error['message'],__FILE__,__LINE__);
return false;
}
return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false;
}
/**
* Copy an object.
*
* @param string $srcBucket Source bucket name
* @param string $srcUri Source object URI
* @param string $bucket Destination bucket name
* @param string $uri Destination object URI
* @param constant $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 constant $storageClass Storage class constant
*
* @return mixed | false
*/
public function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $returnBody = false) {
$rest = $this->s3Request('PUT', $bucket, $uri, $this->endpoint);
$rest->setHeader('Content-Length', 0);
foreach ($requestHeaders as $h => $v) {
strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v);
}
foreach ($metaHeaders as $h => $v) {
$rest->setAmzHeader('x-amz-meta-' . $h, $v);
}
if ($storageClass !== self::STORAGE_CLASS_STANDARD) { // 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(true);
if (!$body = $this->__execReponse($rest, __FUNCTION__, 0, array($srcBucket, $srcUri, $bucket, $uri)))
return false;
if($returnBody) return $body;
return isset($body['LastModified'], $body['LastModified']) ? true : false;
}
/**
* Get object or bucket Access Control Policy.
*
* @param string $bucket Bucket name
* @param string $uri Object URI
*
* @return mixed | false
*/
public function getAccessControlPolicy($bucket, $uri = '') {
$rest = $this->s3Request('GET', $bucket, $uri, $this->endpoint);
$rest->setParameter('acl', null);
$rest = $rest->getResponse();
if (!$body = $this->__execReponse($rest, __FUNCTION__, 0, array($bucket, $uri)))
return false;
return isset($body['AccessControlList']['Grant']['Permission']) ? $body['AccessControlList']['Grant']['Permission'] : false;
}
/**
* Delete an object.
*
* @param string $bucket Bucket name
* @param string $uri Object URI
*
* @return bool
*/
public function deleteObject($bucket, $uri) {
$rest = $this->s3Request('DELETE', $bucket, $uri, $this->endpoint);
$rest = $rest->getResponse();
$code = $rest->code == '200' ? 200 : 204;
return $this->__execReponse($rest, __FUNCTION__, 1, array(), $code);
}
/**
* Delete objects.
*
* @param type $bucket
* @param type $data
*
* @return bool
*/
public function deleteObjects($bucket, $data) {
$xml = new SimpleXMLElement('');
// add the objects
foreach ($data as $object) {
$xmlObject = $xml->addChild('Object');
$node = $xmlObject->addChild('Key');
$node[0] = $object;
}
$xml->addChild('Quiet', true);
$body = $xml->asXML();
$rest = $this->s3Request('POST', $bucket, '', $this->endpoint);
$rest->setHeader('Content-Type', 'application/octet-stream');
$rest->setHeader('Content-MD5', $this->__base64(md5($body)));
$rest->setHeader('Content-Length', strlen($body));
$rest->setParameter('delete', '');
$rest->setBody($body);
$rest = $rest->getResponse();
return $this->__execReponse($rest, __FUNCTION__, 1);
}
/**
* Get a query string authenticated URL.
* https://oos-cn.ctyunapi.cn/docs/oos/S3%E5%BC%80%E5%8F%91%E8%80%85%E6%96%87%E6%A1%A3-v6.pdf
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param int $lifetime Lifetime in seconds
*
* @return string
*/
public function getAuthenticatedURL($bucket, $uri, $lifetime, $subResource = array()){
// $expires = $this->__getTime() + $lifetime;
$expires = strtotime(date('Ymd 23:59:59')); // kodbox:签名链接有效期,改为当天有效
// $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); // 文件名含+时会导致签名错误
$uri = str_replace('%2F', '/', rawurlencode($uri));
ksort($subResource);
$ext = http_build_query($subResource);
$url = sprintf(
'%s/%sAWSAccessKeyId=%s&Expires=%u&Signature=%s',
$this->endpoint,
$uri . '?' . $ext . ($ext ? '&' : ''),
$this->__accessKey,
$expires,
urlencode($this->__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}".($ext ? '?' . urldecode($ext) : '')))
);
return $url;
}
/**
* Get object authenticated url v4
* @param type $access_key
* @param type $secret_key
* @param type $bucket
* @param type $canonical_uri
* @param type $expires
* @param type $region
* @param type $extra_headers
* @param type $preview
* @return string
*/
public function getObjectUrl($access_key, $secret_key, $bucket, $canonical_uri, $expires = 0, $region = 'us-east-1', $extra_headers = array(), $preview = true, $paramsAdd=array()) {
$encoded_uri = '/' . str_replace('%2F', '/', rawurlencode($canonical_uri));
$signed_headers = array();
foreach ($extra_headers as $key => $value) {
$signed_headers[strtolower($key)] = $value;
}
if (!array_key_exists('host', $signed_headers)) {
$res = parse_url($this->endpoint);
$signed_headers['host'] = $res['host'];
}
ksort($signed_headers);
$header_string = '';
foreach ($signed_headers as $key => $value) {
$header_string .= $key . ':' . trim($value) . "\n";
}
$signed_headers_string = implode(';', array_keys($signed_headers));
$date_text = gmdate('Ymd');
// $time_text = gmdate('Ymd\THis\Z');
$time_text = gmdate('Ymd\T000000\Z');
$expires = 3600*24; // kodbox:签名链接有效期,改为当天有效
$algorithm = 'AWS4-HMAC-SHA256';
$scope = "$date_text/$region/s3/aws4_request";
$x_amz_params = array(
'response-content-disposition' => $preview ? 'inline' : 'attachment',
'X-Amz-Algorithm' => $algorithm,
'X-Amz-Credential' => $access_key . '/' . $scope,
'X-Amz-Date' => $time_text,
'X-Amz-SignedHeaders' => $signed_headers_string,
);
$x_amz_params = array_merge($x_amz_params,$paramsAdd);
if ($expires > 0) {
$x_amz_params['X-Amz-Expires'] = $expires;
}
ksort($x_amz_params);
$query_string_items = array();
foreach ($x_amz_params as $key => $value) {
$query_string_items[] = rawurlencode($key) . '=' . rawurlencode($value);
}
$query_string = implode('&', $query_string_items);
$canonical_request = "GET\n$encoded_uri\n$query_string\n$header_string\n$signed_headers_string\nUNSIGNED-PAYLOAD";
$string_to_sign = "$algorithm\n$time_text\n$scope\n" . hash('sha256', $canonical_request, false);
$signing_key = hash_hmac('sha256', 'aws4_request', hash_hmac('sha256', 's3', hash_hmac('sha256', $region, hash_hmac('sha256', $date_text, 'AWS4' . $secret_key, true), true), true), true);
$signature = hash_hmac('sha256', $string_to_sign, $signing_key);
$host = $this->endpoint;
$url = $host . $encoded_uri . '?' . $query_string . '&X-Amz-Signature=' . $signature;
return $url;
}
/**
* Get upload POST parameters for form uploads.
*
* @param string $bucket Bucket name
* @param string $uriPrefix Object URI prefix
* @param constant $acl ACL constant
* @param int $lifetime Lifetime in seconds
* @param int $maxFileSize Maximum filesize in bytes (default 5MB)
* @param string $successRedirect Redirect URL or 200 / 201 status code
* @param array $amzHeaders Array of x-amz-meta-* headers
* @param array $headers Array of request headers or content type as a string
* @param bool $flashVars Includes additional "Filename" variable posted by Flash
*
* @return object
*/
public function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, $maxFileSize = 10485760, $successRedirect = '201', $amzHeaders = array(), $headers = array(), $flashVars = false) {
// Create policy object
$policy = new stdClass();
$policy->expiration = gmdate('Y-m-d\TH:i:s.000\Z', ($this->__getTime() + $lifetime));
$policy->conditions = array();
$obj = new stdClass();
$obj->bucket = $bucket;
array_push($policy->conditions, $obj);
$obj = new stdClass();
$obj->acl = $acl;
array_push($policy->conditions, $obj);
$obj = new stdClass(); // 200 for non-redirect uploads
if (is_numeric($successRedirect) && in_array((int) $successRedirect, array(200, 201))) {
$obj->success_action_status = (string) $successRedirect;
} else { // URL
$obj->success_action_redirect = $successRedirect;
}
array_push($policy->conditions, $obj);
if ($acl !== self::ACL_PUBLIC_READ) {
array_push($policy->conditions, array('eq', '$acl', $acl));
}
array_push($policy->conditions, array('starts-with', '$key', $uriPrefix));
if ($flashVars) {
array_push($policy->conditions, array('starts-with', '$Filename', ''));
}
foreach (array_keys($headers) as $headerKey) {
array_push($policy->conditions, array('starts-with', '$' . $headerKey, ''));
}
foreach ($amzHeaders as $headerKey => $headerVal) {
$obj = new stdClass();
$obj->{$headerKey} = (string) $headerVal;
array_push($policy->conditions, $obj);
}
array_push($policy->conditions, array('content-length-range', 0, $maxFileSize));
$policy = base64_encode(str_replace('\/', '/', json_encode($policy)));
// Create parameters
$params = new stdClass();
$params->AWSAccessKeyId = $this->__accessKey;
$params->key = $uriPrefix . '${filename}';
$params->acl = $acl;
$params->policy = $policy;
unset($policy);
$params->signature = $this->__getHash($params->policy);
if (is_numeric($successRedirect) && in_array((int) $successRedirect, array(200, 201))) {
$params->success_action_status = (string) $successRedirect;
} else {
$params->success_action_redirect = $successRedirect;
}
foreach ($headers as $headerKey => $headerVal) {
$params->{$headerKey} = (string) $headerVal;
}
foreach ($amzHeaders as $headerKey => $headerVal) {
$params->{$headerKey} = (string) $headerVal;
}
return $params;
}
/**
* Get MIME type for file.
*
* To override the putObject() Content-Type, add it to $requestHeaders
*
* To use fileinfo, ensure the MAGIC environment variable is set
*
* @internal Used to get mime types
*
* @param string &$file File path
*
* @return string
*/
private function __getMIMEType(&$file) {
return get_file_mime(get_path_ext($file));
static $exts = array(
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif',
'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf',
'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash',
'zip' => 'application/zip', 'gz' => 'application/x-gzip',
'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed',
'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload',
'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain',
'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
'css' => 'text/css', 'js' => 'text/javascript',
'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php',
);
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (isset($exts[$ext])) {
return $exts[$ext];
}
// Use fileinfo if available
if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
if (($type = finfo_file($finfo, $file)) !== false) {
// Remove the charset and grab the last content-type
$type = explode(' ', str_replace('; charset=', ';charset=', $type));
$type = array_pop($type);
$type = explode(';', $type);
$type = trim(array_shift($type));
}
finfo_close($finfo);
if ($type !== false && strlen($type) > 0) {
return $type;
}
}
return 'application/octet-stream';
}
/**
* Get the current time.
*
* @internal Used to apply offsets to sytem time
*
* @return int
*/
public function __getTime() {
return time() + $this->__timeOffset;
}
/**
* Generate the auth string: "AWS AccessKey:Signature".
*
* @internal Used by s3Request->getResponse()
*
* @param string $string String to sign
*
* @return string
*/
public function __getSignature($string) {
return 'AWS ' . $this->__accessKey . ':' . $this->__getHash($string);
}
/**
* Creates a HMAC-SHA1 hash.
*
* This uses the hash extension if loaded
*
* @internal Used by __getSignature()
*
* @param string $string String to sign
*
* @return string
*/
private function __getHash($string) {
return base64_encode(extension_loaded('hash') ?
hash_hmac('sha1', $string, $this->__secretKey, true) : pack('H*', sha1(
(str_pad($this->__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
pack('H*', sha1((str_pad($this->__secretKey, 64, chr(0x00)) ^
(str_repeat(chr(0x36), 64))) . $string)))));
}
private function __base64($str) {
$ret = '';
for ($i = 0; $i < strlen($str); $i += 2) {
$ret .= chr(hexdec(substr($str, $i, 2)));
}
return base64_encode($ret);
}
/**
* Generate the headers for AWS Signature V4.
*
* @internal Used by s3Request->getResponse()
*
* @param array $aheaders amzHeaders
* @param array $headers
* @param string $method
* @param string $uri
* @param string $data
*
* @return array $headers
*/
public function __getSignatureV4($aHeaders, $headers, $method = 'GET', $uri = '', $data = '') {
$service = 's3';
$region = $this->getRegion();
$algorithm = 'AWS4-HMAC-SHA256';
$amzHeaders = array();
$amzRequests = array();
$amzDate = gmdate('Ymd\THis\Z');
$amzDateStamp = gmdate('Ymd');
// amz-date ISO8601 format ? for aws request
$amzHeaders['x-amz-date'] = $amzDate;
// CanonicalHeaders
foreach ($headers as $k => $v) {
$amzHeaders[strtolower($k)] = trim($v);
}
foreach ($aHeaders as $k => $v) {
$amzHeaders[strtolower($k)] = trim($v);
}
$x_amz_content_sha256 = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data);
unset($amzHeaders['x-amz-content-sha256']);
uksort($amzHeaders, 'strcmp');
// payload
// $payloadHash = isset($amzHeaders['x-amz-content-sha256']) ? $amzHeaders['x-amz-content-sha256'] : hash('sha256', $data);
$payloadHash = $x_amz_content_sha256;
// parameters
$parameters = array();
if (strpos($uri, '?')) {
list($uri, $query_str) = @explode('?', $uri);
parse_str($query_str, $parameters);
}
// CanonicalRequests
$amzRequests[] = $method;
$uriQmPos = strpos($uri, '?');
$amzRequests[] = ($uriQmPos === false ? $uri : substr($uri, 0, $uriQmPos));
ksort($parameters);
$amzRequests[] = str_replace('+','%20',http_build_query($parameters)); // 空格会被转义为+,导致签名错误
// add header as string to requests
foreach ($amzHeaders as $k => $v) {
$amzRequests[] = $k . ':' . $v;
}
// add a blank entry so we end up with an extra line break
$amzRequests[] = '';
// SignedHeaders
$amzRequests[] = implode(';', array_keys($amzHeaders));
// payload hash
$amzRequests[] = $payloadHash;
// request as string
$amzRequestStr = implode("\n", $amzRequests);
// CredentialScope
$credentialScope = array();
$credentialScope[] = $amzDateStamp;
$credentialScope[] = $region;
$credentialScope[] = $service;
$credentialScope[] = 'aws4_request';
// stringToSign
$stringToSign = array();
$stringToSign[] = $algorithm;
$stringToSign[] = $amzDate;
$stringToSign[] = implode('/', $credentialScope);
$stringToSign[] = hash('sha256', $amzRequestStr);
// as string
$stringToSignStr = implode("\n", $stringToSign);
// Make Signature
$kSecret = 'AWS4' . $this->__secretKey;
$kDate = hash_hmac('sha256', $amzDateStamp, $kSecret, true);
$kRegion = hash_hmac('sha256', $region, $kDate, true);
$kService = hash_hmac('sha256', $service, $kRegion, true);
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
$signature = hash_hmac('sha256', $stringToSignStr, $kSigning);
$authorization = array(
'Credential=' . $this->__accessKey . '/' . implode('/', $credentialScope),
'SignedHeaders=' . implode(';', array_keys($amzHeaders)),
'Signature=' . $signature,
);
$authorizationStr = $algorithm . ' ' . implode(',', $authorization);
$resultHeaders = array(
'X-AMZ-DATE' => $amzDate,
'Authorization' => $authorizationStr,
);
if (!isset($aHeaders['x-amz-content-sha256'])) {
$resultHeaders['x-amz-content-sha256'] = $payloadHash;
}
return $resultHeaders;
}
/**
* 重置原S3Request类的变量
* @return void
*/
public function resetVariable(){
$this->endpoint = null;
$this->verb = null; // Verb.
$this->bucket = null; // S3 bucket name.
$this->uri = null; // Object URI.
$this->resource = ''; // Final object URI.
$this->parameters = array(); // Additional request parameters.
$this->amzHeaders = array(); // Amazon specific request headers.
$this->headers = array( // HTTP request headers.
'Host' => '',
'Date' => '',
'Content-MD5' => '',
'Content-Type' => '',
);
$this->fp = false; // Use HTTP PUT?
$this->size = 0; // PUT file size.
$this->data = false; // PUT post fields.
$this->response = null;
}
/**
* 原为独立的curl请求类的构造方法,因静态属性在多次实例化时调用有问题,合并为一。2021-09-28
*
* @param string $verb Verb
* @param string $bucket Bucket name
* @param string $uri Object URI
* @param string $endpoint AWS endpoint URI
*
* @return mixed
*/
public function s3Request($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') {
$this->resetVariable();
$this->endpoint = $endpoint;
$this->verb = $verb;
$this->bucket = $bucket;
$this->uri = $uri !== '' ? '/' . str_replace('%2F', '/', rawurlencode($uri)) : '/';
$this->headers['Host'] = get_url_domain($endpoint);
if ($this->bucket !== '') {
// 包含'_'时会导致bucket重复,前端已做检查,此处忽略
if ($this->__dnsBucketName($this->bucket)) {
$this->resource = '/' . $this->bucket . $this->uri;
} else {
$this->uri = $this->uri;
if ($this->bucket !== '') {
$this->uri = '/' . $this->bucket . $this->uri;
}
$this->bucket = '';
$this->resource = $this->uri;
}
} else {
$this->resource = $this->uri;
}
$this->headers['Date'] = gmdate('D, d M Y H:i:s T');
$this->response = new \STDClass();
$this->response->error = false;
$this->response->body = null;
$this->response->headers = array();
return $this;
}
/**
* Set request parameter.
*
* @param string $key Key
* @param string $value Value
*/
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
}
/**
* Set request header.
*
* @param string $key Key
* @param string $value Value
*/
public function setHeader($key, $value) {
$this->headers[$key] = $value;
}
/**
* Set x-amz-meta-* header.
*
* @param string $key Key
* @param string $value Value
*/
public function setAmzHeader($key, $value) {
$this->amzHeaders[$key] = $value;
}
/**
* Set POST data.
*
* @param type $value
*/
public function setBody($value) {
$this->data = $value;
}
// 获取有效的请求方式
private function getVerb(){
if ($this->verb == 'HEAD' && !$this->headValid) return 'GET';
return $this->verb;
}
/**
* Get the S3 response.
*
* @return object | false
*/
public function getResponse() {
$query = '';
if (sizeof($this->parameters) > 0) {
$query = substr($this->uri, -1) !== '?' ? '?' : '&';
foreach ($this->parameters as $var => $value) {
if ($value == null || $value == '') {
$query .= $var . '&';
} else {
$query .= $var . '=' . rawurlencode($value) . '&';
}
}
$query = substr($query, 0, -1);
$this->uri .= $query;
if (array_key_exists('acl', $this->parameters) ||
array_key_exists('delete', $this->parameters) ||
array_key_exists('location', $this->parameters) ||
array_key_exists('partNumber', $this->parameters) ||
array_key_exists('torrent', $this->parameters) ||
array_key_exists('uploadId', $this->parameters) ||
array_key_exists('uploads', $this->parameters) ||
array_key_exists('website', $this->parameters) ||
array_key_exists('cors', $this->parameters) ||
array_key_exists('logging', $this->parameters)) {
$this->resource .= $query;
}
}
$url = $this->endpoint . $this->uri;
// Basic setup
$curl = curl_init();
curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php');
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT,30); // 建立连接
// curl_setopt($curl, CURLOPT_TIMEOUT,60); // 建立连接+数据传输
$proxy = $this->proxy;
if ($proxy != null && isset($proxy['host'])) {
curl_setopt($curl, CURLOPT_PROXY, $proxy['host']);
curl_setopt($curl, CURLOPT_PROXYTYPE, $proxy['type']);
if (isset($proxy['user'], $proxy['pass']) && $proxy['user'] != null && $proxy['pass'] != null) {
curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', $proxy['user'], $proxy['pass']));
}
}
// Headers
$headers = array();
$amz = array();
foreach ($this->amzHeaders as $header => $value) {
if (strlen($value) > 0) {
$headers[] = $header . ': ' . $value;
}
}
foreach ($this->headers as $header => $value) {
if (strlen($value) > 0) {
$headers[] = $header . ': ' . $value;
}
}
// Collect AMZ headers for signature
foreach ($this->amzHeaders as $header => $value) {
if (strlen($value) > 0) {
$amz[] = strtolower($header) . ':' . $value;
}
}
// AMZ headers must be sorted
if (sizeof($amz) > 0) {
//sort($amz);
usort($amz, array(&$this, '__sortMetaHeadersCmp'));
$amz = "\n" . implode("\n", $amz);
} else {
$amz = '';
}
if ($this->hasAuth()) {
// Authorization string (CloudFront stringToSign should only contain a date)
if ($this->headers['Host'] == 'cloudfront.amazonaws.com') {
$headers[] = 'Authorization: ' . $this->__getSignature($this->headers['Date']);
} else {
if ($this->signVer == 'v2') {
$headers[] = 'Authorization: ' . $this->__getSignature(
$this->getVerb() . "\n" .
$this->headers['Content-MD5'] . "\n" .
$this->headers['Content-Type'] . "\n" .
$this->headers['Date'] . $amz . "\n" .
$this->resource
);
} else {
$amzHeaders = $this->__getSignatureV4(
$this->amzHeaders, $this->headers, $this->getVerb(), $this->uri, $this->data
);
foreach ($amzHeaders as $k => $v) {
$headers[] = $k . ': ' . $v;
}
}
}
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback'));
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
// Request types
switch ($this->verb) {
case 'GET': break;
case 'PUT': case 'POST': // POST only used for CloudFront
if ($this->fp !== false) {
curl_setopt($curl, CURLOPT_PUT, true);
curl_setopt($curl, CURLOPT_INFILE, $this->fp);
if ($this->size >= 0) {
curl_setopt($curl, CURLOPT_INFILESIZE, $this->size);
}
} elseif ($this->data !== false) {
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data);
} else {
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
}
break;
case 'HEAD':
if ($this->getVerb() == $this->verb) {
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
curl_setopt($curl, CURLOPT_NOBODY, true);
} else {
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_NOBODY, true);
}
break;
case 'DELETE':
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
default: break;
}
// set curl progress function callback
if ($this->progressFunction) {
curl_setopt($curl, CURLOPT_NOPROGRESS, false);
curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, $this->progressFunction);
}
//add by warlee;
$theCurl = $curl;
curl_setopt($theCurl, CURLOPT_NOPROGRESS, false);
curl_setopt($theCurl, CURLOPT_PROGRESSFUNCTION,'curl_progress');
$theResult = curl_progress_start($theCurl);
if(!$theResult){$theResult = curl_exec($theCurl);curl_progress_end($theCurl,$theResult);}
$curl = $theCurl;$result = $theResult;
// Execute, grab errors
if ($result) {
$this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
} else {
$this->response->error = array(
'code' => curl_errno($curl),
'message' => curl_error($curl),
'resource' => $this->resource,
);
}
@curl_close($curl);
// Parse body into XML
if ($this->response->error === false && isset($this->response->headers['type']) &&
$this->response->headers['type'] == 'application/xml' && isset($this->response->body)) {
if (strtolower(substr($this->response->body, 0, 4)) == 'http') {
$temp = explode(PHP_EOL, $this->response->body);
$body = array();
foreach($temp as $value) {
if(stripos($value, ':') === false) continue;
$item = explode(':', trim($value));
$body[$item[0]] = $item[1];
}
$this->response->body = $body;
}else{
if(stripos($this->response->body, 'response->body = stristr($this->response->body,'response->body = simplexml_load_string($this->response->body);
}
// Grab S3 errors
if (!in_array($this->response->code, array(200, 204, 206)) &&
isset($this->response->body->Code, $this->response->body->Message)) {
$this->response->error = array(
'code' => (string) $this->response->body->Code,
'message' => (string) $this->response->body->Message,
);
if (isset($this->response->body->Resource)) {
$this->response->error['resource'] = (string) $this->response->body->Resource;
}
unset($this->response->body);
}
}
// Clean up file resources
if ($this->fp !== false && is_resource($this->fp)) {
fclose($this->fp);
}
return $this->response;
}
/**
* Sort compare for meta headers.
*
* @internal Used to sort x-amz meta headers
*
* @param string $a String A
* @param string $b String B
*
* @return int
*/
private function __sortMetaHeadersCmp($a, $b) {
$lenA = strpos($a, ':');
$lenB = strpos($b, ':');
$minLen = min($lenA, $lenB);
$ncmp = strncmp($a, $b, $minLen);
if ($lenA == $lenB) {
return $ncmp;
}
if (0 == $ncmp) {
return $lenA < $lenB ? -1 : 1;
}
return $ncmp;
}
/**
* CURL write callback.
*
* @param resource &$curl CURL resource
* @param string &$data Data
*
* @return int
*/
private function __responseWriteCallback(&$curl, &$data) {
if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) {
return fwrite($this->fp, $data);
} else {
$this->response->body .= $data;
}
return strlen($data);
}
/**
* Check DNS conformity.
*
* @param string $bucket Bucket name
*
* @return bool
*/
private function __dnsBucketName($bucket) {
if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) {
return false;
}
if (strstr($bucket, '-.') !== false) {
return false;
}
if (strstr($bucket, '..') !== false) {
return false;
}
if (!preg_match('/^[0-9a-z]/', $bucket)) {
return false;
}
if (!preg_match('/[0-9a-z]$/', $bucket)) {
return false;
}
return true;
}
/**
* CURL header callback.
*
* @param resource $curl CURL resource
* @param string $data Data
*
* @return int
*/
private function __responseHeaderCallback($curl, $data) {
if (($strlen = strlen($data)) <= 2) {
return $strlen;
}
if (strtolower(substr($data, 0, 4)) == 'http') {
$this->response->code = (int) substr($data, 9, 3);
} else {
$data = trim($data);
if (strpos($data, ': ') === false) {
return $strlen;
}
list($header, $value) = explode(': ', $data, 2);
$header = strtolower($header);
if ($header == 'last-modified') { // Last-Modified
$this->response->headers['time'] = strtotime($value);
} elseif ($header == 'date') { // Date
$this->response->headers['date'] = strtotime($value);
} elseif ($header == 'content-length') { // Content-Length
$this->response->headers['size'] = (int) $value;
} elseif ($header == 'content-type') { // Content-Type
$this->response->headers['type'] = $value;
} elseif ($header == 'etag') { // ETag
$this->response->headers['hash'] = trim($value, '"');
} elseif (preg_match('/^x-amz-meta-.*$/', $header)) {
$this->response->headers[$header] = $value;
}
}
return $strlen;
}
}
/**
* S3 exception class.
*
* @see http://undesigned.org.za/2007/10/22/amazon-s3-php-class
*
* @version 0.5.0-dev
*/
class S3Exception extends \Exception {
/**
* Class constructor.
*
* @param string $message Exception message
* @param string $file File in which exception was created
* @param string $line Line number on which exception was created
* @param int $code Exception code
*/
public function __construct($message, $file, $line, $code = 0) {
parent::__construct($message, $code);
$this->file = $file;
$this->line = $line;
}
}