2042 lines
64 KiB
PHP
Executable File
2042 lines
64 KiB
PHP
Executable File
<?php
|
||
|
||
class S3 {
|
||
|
||
// ACL flags
|
||
const ACL_PRIVATE = 'private';
|
||
const ACL_PUBLIC_READ = 'public-read';
|
||
const ACL_PUBLIC_READ_WRITE = 'public-read-write';
|
||
const ACL_AUTHENTICATED_READ = 'authenticated-read';
|
||
const STORAGE_CLASS_STANDARD = 'STANDARD';
|
||
const STORAGE_CLASS_RRS = 'REDUCED_REDUNDANCY';
|
||
const STORAGE_CLASS_STANDARD_IA = 'STANDARD_IA';
|
||
const SSE_NONE = '';
|
||
const SSE_AES256 = 'AES256';
|
||
const AMZ_SEEK_TO = 'seekTo';
|
||
const AMZ_LENGTH = 'length';
|
||
const MAX_PART_NUM = 1000; // 最大分片数
|
||
const MIN_PART_SIZE = 10485760; // 最小分片10M
|
||
const MAX_PART_SIZE = 5368709120; // 最大分片5G
|
||
const UPLOAD_RETRY = 3; // 分片、整合失败,重试3次
|
||
|
||
private $__accessKey = null;
|
||
private $__secretKey = null;
|
||
public $defDelimiter = null;
|
||
public $endpoint = 's3.amazonaws.com';
|
||
public $region = '';
|
||
public $proxy = null;
|
||
public $useSSL = false; // Connect using SSL?
|
||
public $useSSLValidation = true; // Use SSL validation?
|
||
public $useSSLVersion = CURL_SSLVERSION_TLSv1; // Use SSL version
|
||
public $useExceptions = false;
|
||
private $__timeOffset = 0;
|
||
public $sslKey = null;
|
||
public $sslCert = null;
|
||
public $sslCACert = null;
|
||
private $__signingKeyPairId = null;
|
||
private $__signingKeyResource = false;
|
||
public $progressFunction = null; // 传输进度回调方法
|
||
public $signVer = 'v4'; // 默认签名版本
|
||
|
||
public $headValid = true; // head请求有效
|
||
|
||
// s3 request相关
|
||
// private $endpoint; // AWS URI.
|
||
private $verb; // Verb.
|
||
private $bucket; // S3 bucket name.
|
||
private $uri; // Object URI.
|
||
private $resource = ''; // Final object URI.
|
||
private $parameters = array(); // Additional request parameters.
|
||
private $amzHeaders = array(); // Amazon specific request headers.
|
||
private $headers = array( // HTTP request headers.
|
||
'Host' => '',
|
||
'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 = "<?xml version='1.0' encoding='UTF-8'?><CORSConfiguration>"
|
||
. "<CORSRule>"
|
||
. "<AllowedOrigin>*</AllowedOrigin>"
|
||
. "<AllowedMethod>GET</AllowedMethod>"
|
||
. "<AllowedMethod>PUT</AllowedMethod>"
|
||
. "<AllowedMethod>POST</AllowedMethod>"
|
||
. "<AllowedMethod>DELETE</AllowedMethod>"
|
||
. "<AllowedMethod>HEAD</AllowedMethod>"
|
||
. "<MaxAgeSeconds>600</MaxAgeSeconds>"
|
||
. "<ExposeHeader>ETag</ExposeHeader>"
|
||
. "<AllowedHeader>*</AllowedHeader>"
|
||
. "</CORSRule>"
|
||
. "</CORSConfiguration>";
|
||
$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 = "<?xml version='1.0' encoding='UTF-8'?><CompleteMultipartUpload>";
|
||
foreach ($data as $part) {
|
||
$xmlStr .= '<Part>'
|
||
. "<PartNumber>{$part['PartNumber']}</PartNumber>"
|
||
. "<ETag>{$part['ETag']}</ETag>"
|
||
. '</Part>';
|
||
}
|
||
$xmlStr .= '</CompleteMultipartUpload>';
|
||
$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('<?xml version="1.0" encoding="utf-8"?><Delete/>');
|
||
// 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, '<?xml')) $this->response->body = stristr($this->response->body,'<?xml');
|
||
$this->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;
|
||
}
|
||
|
||
}
|