Файл: includes/library/aws/Aws/S3/S3Client.php
Строк: 652
<?php
namespace AwsS3;
use AwsApiApiProvider;
use AwsApiDocModel;
use AwsApiService;
use AwsAwsClient;
use AwsClientResolver;
use AwsCommand;
use AwsExceptionAwsException;
use AwsHandlerList;
use AwsMiddleware;
use AwsRetryMiddleware;
use AwsS3ExceptionS3Exception;
use AwsResultInterface;
use AwsCommandInterface;
use GuzzleHttpPsr7;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageStreamInterface;
/**
* Client used to interact with **Amazon Simple Storage Service (Amazon S3)**.
*/
class S3Client extends AwsClient
{
public static function getArguments()
{
$args = parent::getArguments();
$args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
$args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider'];
return $args + [
'bucket_endpoint' => [
'type' => 'config',
'valid' => ['bool'],
'doc' => 'Set to true to send requests to a hardcoded '
. 'bucket endpoint rather than create an endpoint as a '
. 'result of injecting the bucket into the URL. This '
. 'option is useful for interacting with CNAME endpoints.',
],
];
}
/**
* {@inheritdoc}
*
* In addition to the options available to
* {@see AwsAwsClient::__construct}, S3Client accepts the following
* options:
*
* - bucket_endpoint: (bool) Set to true to send requests to a
* hardcoded bucket endpoint rather than create an endpoint as a result
* of injecting the bucket into the URL. This option is useful for
* interacting with CNAME endpoints.
* - calculate_md5: (bool) Set to false to disable calculating an MD5
* for all Amazon S3 signed uploads.
*
* @param array $args
*/
public function __construct(array $args)
{
parent::__construct($args);
$stack = $this->getHandlerList();
$stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec');
$stack->appendBuild(ApplyMd5Middleware::wrap(), 's3.md5');
$stack->appendBuild(
Middleware::contentType(['PutObject', 'UploadPart']),
's3.content_type'
);
// Use the bucket style middleware when using a "bucket_endpoint" (for cnames)
if ($this->getConfig('bucket_endpoint')) {
$stack->appendBuild(BucketEndpointMiddleware::wrap(), 's3.bucket_endpoint');
}
$stack->appendSign(PutObjectUrlMiddleware::wrap(), 's3.put_object_url');
$stack->appendSign(PermanentRedirectMiddleware::wrap(), 's3.permanent_redirect');
$stack->appendInit(Middleware::sourceFile($this->getApi()), 's3.source_file');
$stack->appendInit($this->getSaveAsParameter(), 's3.save_as');
$stack->appendInit($this->getLocationConstraintMiddleware(), 's3.location');
}
/**
* Determine if a string is a valid name for a DNS compatible Amazon S3
* bucket.
*
* DNS compatible bucket names can be used as a subdomain in a URL (e.g.,
* "<bucket>.s3.amazonaws.com").
*
* @param string $bucket Bucket name to check.
*
* @return bool
*/
public static function isBucketDnsCompatible($bucket)
{
$bucketLen = strlen($bucket);
return ($bucketLen >= 3 && $bucketLen <= 63) &&
// Cannot look like an IP address
!filter_var($bucket, FILTER_VALIDATE_IP) &&
preg_match('/^[a-z0-9]([a-z0-9-.]*[a-z0-9])?$/', $bucket);
}
/**
* Create a pre-signed URL for the given S3 command object.
*
* @param CommandInterface $command Command to create a pre-signed
* URL for.
* @param int|string|DateTime $expires The time at which the URL should
* expire. This can be a Unix
* timestamp, a PHP DateTime object,
* or a string that can be evaluated
* by strtotime().
*
* @return RequestInterface
*/
public function createPresignedRequest(CommandInterface $command, $expires)
{
/** @var AwsSignatureSignatureInterface $signer */
$signer = call_user_func(
$this->getSignatureProvider(),
$this->getConfig('signature_version'),
$this->getApi()->getSigningName(),
$this->getRegion()
);
return $signer->presign(
Awsserialize($command),
$this->getCredentials()->wait(),
$expires
);
}
/**
* Returns the URL to an object identified by its bucket and key.
*
* The URL returned by this method is not signed nor does it ensure the the
* bucket and key given to the method exist. If you need a signed URL, then
* use the {@see AwsS3S3Client::createPresignedRequest} method and get
* the URI of the signed request.
*
* @param string $bucket The name of the bucket where the object is located
* @param string $key The key of the object
*
* @return string The URL to the object
*/
public function getObjectUrl($bucket, $key)
{
$command = $this->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $key
]);
return (string) Awsserialize($command)->getUri();
}
/**
* Determines whether or not a bucket exists by name.
*
* @param string $bucket The name of the bucket
*
* @return bool
*/
public function doesBucketExist($bucket)
{
return $this->checkExistenceWithCommand(
$this->getCommand('HeadBucket', ['Bucket' => $bucket])
);
}
/**
* 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 available in the HeadObject
* operation (e.g., VersionId).
*
* @return bool
*/
public function doesObjectExist($bucket, $key, array $options = [])
{
return $this->checkExistenceWithCommand(
$this->getCommand('HeadObject', [
'Bucket' => $bucket,
'Key' => $key
] + $options)
);
}
/**
* Raw URL encode a key and allow for '/' characters
*
* @param string $key Key to encode
*
* @return string Returns the encoded key
*/
public static function encodeKey($key)
{
return str_replace('%2F', '/', rawurlencode($key));
}
/**
* Register the Amazon S3 stream wrapper with this client instance.
*/
public function registerStreamWrapper()
{
StreamWrapper::register($this);
}
/**
* Deletes objects from Amazon S3 that match the result of a ListObjects
* operation. For example, this allows you to do things like delete all
* objects that match a specific key prefix.
*
* @param string $bucket Bucket that contains the object keys
* @param string $prefix Optionally delete only objects under this key prefix
* @param string $regex Delete only objects that match this regex
* @param array $options AwsS3BatchDelete options array.
*
* @see AwsS3S3Client::listObjects
* @throws RuntimeException if no prefix and no regex is given
*/
public function deleteMatchingObjects(
$bucket,
$prefix = '',
$regex = '',
array $options = []
) {
if (!$prefix && !$regex) {
throw new RuntimeException('A prefix or regex is required.');
}
$params = ['Bucket' => $bucket, 'Prefix' => $prefix];
$iter = $this->getIterator('ListObjects', $params);
if ($regex) {
$iter = Awsfilter($iter, function ($c) use ($regex) {
return preg_match($regex, $c['Key']);
});
}
BatchDelete::fromIterator($this, $bucket, $iter, $options)->delete();
}
/**
* Upload a file, stream, or string to a bucket.
*
* If the upload size exceeds the specified threshold, the upload will be
* performed using concurrent multipart uploads.
*
* The options array accepts the following options:
*
* - before_upload: (callable) Callback to invoke before any upload
* operations during the upload process. The callback should have a
* function signature like `function (AwsCommand $command) {...}`.
* - concurrency: (int, default=int(3)) Maximum number of concurrent
* `UploadPart` operations allowed during a multipart upload.
* - mup_threshold: (int, default=int(16777216)) The size, in bytes, allowed
* before the upload must be sent via a multipart upload. Default: 16 MB.
* - params: (array, default=array([])) Custom parameters to use with the
* upload. For single uploads, they must correspond to those used for the
* `PutObject` operation. For multipart uploads, they correspond to the
* parameters of the `CreateMultipartUpload` operation.
* - part_size: (int) Part size to use when doing a multipart upload.
*
* @param string $bucket Bucket to upload the object.
* @param string $key Key of the object.
* @param mixed $body Object data to upload. Can be a
* StreamInterface, PHP stream resource, or a
* string of data to upload.
* @param string $acl ACL to apply to the object (default: private).
* @param array $options Options used to configure the upload process.
*
* @see AwsS3MultipartUploader for more info about multipart uploads.
* @return ResultInterface Returns the result of the upload.
*/
public function upload($bucket, $key, $body, $acl = 'private', array $options = [])
{
// Prepare the options.
static $defaults = [
'before_upload' => null,
'concurrency' => 3,
'mup_threshold' => 16777216,
'params' => [],
'part_size' => null,
];
$options = array_intersect_key($options + $defaults, $defaults);
// Perform the required operations to upload the S3 Object.
$body = Psr7stream_for($body);
if ($this->requiresMultipart($body, $options['mup_threshold'])) {
// Perform a multipart upload.
$options['before_initiate'] = function ($command) use ($options) {
foreach ($options['params'] as $k => $v) {
$command[$k] = $v;
}
};
return (new MultipartUploader($this, $body, [
'bucket' => $bucket,
'key' => $key,
'acl' => $acl
] + $options))->upload();
} else {
// Perform a regular PutObject operation.
$command = $this->getCommand('PutObject', [
'Bucket' => $bucket,
'Key' => $key,
'Body' => $body,
'ACL' => $acl,
] + $options['params']);
if (is_callable($options['before_upload'])) {
$options['before_upload']($command);
}
return $this->execute($command);
}
}
/**
* Recursively uploads all files in a given directory to a given bucket.
*
* @param string $directory Full path to a directory to upload
* @param string $bucket Name of the bucket
* @param string $keyPrefix Virtual directory key prefix to add to each upload
* @param array $options Options available in AwsS3Transfer::__construct
*
* @see AwsS3Transfer for more options and customization
*/
public function uploadDirectory(
$directory,
$bucket,
$keyPrefix = null,
array $options = []
) {
$d = "s3://$bucket" . ($keyPrefix ? '/' . ltrim($keyPrefix, '/') : '');
(new Transfer($this, $directory, $d, $options))->transfer();
}
/**
* Downloads a bucket to the local filesystem
*
* @param string $directory Directory to download to
* @param string $bucket Bucket to download from
* @param string $keyPrefix Only download objects that use this key prefix
* @param array $options Options available in AwsS3Transfer::__construct
*/
public function downloadBucket(
$directory,
$bucket,
$keyPrefix = '',
array $options = []
) {
$s = "s3://$bucket" . ($keyPrefix ? '/' . ltrim($keyPrefix, '/') : '');
(new Transfer($this, $s, $directory, $options))->transfer();
}
/**
* Determines if the body should be uploaded using PutObject or the
* Multipart Upload System. It also modifies the passed-in $body as needed
* to support the upload.
*
* @param StreamInterface $body Stream representing the body.
* @param integer $threshold Minimum bytes before using Multipart.
*
* @return bool
*/
private function requiresMultipart(StreamInterface &$body, $threshold)
{
// If body size known, compare to threshold to determine if Multipart.
if ($body->getSize() !== null) {
return $body->getSize() >= $threshold;
}
// Handle the situation where the body size is unknown.
// Read up to 5MB into a buffer to determine how to upload the body.
$buffer = Psr7stream_for();
Psr7copy_to_stream($body, $buffer, MultipartUploader::PART_MIN_SIZE);
// If body < 5MB, use PutObject with the buffer.
if ($buffer->getSize() < MultipartUploader::PART_MIN_SIZE) {
$buffer->seek(0);
$body = $buffer;
return false;
}
// If body >= 5 MB, then use multipart. [YES]
if ($body->isSeekable()) {
// If the body is seekable, just rewind the body.
$body->seek(0);
} else {
// If the body is non-seekable, stitch the rewind the buffer and
// the partially read body together into one stream. This avoids
// unnecessary disc usage and does not require seeking on the
// original stream.
$buffer->seek(0);
$body = new Psr7AppendStream([$buffer, $body]);
}
return true;
}
/**
* Determines whether or not a resource exists using a command
*
* @param CommandInterface $command Command used to poll for the resource
*
* @return bool
* @throws S3Exception|Exception if there is an unhandled exception
*/
private function checkExistenceWithCommand(CommandInterface $command)
{
try {
$this->execute($command);
return true;
} catch (S3Exception $e) {
if ($e->getAwsErrorCode() == 'AccessDenied') {
return true;
}
if ($e->getStatusCode() >= 500) {
throw $e;
}
return false;
}
}
/**
* Provides a middleware that removes the need to specify LocationConstraint on CreateBucket.
*
* @return Closure
*/
private function getLocationConstraintMiddleware()
{
return function (callable $handler) {
return function (Command $command, $request = null) use ($handler) {
if ($command->getName() === 'CreateBucket') {
$region = $this->getRegion();
if ($region === 'us-east-1') {
unset($command['CreateBucketConfiguration']);
} else {
$command['CreateBucketConfiguration'] = ['LocationConstraint' => $region];
}
}
return $handler($command, $request);
};
};
}
/**
* Provides a middleware that supports the `SaveAs` parameter.
*
* @return Closure
*/
private function getSaveAsParameter()
{
return function (callable $handler) {
return function (Command $command, $request = null) use ($handler) {
if ($command->getName() === 'GetObject' && isset($command['SaveAs'])) {
$command['@http']['sink'] = $command['SaveAs'];
unset($command['SaveAs']);
}
return $handler($command, $request);
};
};
}
/** @internal */
public static function _applyRetryConfig($value, $_, HandlerList $list)
{
if (!$value) {
return;
}
$decider = RetryMiddleware::createDefaultDecider($value);
$decider = function ($retries, $request, $result, $error) use ($decider) {
if ($decider($retries, $request, $result, $error)) {
return true;
} elseif ($error instanceof AwsException) {
return $error->getResponse()
&& strpos(
$error->getResponse()->getBody(),
'Your socket connection to the server'
) !== false;
}
return false;
};
$delay = [RetryMiddleware::class, 'exponentialDelay'];
$list->appendSign(Middleware::retry($decider, $delay), 'retry');
}
/** @internal */
public static function _applyApiProvider($value, array &$args, HandlerList $list)
{
ClientResolver::_apply_api_provider($value, $args, $list);
$args['parser'] = new GetBucketLocationParser($args['parser']);
}
/**
* @internal
* @codeCoverageIgnore
*/
public static function applyDocFilters(array $api, array $docs)
{
$b64 = '<div class="alert alert-info">This value will be base64 '
. 'encoded on your behalf.</div>';
// Add the SourceFile parameter.
$docs['shapes']['SourceFile']['base'] = 'The path to a file on disk to use instead of the Body parameter.';
$api['shapes']['SourceFile'] = ['type' => 'string'];
$api['shapes']['PutObjectRequest']['members']['SourceFile'] = ['shape' => 'SourceFile'];
$api['shapes']['UploadPartRequest']['members']['SourceFile'] = ['shape' => 'SourceFile'];
// Add the SaveAs parameter.
$docs['shapes']['SaveAs']['base'] = 'The path to a file on disk to save the object data.';
$api['shapes']['SaveAs'] = ['type' => 'string'];
$api['shapes']['GetObjectRequest']['members']['SaveAs'] = ['shape' => 'SaveAs'];
// Several SSECustomerKey documentation updates.
$docs['shapes']['SSECustomerKey']['append'] = $b64;
$docs['shapes']['CopySourceSSECustomerKey']['append'] = $b64;
$docs['shapes']['SSECustomerKeyMd5']['append'] = '<div class="alert alert-info">The value will be computed on '
. 'your behalf if it is not supplied.</div>';
// Add the ObjectURL to various output shapes and documentation.
$docs['shapes']['ObjectURL']['base'] = 'The URI of the created object.';
$api['shapes']['ObjectURL'] = ['type' => 'string'];
$api['shapes']['PutObjectOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL'];
$api['shapes']['CopyObjectOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL'];
$api['shapes']['CompleteMultipartUploadOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL'];
// Fix references to Location Constraint.
unset($api['shapes']['CreateBucketRequest']['payload']);
$api['shapes']['BucketLocationConstraint']['enum'] = [
"ap-northeast-1",
"ap-southeast-2",
"ap-southeast-1",
"cn-north-1",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-west-1",
"us-west-2",
"sa-east-1",
];
// Add a note that the ContentMD5 is optional.
$docs['shapes']['ContentMD5']['append'] = '<div class="alert alert-info">The value will be computed on '
. 'your behalf.</div>';
return [
new Service($api, ApiProvider::defaultProvider()),
new DocModel($docs)
];
}
}