初始化代码

This commit is contained in:
2025-12-22 14:32:54 +08:00
parent e27ab90d9f
commit d02b31a8b9
1459 changed files with 240973 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Common\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Change from path style to host style, currently only host style is supported in cos.
*/
function endWith($haystack, $needle) {
$length = strlen($needle);
if($length == 0)
{
return true;
}
return (substr($haystack, -$length) === $needle);
}
class BucketStyleListener implements EventSubscriberInterface {
private $appId; // string: application id.
public function __construct($appId) {
$this->appId = $appId;
}
public static function getSubscribedEvents() {
return array('command.after_prepare' => array('onCommandAfterPrepare', -230));
}
/**
* Change from path style to host style.
* @param Event $event Event emitted.
*/
public function onCommandAfterPrepare(Event $event) {
$command = $event['command'];
$bucket = $command['Bucket'];
$request = $command->getRequest();
if ($command->getName() == 'ListBuckets')
{
$request->setHost('service.cos.myqcloud.com');
return ;
}
if ($key = $command['Key']) {
// Modify the command Key to account for the {/Key*} explosion into an array
if (is_array($key)) {
$command['Key'] = $key = implode('/', $key);
}
}
$request->setHeader('Date', gmdate('D, d M Y H:i:s T'));
$request->setPath(preg_replace("#^/{$bucket}#", '', $request->getPath()));
if ($this->appId != null && endWith($bucket,'-'.$this->appId) == False)
{
$bucket = $bucket.'-'.$this->appId;
}
// Set the key and bucket on the request
$request->getParams()->set('bucket', $bucket)->set('key', $key);
//$request->setPath(urldecode($request->getPath()));
// Switch to virtual hosted bucket
$request->setHost($bucket. '.' . $request->getHost());
if (!$bucket) {
$request->getParams()->set('cos.resource', '/');
} else {
// Bucket style needs a trailing slash
$request->getParams()->set(
'cos.resource',
'/' . rawurlencode($bucket) . ($key ? ('/' . Client::encodeKey($key)) : '/')
);
}
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Service\Description\Parameter;
use Guzzle\Service\Description\ServiceDescription;
use Guzzle\Service\Client as GSClient;
use Guzzle\Common\Collection;
use Guzzle\Http\EntityBody;
use Guzzle\Http\Message\RequestInterface;
use Qcloud\Cos\Signature;
use Qcloud\Cos\TokenListener;
class Client extends GSClient {
const VERSION = '1.3.0';
private $region; // string: region.
private $credentials;
private $appId; // string: application id.
private $secretId; // string: secret id.
private $secretKey; // string: secret key.
private $timeout; // int: timeout
private $connect_timeout; // int: connect_timeout
private $signature;
public function __construct($config) {
$this->region = $config['region'];
$regionmap = array('cn-east'=>'ap-shanghai',
'cn-sorth'=>'ap-guangzhou',
'cn-north'=>'ap-beijing-1',
'cn-south-2'=>'ap-guangzhou-2',
'cn-southwest'=>'ap-chengdu',
'sg'=>'ap-singapore',
'tj'=>'ap-beijing-1',
'bj'=>'ap-beijing',
'sh'=>'ap-shanghai',
'gz'=>'ap-guangzhou',
'cd'=>'ap-chengdu',
'sgp'=>'ap-singapore',);
$this->region = isset($regionmap[$this->region]) ? $regionmap[$this->region] : $this->region;
$this->credentials = $config['credentials'];
$this->appId = isset($config['credentials']['appId']) ? $config['credentials']['appId'] : null;
$this->secretId = $config['credentials']['secretId'];
$this->secretKey = $config['credentials']['secretKey'];
$this->token = isset($config['credentials']['token']) ? $config['credentials']['token'] : null;
$this->timeout = isset($config['timeout']) ? $config['timeout'] : 3600;
$this->connect_timeout = isset($config['connect_timeout']) ? $config['connect_timeout'] : 3600;
$this->signature = new signature($this->secretId, $this->secretKey);
parent::__construct(
'http://cos.' . $this->region . '.myqcloud.com/', // base url
array('request.options' => array('timeout' => $this->timeout, 'connect_timeout' => $this->connect_timeout),
)); // show curl verbose or not
$desc = ServiceDescription::factory(Service::getService());
$this->setDescription($desc);
$this->setUserAgent('cos-php-sdk-v5.' . Client::VERSION, true);
$this->addSubscriber(new ExceptionListener());
$this->addSubscriber(new Md5Listener($this->signature));
$this->addSubscriber(new TokenListener($this->token));
$this->addSubscriber(new SignatureListener($this->secretId, $this->secretKey));
$this->addSubscriber(new BucketStyleListener($this->appId));
// Allow for specifying bodies with file paths and file handles
$this->addSubscriber(new UploadBodyListener(array('PutObject', 'UploadPart')));
}
public function set_config($config) {
$this->region = $config['region'];
$regionmap = array('cn-east'=>'ap-shanghai',
'cn-sorth'=>'ap-guangzhou',
'cn-north'=>'ap-beijing-1',
'cn-south-2'=>'ap-guangzhou-2',
'cn-southwest'=>'ap-chengdu',
'sg'=>'ap-singapore',
'tj'=>'ap-beijing-1',
'bj'=>'ap-beijing',
'sh'=>'ap-shanghai',
'gz'=>'ap-guangzhou',
'cd'=>'ap-chengdu',
'sgp'=>'ap-singapore',);
$this->region = isset($regionmap[$this->region]) ? $regionmap[$this->region] : $this->region;
$this->credentials = $config['credentials'];
$this->appId = isset($config['credentials']['appId']) ? $config['credentials']['appId'] : null;
$this->secretId = $config['credentials']['secretId'];
$this->secretKey = $config['credentials']['secretKey'];
$this->token = isset($config['credentials']['token']) ? $config['credentials']['token'] : null;
$this->timeout = isset($config['timeout']) ? $config['timeout'] : 3600;
$this->connect_timeout = isset($config['connect_timeout']) ? $config['connect_timeout'] : 3600;
$this->signature = new signature($this->secretId, $this->secretKey);
parent::__construct(
'http://cos.' . $this->region . '.myqcloud.com/', // base url
array('request.options' => array('timeout' => $this->timeout, 'connect_timeout' => $this->connect_timeout),
)); // show curl verbose or not
}
public function __destruct() {
}
public function __call($method, $args) {
return parent::__call(ucfirst($method), $args);
}
public function createAuthorization(RequestInterface $request, $expires)
{
if ($request->getClient() !== $this) {
throw new InvalidArgumentException('The request object must be associated with the client. Use the '
. '$client->get(), $client->head(), $client->post(), $client->put(), etc. methods when passing in a '
. 'request object');
}
return $this->signature->createAuthorization($request, $expires);
}
public function createPresignedUrl(RequestInterface $request, $expires)
{
if ($request->getClient() !== $this) {
throw new InvalidArgumentException('The request object must be associated with the client. Use the '
. '$client->get(), $client->head(), $client->post(), $client->put(), etc. methods when passing in a '
. 'request object');
}
return $this->signature->createPresignedUrl($request, $expires);
}
public function getObjectUrl($bucket, $key, $expires = null, array $args = array())
{
$command = $this->getCommand('GetObject', $args + array('Bucket' => $bucket, 'Key' => $key));
if ($command->hasKey('Scheme')) {
$scheme = $command['Scheme'];
$request = $command->remove('Scheme')->prepare()->setScheme($scheme)->setPort(null);
} else {
$request = $command->prepare();
}
return $expires ? $this->createPresignedUrl($request, $expires) : $request->getUrl();
}
public function Upload($bucket, $key, $body, $options = array()) {
$body = EntityBody::factory($body);
$options = Collection::fromConfig(array_change_key_case($options), array(
'min_part_size' => MultipartUpload::MIN_PART_SIZE,
'params' => $options));
if ($body->getSize() < $options['min_part_size']) {
// Perform a simple PutObject operation
$rt = $this->putObject(array(
'Bucket' => $bucket,
'Key' => $key,
'Body' => $body,
) + $options['params']);
$rt['Location'] = $rt['ObjectURL'];
unset($rt['ObjectURL']);
}
else {
$multipartUpload = new MultipartUpload($this, $body, $options['min_part_size'], array(
'Bucket' => $bucket,
'Key' => $key,
'Body' => $body,
) + $options['params']);
$rt = $multipartUpload->performUploading();
}
return $rt;
}
public function resumeUpload($bucket, $key, $body, $uploadId, $options = array()) {
$body = EntityBody::factory($body);
$options = Collection::fromConfig(array_change_key_case($options), array(
'min_part_size' => MultipartUpload::MIN_PART_SIZE,
'params' => $options));
$multipartUpload = new MultipartUpload($this, $body, $options['min_part_size'], array(
'Bucket' => $bucket,
'Key' => $key,
'Body' => $body,
'UploadId' => $uploadId,
) + $options['params']);
$rt = $multipartUpload->resumeUploading();
return $rt;
}
public function Copy($bucket, $key, $copysource, $options = array()) {
$options = Collection::fromConfig(array_change_key_case($options), array(
'min_part_size' => Copy::MIN_PART_SIZE,
'params' => $options));
$sourcelistdot = explode('.',$copysource);
$sourcelistline = explode('-',$sourcelistdot[0]);
$sourceappid = array_pop($sourcelistline);
$sourcebucket = implode('-', $sourcelistline);
$sourceregion = $sourcelistdot[2];
$sourcekey = substr(strstr($copysource,'/'),1);
$sourceversion = "";
$cosClient = new Client(array('region' => $sourceregion,
'credentials'=> array(
'appId' => $sourceappid,
'secretId' => $this->secretId,
'secretKey' => $this->secretKey)));
if (!key_exists('VersionId',$options['params'])) {
$sourceversion = "";
}
else{
$sourceversion = $options['params']['VersionId'];
}
$rt = $cosClient->headObject(array('Bucket'=>$sourcebucket,
'Key'=>$sourcekey,
'VersionId'=>$sourceversion));
$contentlength =$rt['ContentLength'];
if ($contentlength < $options['min_part_size']) {
return $this->copyObject(array(
'Bucket' => $bucket,
'Key' => $key,
'CopySource' => $copysource."?versionId=".$sourceversion,
) + $options['params']);
}
$copy = new Copy($this, $contentlength, $copysource."?versionId=".$sourceversion, $options['min_part_size'], array(
'Bucket' => $bucket,
'Key' => $key
) + $options['params']);
return $copy->copy();
}
/**
* Determines whether or not a bucket exists by name
*
* @param string $bucket The name of the bucket
* @param bool $accept403 Set to true if 403s are acceptable
* @param array $options Additional options to add to the executed command
*
* @return bool
*/
public function doesBucketExist($bucket, $accept403 = true, array $options = array())
{
try {
$this->HeadBucket(array(
'Bucket' => $bucket));
return True;
}catch (\Exception $e){
return False;
}
}
/**
* Determines whether or not an object exists by name
*
* @param string $bucket The name of the bucket
* @param string $key The key of the object
* @param array $options Additional options to add to the executed command
*
* @return bool
*/
public function doesObjectExist($bucket, $key, array $options = array())
{
try {
$this->HeadObject(array(
'Bucket' => $bucket,
'Key' => $key));
return True;
}catch (\Exception $e){
return False;
}
}
public static function encodeKey($key) {
return $key;
return str_replace('%2F', '/', rawurlencode($key));
}
public static function explodeKey($key) {
// Remove a leading slash if one is found
//return explode('/', $key && $key[0] == '/' ? substr($key, 1) : $key);
return $key;
return ltrim($key, "/");
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Service\Command\OperationCommand;
use Guzzle\Service\Resource\Model;
use Guzzle\Common\Event;
/**
* Adds functionality to Qcloud Cos commands:
* - Adds the PutObject URL to a response
* - Allows creating a Pre-signed URL from any command
*/
class Command extends OperationCommand {
/**
* Create a pre-signed URL for the operation
*
* @param int|string $expires The Unix timestamp to expire at or a string that can be evaluated by strtotime
*
* @return string
*/
public function createPresignedUrl($expires)
{
return $this->client->createPresignedUrl($this->prepare(), $expires);
}
public function createAuthorization($expires)
{
return $this->client->createAuthorization($this->prepare(), $expires);
}
protected function process() {
parent::process();
// Set the GetObject URL if using the PutObject operation
if ($this->result instanceof Model && $this->getName() == 'PutObject') {
$request = $this->getRequest();;
$this->result->set('ObjectURL', $request->getUrl());
}
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Http\ReadLimitEntityBody;
class Copy {
/**
* const var: part size from 5MB to 5GB, and max parts of 10000 are allowed for each upload.
*/
const MIN_PART_SIZE = 5242880;
const MAX_PART_SIZE = 5368709120;
const MAX_PARTS = 10000;
private $client;
private $source;
private $options;
private $partSize;
private $size;
public function __construct($client, $contentlength, $source, $minPartSize, $options = array()) {
$this->client = $client;
$this->source = $source;
$this->options = $options;
$this->size = $contentlength;
$this->partSize = $this->calculatePartSize($minPartSize);
$this->concurrency = isset($options['concurrency']) ? $options['concurrency'] : 10;
$this->retry = isset($options['retry']) ? $options['retry'] : 5;
}
public function copy() {
$uploadId= $this->initiateMultipartUpload();
for ($i = 0; $i < 5; $i += 1) {
$rt = $this->uploadParts($uploadId);
if ($rt == 0) {
break;
}
sleep(1 << $i);
}
return $this->client->completeMultipartUpload(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'UploadId' => $uploadId,
'Parts' => $this->parts));
}
public function uploadParts($uploadId) {
$commands = array();
$offset = 0;
$partNumber = 1;
$partSize = $this->partSize;
$finishedNum = 0;
$this->parts = array();
for (;;) {
if ($offset + $partSize >= $this->size)
{
$partSize = $this->size - $offset;
}
$params = array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
'CopySource'=> $this->source,
'CopySourceRange' => 'bytes='.((string)$offset).'-'.(string)($offset+$partSize - 1),
);
if(!isset($parts[$partNumber])) {
$commands[] = $this->client->getCommand('UploadPartCopy', $params);
}
if ($partNumber % $this->concurrency == 0) {
$this->client->execute($commands);
$commands = array();
}
++$partNumber;
$offset += $partSize;
if ($this->size == $offset)
{
break;
}
}
if (!empty($commands)) {
$this->client->execute($commands);
}
try {
$marker = 0;
$finishedNum = 1;
while (true) {
$rt = $this->client->listParts(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'PartNumberMarker' => $marker,
'MaxParts' => 1000,
'UploadId' => $uploadId));
if (!empty($rt['Parts'])) {
foreach ($rt['Parts'] as $part) {
$part = array('PartNumber' => $finishedNum, 'ETag' => $part['ETag']);
$this->parts[$finishedNum] = $part;
$finishedNum++;
}
}
$marker = $rt['NextPartNumberMarker'];
if (!$rt['IsTruncated']) {
break;
}
}
} catch (\Exception $e) {
echo($e);
}
if ($finishedNum == $partNumber) {
return 0;
} else {
return -1;
}
}
private function calculatePartSize($minPartSize)
{
$partSize = intval(ceil(($this->size / self::MAX_PARTS)));
$partSize = max($minPartSize, $partSize);
$partSize = min($partSize, self::MAX_PART_SIZE);
$partSize = max($partSize, self::MIN_PART_SIZE);
return $partSize;
}
private function initiateMultipartUpload() {
$result = $this->client->createMultipartUpload($this->options);
return $result['UploadId'];
}
}
function partUploadCopy($client, $params) {
$rt = $client->uploadPartCopy($params);
// $part = array('PartNumber' => $params['PartNumber'], 'ETag' => $rt['ETag']);
$rt['PartNumber'] = $params['PartNumber'];
return $rt;
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Qcloud\Cos\Exception;
class BucketAlreadyExistsException extends CosException {}

View File

@@ -0,0 +1,6 @@
<?php
namespace Qcloud\Cos\Exception;
// The bucket you tried to delete is not empty.
class BucketNotEmptyException extends CosException {}

View File

@@ -0,0 +1,7 @@
<?php
namespace Qcloud\Cos\Exception;
use Qcloud\Cos\Exception\ServiceResponseException;
class CosException extends ServiceResponseException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace Qcloud\Cos\Exception;
class CurlException extends CosException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace Qcloud\Cos\Exception;
class InvalidArgumentException extends CosException {}

View File

@@ -0,0 +1,6 @@
<?php
namespace Qcloud\Cos\Exception;
// The specified bucket does not exist.
class NoSuchBucketException extends CosException {}

View File

@@ -0,0 +1,6 @@
<?php
namespace Qcloud\Cos\Exception;
// The specified key does not exist.
class NoSuchKeyException extends CosException {}

View File

@@ -0,0 +1,8 @@
<?php
namespace Qcloud\Cos\Exception;
/**
* The specified multipart upload does not exist.
*/
class NoSuchUploadException extends CosException {}

View File

@@ -0,0 +1,189 @@
<?php
namespace Qcloud\Cos\Exception;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
class ServiceResponseException extends \RuntimeException {
/**
* @var Response Response
*/
protected $response;
/**
* @var RequestInterface Request
*/
protected $request;
/**
* @var string Request ID
*/
protected $requestId;
/**
* @var string Exception type (client / server)
*/
protected $exceptionType;
/**
* @var string Exception code
*/
protected $exceptionCode;
/**
* Set the exception code
*
* @param string $code Exception code
*/
public function setExceptionCode($code) {
$this->exceptionCode = $code;
}
/**
* Get the exception code
*
* @return string|null
*/
public function getExceptionCode() {
return $this->exceptionCode;
}
/**
* Set the exception type
*
* @param string $type Exception type
*/
public function setExceptionType($type) {
$this->exceptionType = $type;
}
/**
* Get the exception type (one of client or server)
*
* @return string|null
*/
public function getExceptionType() {
return $this->exceptionType;
}
/**
* Set the request ID
*
* @param string $id Request ID
*/
public function setRequestId($id) {
$this->requestId = $id;
}
/**
* Get the Request ID
*
* @return string|null
*/
public function getRequestId() {
return $this->requestId;
}
/**
* Set the associated response
*
* @param Response $response Response
*/
public function setResponse(Response $response) {
$this->response = $response;
}
/**
* Get the associated response object
*
* @return Response|null
*/
public function getResponse() {
return $this->response;
}
/**
* Set the associated request
*
* @param RequestInterface $request
*/
public function setRequest(RequestInterface $request) {
$this->request = $request;
}
/**
* Get the associated request object
*
* @return RequestInterface|null
*/
public function getRequest() {
return $this->request;
}
/**
* Get the status code of the response
*
* @return int|null
*/
public function getStatusCode() {
return $this->response ? $this->response->getStatusCode() : null;
}
/**
* Cast to a string
*
* @return string
*/
public function __toString() {
$message = get_class($this) . ': '
. 'Cos Error Code: ' . $this->getExceptionCode() . ', '
. 'Status Code: ' . $this->getStatusCode() . ', '
. 'Cos Request ID: ' . $this->getRequestId() . ', '
. 'Cos Error Type: ' . $this->getExceptionType() . ', '
. 'Cos Error Message: ' . $this->getMessage();
// Add the User-Agent if available
if ($this->request) {
$message .= ', ' . 'User-Agent: ' . $this->request->getHeader('User-Agent');
}
return $message;
}
/**
* Get the request ID of the error. This value is only present if a
* response was received, and is not present in the event of a networking
* error.
*
* Same as `getRequestId()` method, but matches the interface for SDKv3.
*
* @return string|null Returns null if no response was received
*/
public function getCosRequestId() {
return $this->requestId;
}
/**
* Get the Cos error type.
*
* Same as `getExceptionType()` method, but matches the interface for SDKv3.
*
* @return string|null Returns null if no response was received
*/
public function getCosErrorType() {
return $this->exceptionType;
}
/**
* Get the Cos error code.
*
* Same as `getExceptionCode()` method, but matches the interface for SDKv3.
*
* @return string|null Returns null if no response was received
*/
public function getCosErrorCode() {
return $this->exceptionCode;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Qcloud\Cos;
use Qcloud\Cos\Exception\ServiceResponseException;
use Qcloud\Cos\Exception\NoSuchKeyException;
use Guzzle\Common\Event;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Converts generic Guzzle response exceptions into cos specific exceptions
*/
class ExceptionListener implements EventSubscriberInterface {
protected $parser;
protected $defaultException;
public function __construct() {
$this->parser = new ExceptionParser();
$this->defaultException = 'Qcloud\Cos\Exception\ServiceResponseException';
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return array('request.error' => array('onRequestError', -1));
}
/**
* Throws a more meaningful request exception if available
*
* @param Event $event Event emitted
*/
public function onRequestError(Event $event) {
$e = $this->fromResponse($event['request'], $event['response']);
$event->stopPropagation();
throw $e;
}
public function fromResponse(RequestInterface $request, Response $response) {
$parts = $this->parser->parse($request, $response);
$className = 'Qcloud\\Cos\\Exception\\' . $parts['code'];
if (substr($className, -9) !== 'Exception') {
$className .= 'Exception';
}
$className = class_exists($className) ? $className : $this->defaultException;
return $this->createException($className, $request, $response, $parts);
}
protected function createException($className, RequestInterface $request, Response $response, array $parts) {
$class = new $className($parts['message']);
if ($class instanceof ServiceResponseException) {
$class->setExceptionCode($parts['code']);
$class->setExceptionType($parts['type']);
$class->setResponse($response);
$class->setRequest($request);
$class->setRequestId($parts['request_id']);
}
return $class;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
/**
* Parses default XML exception responses
*/
class ExceptionParser {
public function parse(RequestInterface $request, Response $response) {
$data = array(
'code' => null,
'message' => null,
'type' => $response->isClientError() ? 'client' : 'server',
'request_id' => null,
'parsed' => null
);
$body = $response->getBody(true);
if (!$body) {
$this->parseHeaders($request, $response, $data);
return $data;
}
try {
$xml = new \SimpleXMLElement($body);
$this->parseBody($xml, $data);
return $data;
} catch (\Exception $e) {
// Gracefully handle parse errors. This could happen when the
// server responds with a non-XML response (e.g., private beta
// services).
$data['code'] = 'PhpInternalXmlParseError';
$data['message'] = 'A non-XML response was received';
return $data;
}
}
/**
* Parses additional exception information from the response headers
*
* @param RequestInterface $request Request that was issued
* @param Response $response The response from the request
* @param array $data The current set of exception data
*/
protected function parseHeaders(RequestInterface $request, Response $response, array &$data) {
$data['message'] = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
if ($requestId = $response->getHeader('x-cos-request-id')) {
$data['request_id'] = $requestId;
$data['message'] .= " (Request-ID: $requestId)";
}
// Get the request
$status = $response->getStatusCode();
$method = $request->getMethod();
// Attempt to determine code for 403s and 404s
if ($status === 403) {
$data['code'] = 'AccessDenied';
} elseif ($method === 'HEAD' && $status === 404) {
$path = explode('/', trim($request->getPath(), '/'));
$host = explode('.', $request->getHost());
$bucket = (count($host) >= 4) ? $host[0] : array_shift($path);
$object = array_shift($path);
if ($bucket && $object) {
$data['code'] = 'NoSuchKey';
} elseif ($bucket) {
$data['code'] = 'NoSuchBucket';
}
}
}
/**
* Parses additional exception information from the response body
*
* @param \SimpleXMLElement $body The response body as XML
* @param array $data The current set of exception data
*/
protected function parseBody(\SimpleXMLElement $body, array &$data) {
$data['parsed'] = $body;
$namespaces = $body->getDocNamespaces();
if (isset($namespaces[''])) {
// Account for the default namespace being defined and PHP not being able to handle it :(
$body->registerXPathNamespace('ns', $namespaces['']);
$prefix = 'ns:';
} else {
$prefix = '';
}
if ($tempXml = $body->xpath("//{$prefix}Code[1]")) {
$data['code'] = (string) $tempXml[0];
}
if ($tempXml = $body->xpath("//{$prefix}Message[1]")) {
$data['message'] = (string) $tempXml[0];
}
$tempXml = $body->xpath("//{$prefix}RequestId[1]");
if (empty($tempXml)) {
$tempXml = $body->xpath("//{$prefix}RequestID[1]");
}
if (isset($tempXml[0])) {
$data['request_id'] = (string) $tempXml[0];
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Common\Event;
use Guzzle\Service\Command\CommandInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Adds required and optional Content-MD5 headers
*/
class Md5Listener implements EventSubscriberInterface
{
/** @var S3SignatureInterface */
private $signature;
public static function getSubscribedEvents()
{
return array('command.after_prepare' => 'onCommandAfterPrepare');
}
public function __construct(Signature $signature)
{
$this->signature = $signature;
}
public function onCommandAfterPrepare(Event $event)
{
$command = $event['command'];
$operation = $command->getOperation();
if ($operation->getData('contentMd5')) {
// Add the MD5 if it is required for all signers
$this->addMd5($command);
} elseif ($operation->hasParam('ContentMD5')) {
$value = $command['ContentMD5'];
// Add a computed MD5 if the parameter is set to true or if
// not using Signature V4 and the value is not set (null).
if ($value === true ||
($value === null && !($this->signature instanceof SignatureV4))
) {
$this->addMd5($command);
}
}
}
private function addMd5(CommandInterface $command)
{
$request = $command->getRequest();
$body = $request->getBody();
if ($body && $body->getSize() > 0) {
if (false !== ($md5 = $body->getContentMd5(true, true))) {
$request->setHeader('Content-MD5', $md5);
}
}
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Http\ReadLimitEntityBody;
use Qcloud\Cos\Exception\CosException;
class MultipartUpload {
/**
* const var: part size from 5MB to 5GB, and max parts of 10000 are allowed for each upload.
*/
const MIN_PART_SIZE = 5242880;
const MAX_PART_SIZE = 5368709120;
const MAX_PARTS = 10000;
private $client;
private $source;
private $options;
private $partSize;
public function __construct($client, $source, $minPartSize, $options = array()) {
$this->client = $client;
$this->source = $source;
$this->options = $options;
$this->partSize = $this->calculatePartSize($minPartSize);
}
public function performUploading() {
$uploadId = $this->initiateMultipartUpload();
$partNumber = 1;
$parts = array();
for (;;) {
if ($this->source->isConsumed()) {
break;
}
$body = new ReadLimitEntityBody($this->source, $this->partSize, $this->source->ftell());
if ($body->getContentLength() == 0) {
break;
}
$result = $this->client->uploadPart(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'Body' => $body,
'UploadId' => $uploadId,
'PartNumber' => $partNumber));
if (md5($body) != substr($result['ETag'], 1, -1)){
throw new CosException("ETag check inconsistency");
}
$part = array('PartNumber' => $partNumber, 'ETag' => $result['ETag']);
array_push($parts, $part);
++$partNumber;
}
try {
$rt = $this->client->completeMultipartUpload(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'UploadId' => $uploadId,
'Parts' => $parts));
} catch(\Exception $e){
throw $e;
}
return $rt;
}
public function resumeUploading() {
$uploadId = $this->options['UploadId'];
$rt = $this->client->ListParts(
array('UploadId' => $uploadId,
'Bucket'=>$this->options['Bucket'],
'Key'=>$this->options['Key']));
$parts = array();
$offset = $this->partSize;
if (count($rt['Parts']) > 0) {
foreach ($rt['Parts'] as $part) {
$parts[$part['PartNumber'] - 1] = array('PartNumber' => $part['PartNumber'], 'ETag' => $part['ETag']);
}
}
for ($partNumber = 1;;++$partNumber,$offset+=$body->getContentLength()) {
if ($this->source->isConsumed()) {
break;
}
$body = new ReadLimitEntityBody($this->source, $this->partSize, $this->source->ftell());
if ($body->getContentLength() == 0) {
break;
}
if (array_key_exists($partNumber-1,$parts)){
if (md5($body) != substr($parts[$partNumber-1]['ETag'], 1, -1)){
throw new CosException("ETag check inconsistency");
}
$body->setOffset($offset);
continue;
}
$result = $this->client->uploadPart(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'Body' => $body,
'UploadId' => $uploadId,
'PartNumber' => $partNumber));
if (md5($body) != substr($result['ETag'], 1, -1)){
throw new CosException("ETag check inconsistency");
}
$parts[$partNumber-1] = array('PartNumber' => $partNumber, 'ETag' => $result['ETag']);
}
$rt = $this->client->completeMultipartUpload(array(
'Bucket' => $this->options['Bucket'],
'Key' => $this->options['Key'],
'UploadId' => $uploadId,
'Parts' => $parts));
return $rt;
}
private function calculatePartSize($minPartSize) {
$partSize = intval(ceil(($this->source->getContentLength() / self::MAX_PARTS)));
$partSize = max($minPartSize, $partSize);
$partSize = min($partSize, self::MAX_PART_SIZE);
$partSize = max($partSize, self::MIN_PART_SIZE);
return $partSize;
}
private function initiateMultipartUpload() {
$result = $this->client->createMultipartUpload($this->options);
return $result['UploadId'];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Http\Message\RequestInterface;
class Signature {
private $accessKey; // string: access key.
private $secretKey; // string: secret key.
public function __construct($accessKey, $secretKey) {
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
}
public function __destruct() {
}
public function signRequest(RequestInterface $request) {
$signTime = (string)(time() - 60) . ';' . (string)(time() + 3600);
$httpString = strtolower($request->getMethod()) . "\n" . urldecode($request->getPath()) .
"\n\nhost=" . $request->getHost() . "\n";
$sha1edHttpString = sha1($httpString);
$stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
$signKey = hash_hmac('sha1', $signTime, $this->secretKey);
$signature = hash_hmac('sha1', $stringToSign, $signKey);
$authorization = 'q-sign-algorithm=sha1&q-ak='. $this->accessKey .
"&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=host&q-url-param-list=&" .
"q-signature=$signature";
$request->setHeader('Authorization', $authorization);
}
public function createAuthorization(
RequestInterface $request,
$expires = "10 minutes"
) {
$signTime = (string)(time() - 60) . ';' . (string)(strtotime($expires));
$httpString = strtolower($request->getMethod()) . "\n" . urldecode($request->getPath()) .
"\n\nhost=" . $request->getHost() . "\n";
$sha1edHttpString = sha1($httpString);
$stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
$signKey = hash_hmac('sha1', $signTime, $this->secretKey);
$signature = hash_hmac('sha1', $stringToSign, $signKey);
$authorization = 'q-sign-algorithm=sha1&q-ak='. $this->accessKey .
"&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=host&q-url-param-list=&" .
"q-signature=$signature";
return $authorization;
}
public function createPresignedUrl(
RequestInterface $request,
$expires = "10 minutes"
) {
$authorization = $this->createAuthorization($request, $expires);
$request->getQuery()->add('sign', $authorization);
return $request->getUrl();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Qcloud\Cos;
use Guzzle\Common\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Listener used to sign requests before they are sent over the wire.
*/
class SignatureListener implements EventSubscriberInterface {
// cos signature.
protected $signature;
/**
* Construct a new request signing plugin
*/
public function __construct($accessKey, $secretKey) {
$this->signature = new Signature($accessKey, $secretKey);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
'request.before_send' => array('onRequestBeforeSend', -255));
}
/**
* Signs requests before they are sent
*
* @param Event $event Event emitted
*/
public function onRequestBeforeSend(Event $event) {
$this->signature->signRequest($event['request']);
/*
if(!$this->credentials instanceof NullCredentials) {
$this->signature->signRequest($event['request'], $this->credentials);
}
*/
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
<?php
namespace Qcloud\Cos\Tests;
use Qcloud\Cos\Client;
class TestHelper {
public static function nuke($bucket) {
try {
$cosClient = new Client(array('region' => getenv('COS_REGION'),
'credentials'=> array(
'appId' => getenv('COS_APPID'),
'secretId' => getenv('COS_KEY'),
'secretKey' => getenv('COS_SECRET'))));
$result = $cosClient->listObjects(array('Bucket' => $bucket));
if ($result->get('Contents')) {
foreach ($result ->get('Contents') as $content) {
$cosClient->deleteObject(array('Bucket' => $bucket, 'Key' => $content['Key']));
}
}
$cosClient->deleteBucket(array('Bucket' => $bucket));
while(True){
$result = $cosClient->ListMultipartUploads(
array('Bucket' => $bucket,
'Prefix' => ''));
if (count($result['Uploads']) == 0){
break;
}
foreach ($result['Uploads'] as $upload) {
try {
$rt = $cosClient->AbortMultipartUpload(
array('Bucket' => $bucket,
'Key' => $upload['Key'],
'UploadId' => $upload['UploadId']));
print_r($rt);
} catch (\Exception $e) {
print_r($e);
}
}
}
} catch (\Exception $e) {
//echo "$e\n";
// Ignore
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Qcloud\Cos;
use Aws\Common\Credentials\CredentialsInterface;
use Aws\Common\Credentials\NullCredentials;
use Guzzle\Common\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Listener used to sign requests before they are sent over the wire.
*/
class TokenListener implements EventSubscriberInterface {
// cos signature.
protected $token;
/**
* Construct a new request signing plugin
*/
public function __construct($token) {
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
'request.before_send' => array('onRequestBeforeSend', -240));
}
/**
* Signs requests before they are sent
*
* @param Event $event Event emitted
*/
public function onRequestBeforeSend(Event $event) {
if ($this->token != null) {
$event['request']->setHeader('x-cos-security-token', $this->token);
}
/*
if(!$this->credentials instanceof NullCredentials) {
$this->signature->signRequest($event['request'], $this->credentials);
}
*/
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Qcloud\Cos;
use Qcloud\Cos\Exception\InvalidArgumentException;
use Guzzle\Common\Event;
use Guzzle\Http\EntityBody;
use Guzzle\Service\Command\AbstractCommand as Command;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Prepares the body parameter of a command such that the parameter is more flexible
* (e.g. accepts file handles) with the value it accepts but converts it to the correct format
* for the command. Also looks for a "Filename" parameter.
*/
class UploadBodyListener implements EventSubscriberInterface {
/**
* @var array The names of the commands of which to modify the body parameter
*/
protected $commands;
/**
* @var string The key for the upload body parameter
*/
protected $bodyParameter;
/**
* @var string The key for the source file parameter
*/
protected $sourceParameter;
/**
* @param array $commands The commands to modify
* @param string $bodyParameter The key for the body parameter
* @param string $sourceParameter The key for the source file parameter
*/
public function __construct(array $commands, $bodyParameter = 'Body', $sourceParameter = 'SourceFile') {
$this->commands = $commands;
$this->bodyParameter = (string) $bodyParameter;
$this->sourceParameter = (string) $sourceParameter;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return array('command.before_prepare' => array('onCommandBeforePrepare'));
}
/**
* Converts filenames and file handles into EntityBody objects before the command is validated
*
* @param Event $event Event emitted
* @throws InvalidArgumentException
*/
public function onCommandBeforePrepare(Event $event) {
/** @var Command $command */
$command = $event['command'];
if (in_array($command->getName(), $this->commands)) {
// Get the interesting parameters
$source = $command->get($this->sourceParameter);
$body = $command->get($this->bodyParameter);
// If a file path is passed in then get the file handle
if (is_string($source) && file_exists($source)) {
$body = fopen($source, 'rb');
}
// Prepare the body parameter and remove the source file parameter
if (null !== $body) {
$command->remove($this->sourceParameter);
$command->set($this->bodyParameter, EntityBody::factory($body));
} else {
throw new InvalidArgumentException(
"You must specify a non-null value for the {$this->bodyParameter} or {$this->sourceParameter} parameters.");
}
}
}
}