allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

213
vendor/google/cloud-storage/src/Acl.php vendored Normal file
View File

@@ -0,0 +1,213 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use InvalidArgumentException;
/**
* Google Cloud Storage uses access control lists (ACLs) to manage bucket and
* object access. ACLs are the mechanism you use to share objects with other
* users and allow other users to access your buckets and objects. For more
* information please see the overview on
* [access-control](https://cloud.google.com/storage/docs/access-control).
*
* Example:
* ```
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
*
* $bucket = $storage->bucket('my-bucket');
* $acl = $bucket->acl();
* ```
*/
class Acl
{
const ROLE_READER = 'READER';
const ROLE_WRITER = 'WRITER';
const ROLE_OWNER = 'OWNER';
/**
* @var ConnectionInterface Represents a connection to Cloud Storage.
* @internal
*/
protected $connection;
/**
* @var array ACL specific options.
*/
private $aclOptions;
/**
* @param ConnectionInterface $connection Represents a connection to
* Cloud Storage. This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param string $type The type of access control this instance applies to.
* @param array $identity Represents which bucket, file, or generation this
* instance applies to.
* @throws \InvalidArgumentException Thrown when an invalid type is passed in.
*/
public function __construct(ConnectionInterface $connection, $type, array $identity)
{
$validTypes = [
'bucketAccessControls',
'defaultObjectAccessControls',
'objectAccessControls'
];
if (!in_array($type, $validTypes)) {
throw new InvalidArgumentException('type must be one of the following: ' . implode(', ', $validTypes));
}
$this->connection = $connection;
$this->aclOptions = $identity + ['type' => $type];
}
/**
* Delete access controls.
*
* Delete access controls on a {@see Bucket} or
* {@see StorageObject} for a specified entity.
*
* Example:
* ```
* $acl->delete('allAuthenticatedUsers');
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/delete BucketAccessControls delete
* API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/defaultObjectAccessControls/delete
* DefaultObjectAccessControls delete API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls/delete ObjectAccessControls delete
* API documentation.
*
* @param string $entity The entity to delete.
* @param array $options [optional] Configuration Options.
* @return void
*/
public function delete($entity, array $options = [])
{
$aclOptions = $this->aclOptions + ['entity' => $entity];
$this->connection->deleteAcl($options + $aclOptions);
}
/**
* Get access controls.
*
* Get access controls on a {@see Bucket} or
* {@see StorageObject}. By default this will return all available
* access controls. You may optionally specify a single entity to return
* details for as well.
*
* Example:
* ```
* $res = $acl->get(['entity' => 'allAuthenticatedUsers']);
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/get BucketAccessControls get API
* documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/defaultObjectAccessControls/get
* DefaultObjectAccessControls get API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls/get ObjectAccessControls get API
* documentation.
*
* @param array $options [optional] {
* Configuration options.
*
* @type string $entity The entity to fetch.
* }
* @return array
*/
public function get(array $options = [])
{
if (isset($options['entity'])) {
return $this->connection->getAcl($options + $this->aclOptions);
}
$response = $this->connection->listAcl($options + $this->aclOptions);
return $response['items'];
}
/**
* Add access controls.
*
* Add access controls on a {@see Bucket} or
* {@see StorageObject}.
*
* Example:
* ```
* $acl->add('allAuthenticatedUsers', 'WRITER');
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/insert BucketAccessControls insert
* API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/defaultObjectAccessControls/insert
* DefaultObjectAccessControls insert API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls/insert ObjectAccessControls insert
* API documentation.
*
* @param string $entity The entity to add access controls to.
* @param string $role The permissions to add for the specified entity. May
* be one of 'OWNER', 'READER', or 'WRITER'.
* @param array $options [optional] Configuration Options.
* @return array
*/
public function add($entity, $role, array $options = [])
{
$aclOptions = $this->aclOptions + [
'entity' => $entity,
'role' => $role
];
return $this->connection->insertAcl($options + $aclOptions);
}
/**
* Update access controls.
*
* Update access controls on a {@see Bucket} or {@see StorageObject}.
*
* Example:
* ```
* $acl->update('allAuthenticatedUsers', 'READER');
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/patch BucketAccessControls patch API
* documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/defaultObjectAccessControls/patch
* DefaultObjectAccessControls patch API documentation.
* @see https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls/patch ObjectAccessControls patch
* API documentation.
*
* @param string $entity The entity to update access controls for.
* @param string $role The permissions to update for the specified entity.
* May be one of 'OWNER', 'READER', or 'WRITER'.
* @param array $options [optional] Configuration Options.
* @return array
*/
public function update($entity, $role, array $options = [])
{
$aclOptions = $this->aclOptions + [
'entity' => $entity,
'role' => $role
];
return $this->connection->patchAcl($options + $aclOptions);
}
}

1771
vendor/google/cloud-storage/src/Bucket.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
<?php
/**
* Copyright 2025 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use ArrayObject;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\Iterator\PageIterator;
/**
* Iterates over a set of buckets.
*
* Use the `unreachable()` method to get a list of bucket names that could not
* be retrieved. This is only populated when the `returnPartialSuccess` is set to true
*/
class BucketIterator extends ItemIterator
{
/**
* @param PageIterator $iterator
* @param ArrayObject $unreachable
*/
public function __construct(PageIterator $iterator, private ArrayObject $unreachable)
{
parent::__construct($iterator);
}
/**
* Get the list of unreachable buckets.
*
* @return array<string>
*/
public function unreachable()
{
return $this->unreachable->getArrayCopy();
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage\Connection;
/**
* Represents a connection to
* [Cloud Storage](https://cloud.google.com/storage/).
*
* @internal
*/
interface ConnectionInterface
{
/**
* @param array $args
*/
public function deleteAcl(array $args = []);
/**
* @param array $args
*/
public function getAcl(array $args = []);
/**
* @param array $args
*/
public function listAcl(array $args = []);
/**
* @param array $args
*/
public function insertAcl(array $args = []);
/**
* @param array $args
*/
public function patchAcl(array $args = []);
/**
* @param array $args
*/
public function deleteBucket(array $args = []);
/**
* @param array $args
*/
public function restoreBucket(array $args = []);
/**
* @param array $args
*/
public function getBucket(array $args = []);
/**
* @param array $args
*/
public function listBuckets(array $args = []);
/**
* @param array $args
*/
public function insertBucket(array $args = []);
/**
* @param array $args
*/
public function getBucketIamPolicy(array $args);
/**
* @param array $args
*/
public function setBucketIamPolicy(array $args);
/**
* @param array $args
*/
public function testBucketIamPermissions(array $args);
/**
* @param array $args
*/
public function patchBucket(array $args = []);
/**
* @param array $args
*/
public function deleteObject(array $args = []);
/**
* @param array $args
*/
public function restoreObject(array $args = []);
/**
* @param array $args
*/
public function copyObject(array $args = []);
/**
* @param array $args
*/
public function rewriteObject(array $args = []);
/**
* @param array $args
*/
public function moveObject(array $args = []);
/**
* @param array $args
*/
public function composeObject(array $args = []);
/**
* @param array $args
*/
public function getObject(array $args = []);
/**
* @param array $args
*/
public function listObjects(array $args = []);
/**
* @param array $args
*/
public function patchObject(array $args = []);
/**
* @param array $args
*/
public function downloadObject(array $args = []);
/**
* @param array $args
*/
public function insertObject(array $args = []);
/**
* @param array $args
*/
public function getNotification(array $args = []);
/**
* @param array $args
*/
public function deleteNotification(array $args = []);
/**
* @param array $args
*/
public function insertNotification(array $args = []);
/**
* @param array $args
*/
public function listNotifications(array $args = []);
/**
* @param array $args
*/
public function getServiceAccount(array $args = []);
/**
* @param array $args
*/
public function lockRetentionPolicy(array $args = []);
/**
* @param array $args
*/
public function createHmacKey(array $args = []);
/**
* @param array $args
*/
public function deleteHmacKey(array $args = []);
/**
* @param array $args
*/
public function getHmacKey(array $args = []);
/**
* @param array $args
*/
public function updateHmacKey(array $args = []);
/**
* @param array $args
*/
public function listHmacKeys(array $args = []);
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage\Connection;
use Google\Cloud\Core\Iam\IamConnectionInterface;
/**
* IAM Implementation for GCS Buckets
*
* @internal
*/
class IamBucket implements IamConnectionInterface
{
/**
* @var ConnectionInterface
*/
private $connection;
/**
* @param ConnectionInterface $connection
*/
public function __construct(ConnectionInterface $connection)
{
$this->connection = $connection;
}
/**
* @param array $args
*/
public function getPolicy(array $args)
{
if (isset($args['requestedPolicyVersion'])) {
$args['optionsRequestedPolicyVersion'] = $args['requestedPolicyVersion'];
unset($args['requestedPolicyVersion']);
}
return $this->connection->getBucketIamPolicy($args);
}
/**
* @param array $args
*/
public function setPolicy(array $args)
{
unset($args['resource']);
return $this->connection->setBucketIamPolicy($args);
}
/**
* @param array $args
*/
public function testPermissions(array $args)
{
unset($args['resource']);
return $this->connection->testBucketIamPermissions($args);
}
}

View File

@@ -0,0 +1,915 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage\Connection;
use Google\Auth\GetUniverseDomainInterface;
use Google\Cloud\Core\RequestBuilder;
use Google\Cloud\Core\RequestWrapper;
use Google\Cloud\Core\RestTrait;
use Google\Cloud\Core\Retry;
use Google\Cloud\Core\Upload\AbstractUploader;
use Google\Cloud\Core\Upload\MultipartUploader;
use Google\Cloud\Core\Upload\ResumableUploader;
use Google\Cloud\Core\Upload\StreamableUploader;
use Google\Cloud\Core\UriTrait;
use Google\Cloud\Storage\StorageClient;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\MimeType;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Ramsey\Uuid\Uuid;
/**
* Implementation of the
* [Google Cloud Storage JSON API](https://cloud.google.com/storage/docs/json_api/).
*
* @internal
*/
class Rest implements ConnectionInterface
{
use RestTrait {
send as private traitSend;
}
use RetryTrait;
use UriTrait;
/**
* Header and value that helps us identify a transcoded obj
* w/o making a metadata(info) call.
*/
private const TRANSCODED_OBJ_HEADER_KEY = 'X-Goog-Stored-Content-Encoding';
private const TRANSCODED_OBJ_HEADER_VAL = 'gzip';
/**
* @deprecated
*/
const BASE_URI = 'https://storage.googleapis.com/storage/v1/';
/**
* @deprecated
*/
const DEFAULT_API_ENDPOINT = 'https://storage.googleapis.com';
const DEFAULT_API_ENDPOINT_TEMPLATE = 'https://storage.UNIVERSE_DOMAIN';
/**
* @deprecated
*/
const UPLOAD_URI = 'https://storage.googleapis.com/upload/storage/v1/b/{bucket}/o{?query*}';
const UPLOAD_PATH = 'upload/storage/v1/b/{bucket}/o{?query*}';
/**
* @deprecated
*/
const DOWNLOAD_URI = 'https://storage.googleapis.com/storage/v1/b/{bucket}/o/{object}{?query*}';
const DOWNLOAD_PATH = 'storage/v1/b/{bucket}/o/{object}{?query*}';
/**
* @var string
*/
private $projectId;
/**
* @var string
*/
private $apiEndpoint;
/**
* @var callable
* value null accepted
*/
private $restRetryFunction;
/**
* @var string|null
*/
private ?string $retryStrategy;
/**
* @var callable|null
*/
private $restDelayFunction;
/**
* @var callable|null
*/
private $restCalcDelayFunction;
/**
* @var callable|null
*/
private $restRetryListener;
/**
* @param array $config
*/
public function __construct(array $config = [])
{
$config += [
'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/storage-v1.json',
'componentVersion' => StorageClient::VERSION,
'apiEndpoint' => null,
// If the user has not supplied a universe domain, use the environment variable if set.
// Otherwise, use the default ("googleapis.com").
'universeDomain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN')
?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
// Cloud Storage needs to provide a default scope because the Storage
// API does not accept JWTs with "audience"
'scopes' => StorageClient::FULL_CONTROL_SCOPE,
];
$this->apiEndpoint = $this->getApiEndpoint(null, $config, self::DEFAULT_API_ENDPOINT_TEMPLATE);
$this->setRequestWrapper(new RequestWrapper($config));
$this->setRequestBuilder(new RequestBuilder(
$config['serviceDefinitionPath'],
$this->apiEndpoint
));
$this->projectId = $this->pluck('projectId', $config, false);
$this->restRetryFunction = (isset($config['restRetryFunction'])) ? $config['restRetryFunction'] : null;
$this->retryStrategy = $config['retryStrategy'] ?? null;
$this->restDelayFunction = $config['restDelayFunction'] ?? null;
$this->restCalcDelayFunction = $config['restCalcDelayFunction'] ?? null;
$this->restRetryListener = $config['restRetryListener'] ?? null;
}
/**
* @return string
*/
public function projectId()
{
return $this->projectId;
}
/**
* @param array $args
*/
public function deleteAcl(array $args = [])
{
return $this->send($args['type'], 'delete', $args);
}
/**
* @param array $args
*/
public function getAcl(array $args = [])
{
return $this->send($args['type'], 'get', $args);
}
/**
* @param array $args
*/
public function listAcl(array $args = [])
{
return $this->send($args['type'], 'list', $args);
}
/**
* @param array $args
*/
public function insertAcl(array $args = [])
{
return $this->send($args['type'], 'insert', $args);
}
/**
* @param array $args
*/
public function patchAcl(array $args = [])
{
return $this->send($args['type'], 'patch', $args);
}
/**
* @param array $args
*/
public function deleteBucket(array $args = [])
{
return $this->send('buckets', 'delete', $args);
}
/**
* @param array $args
*/
public function restoreBucket(array $args = [])
{
return $this->send('buckets', 'restore', $args);
}
/**
* @param array $args
*/
public function getBucket(array $args = [])
{
return $this->send('buckets', 'get', $args);
}
/**
* @param array $args
*/
public function listBuckets(array $args = [])
{
return $this->send('buckets', 'list', $args);
}
/**
* @param array $args
*/
public function insertBucket(array $args = [])
{
return $this->send('buckets', 'insert', $args);
}
/**
* @param array $args
*/
public function patchBucket(array $args = [])
{
return $this->send('buckets', 'patch', $args);
}
/**
* @param array $args
*/
public function deleteObject(array $args = [])
{
return $this->send('objects', 'delete', $args);
}
/**
* @param array $args
*/
public function restoreObject(array $args = [])
{
return $this->send('objects', 'restore', $args);
}
/**
* @param array $args
*/
public function copyObject(array $args = [])
{
return $this->send('objects', 'copy', $args);
}
/**
* @param array $args
*/
public function rewriteObject(array $args = [])
{
return $this->send('objects', 'rewrite', $args);
}
/**
* @param array $args
*/
public function moveObject(array $args = [])
{
return $this->send('objects', 'move', $args);
}
/**
* @param array $args
*/
public function composeObject(array $args = [])
{
return $this->send('objects', 'compose', $args);
}
/**
* @param array $args
*/
public function getObject(array $args = [])
{
return $this->send('objects', 'get', $args);
}
/**
* @param array $args
*/
public function listObjects(array $args = [])
{
return $this->send('objects', 'list', $args);
}
/**
* @param array $args
*/
public function patchObject(array $args = [])
{
return $this->send('objects', 'patch', $args);
}
/**
* @param array $args
*/
public function downloadObject(array $args = [])
{
// This makes sure we honour the range headers specified by the user
$requestedBytes = $this->getRequestedBytes($args);
$resultStream = Utils::streamFor(null);
$transcodedObj = false;
$args['retryStrategy'] ??= $this->retryStrategy;
list($request, $requestOptions) = $this->buildDownloadObjectParams($args);
$invocationId = Uuid::uuid4()->toString();
$requestOptions['retryHeaders'] = self::getRetryHeaders($invocationId, 1);
$requestOptions['restRetryFunction'] = $this->getRestRetryFunction('objects', 'get', $args);
// We try to deduce if the object is a transcoded object when we receive the headers.
$requestOptions['restOptions']['on_headers'] = function ($response) use (&$transcodedObj) {
$header = $response->getHeader(self::TRANSCODED_OBJ_HEADER_KEY);
if (is_array($header) && in_array(self::TRANSCODED_OBJ_HEADER_VAL, $header)) {
$transcodedObj = true;
}
};
$attempt = null;
$requestOptions['restRetryListener'] = function (
\Exception $e,
$retryAttempt,
&$arguments
) use (
$resultStream,
$requestedBytes,
$invocationId,
&$attempt,
) {
// if the exception has a response for us to use
if ($e instanceof RequestException
&& $e->hasResponse()
&& $e->getResponse()->getStatusCode() >= 200
&& $e->getResponse()->getStatusCode() < 300
) {
$msg = (string) $e->getResponse()->getBody();
$fetchedStream = Utils::streamFor($msg);
// add the partial response to our stream that we will return
Utils::copyToStream($fetchedStream, $resultStream);
// Start from the byte that was last fetched
$startByte = intval($requestedBytes['startByte']) + $resultStream->getSize();
$endByte = $requestedBytes['endByte'];
// modify the range headers to fetch the remaining data
$arguments[1]['headers']['Range'] = sprintf('bytes=%s-%s', $startByte, $endByte);
$arguments[0] = $this->modifyRequestForRetry($arguments[0], $retryAttempt, $invocationId);
// Copy the final result to the end of the stream
$attempt = $retryAttempt;
}
};
$fetchedStream = $this->requestWrapper->send(
$request,
$requestOptions
)->getBody();
// If no retry attempt was made, then we can return the stream as is.
// This is important in the case where downloadObject is called to open
// the file but not to read from it yet.
if ($attempt === null) {
return $fetchedStream;
}
// If our object is a transcoded object, then Range headers are not honoured.
// That means even if we had a partial download available, the final obj
// that was fetched will contain the complete object. So, we don't need to copy
// the partial stream, we can just return the stream we fetched.
if ($transcodedObj) {
return $fetchedStream;
}
Utils::copyToStream($fetchedStream, $resultStream);
$resultStream->seek(0);
return $resultStream;
}
/**
* @param array $args
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
public function downloadObjectAsync(array $args = [])
{
list($request, $requestOptions) = $this->buildDownloadObjectParams($args);
return $this->requestWrapper->sendAsync(
$request,
$requestOptions
)->then(function (ResponseInterface $response) {
return $response->getBody();
});
}
/**
* @param array $args
*/
public function insertObject(array $args = [])
{
$args = $this->resolveUploadOptions($args);
$uploadType = AbstractUploader::UPLOAD_TYPE_RESUMABLE;
if ($args['streamable']) {
$uploaderClass = StreamableUploader::class;
} elseif ($args['resumable']) {
$uploaderClass = ResumableUploader::class;
} else {
$uploaderClass = MultipartUploader::class;
$uploadType = AbstractUploader::UPLOAD_TYPE_MULTIPART;
}
$uriParams = [
'bucket' => $args['bucket'],
'query' => [
'predefinedAcl' => $args['predefinedAcl'],
'uploadType' => $uploadType,
'userProject' => $args['userProject']
]
];
// Passing the preconditions we want to extract out of arguments
// into our query params.
$preconditions = self::$condIdempotentOps['objects.insert'];
foreach ($preconditions as $precondition) {
if (isset($args[$precondition])) {
$uriParams['query'][$precondition] = $args[$precondition];
}
}
return new $uploaderClass(
$this->requestWrapper,
$args['data'],
$this->expandUri($this->apiEndpoint . self::UPLOAD_PATH, $uriParams),
$args['uploaderOptions']
);
}
/**
* @param array $args
*/
private function resolveUploadOptions(array $args)
{
$args += [
'bucket' => null,
'name' => null,
'validate' => true,
'resumable' => null,
'streamable' => null,
'predefinedAcl' => null,
'metadata' => [],
'userProject' => null,
];
$args['retryStrategy'] ??= $this->retryStrategy;
$args['data'] = Utils::streamFor($args['data']);
if ($args['resumable'] === null) {
$args['resumable'] = $args['data']->getSize() > AbstractUploader::RESUMABLE_LIMIT;
}
if (!$args['name']) {
$args['name'] = basename($args['data']->getMetadata('uri'));
}
$validate = $this->chooseValidationMethod($args);
if ($validate === 'md5') {
$args['metadata']['md5Hash'] = base64_encode(Utils::hash($args['data'], 'md5', true));
} elseif ($validate === 'crc32') {
$args['metadata']['crc32c'] = $this->crcFromStream($args['data']);
}
$args['metadata']['name'] = $args['name'];
if (isset($args['retention'])) {
// during object creation retention properties go into metadata
// but not into request body
$args['metadata']['retention'] = $args['retention'];
unset($args['retention']);
}
unset($args['name']);
$args['contentType'] = $args['metadata']['contentType']
?? MimeType::fromFilename($args['metadata']['name']);
$uploaderOptionKeys = [
'restOptions',
'retries',
'requestTimeout',
'chunkSize',
'contentType',
'metadata',
'uploadProgressCallback',
'restDelayFunction',
'restCalcDelayFunction',
];
$args['uploaderOptions'] = array_intersect_key($args, array_flip($uploaderOptionKeys));
$args = array_diff_key($args, array_flip($uploaderOptionKeys));
// Passing on custom retry function to $args['uploaderOptions']
$retryFunc = $this->getRestRetryFunction(
'objects',
'insert',
$args
);
$args['uploaderOptions']['restRetryFunction'] = $retryFunc;
$args['uploaderOptions'] = $this->addRetryHeaderLogic(
$args['uploaderOptions']
);
return $args;
}
/**
* @param array $args
*/
public function getBucketIamPolicy(array $args)
{
return $this->send('buckets', 'getIamPolicy', $args);
}
/**
* @param array $args
*/
public function setBucketIamPolicy(array $args)
{
return $this->send('buckets', 'setIamPolicy', $args);
}
/**
* @param array $args
*/
public function testBucketIamPermissions(array $args)
{
return $this->send('buckets', 'testIamPermissions', $args);
}
/**
* @param array $args
*/
public function getNotification(array $args = [])
{
return $this->send('notifications', 'get', $args);
}
/**
* @param array $args
*/
public function deleteNotification(array $args = [])
{
return $this->send('notifications', 'delete', $args);
}
/**
* @param array $args
*/
public function insertNotification(array $args = [])
{
return $this->send('notifications', 'insert', $args);
}
/**
* @param array $args
*/
public function listNotifications(array $args = [])
{
return $this->send('notifications', 'list', $args);
}
/**
* @param array $args
*/
public function getServiceAccount(array $args = [])
{
return $this->send('projects.resources.serviceAccount', 'get', $args);
}
/**
* @param array $args
*/
public function lockRetentionPolicy(array $args = [])
{
return $this->send('buckets', 'lockRetentionPolicy', $args);
}
/**
* @param array $args
*/
public function createHmacKey(array $args = [])
{
return $this->send('projects.resources.hmacKeys', 'create', $args);
}
/**
* @param array $args
*/
public function deleteHmacKey(array $args = [])
{
return $this->send('projects.resources.hmacKeys', 'delete', $args);
}
/**
* @param array $args
*/
public function getHmacKey(array $args = [])
{
return $this->send('projects.resources.hmacKeys', 'get', $args);
}
/**
* @param array $args
*/
public function updateHmacKey(array $args = [])
{
return $this->send('projects.resources.hmacKeys', 'update', $args);
}
/**
* @param array $args
*/
public function listHmacKeys(array $args = [])
{
return $this->send('projects.resources.hmacKeys', 'list', $args);
}
/**
* @param array $args
* @return array
*/
private function buildDownloadObjectParams(array $args)
{
$args += [
'bucket' => null,
'object' => null,
'generation' => null,
'userProject' => null
];
$requestOptions = array_intersect_key($args, [
'restOptions' => null,
'retries' => null,
'restRetryFunction' => null,
'restCalcDelayFunction' => null,
'restDelayFunction' => null
]);
$queryOptions = [
'generation' => $args['generation'],
'alt' => 'media',
'userProject' => $args['userProject'],
];
if (isset($args['softDeleted'])) {
// alt param cannot be specified with softDeleted param. See:
// https://cloud.google.com/storage/docs/json_api/v1/objects/get
unset($args['alt']);
$queryOptions['softDeleted'] = $args['softDeleted'];
}
$uri = $this->expandUri($this->apiEndpoint . self::DOWNLOAD_PATH, [
'bucket' => $args['bucket'],
'object' => $args['object'],
'query' => $queryOptions,
]);
return [
new Request('GET', Utils::uriFor($uri)),
$requestOptions
];
}
/**
* Choose an upload validation method based on user input and platform
* requirements.
*
* @param array $args
* @return bool|string
*/
private function chooseValidationMethod(array $args)
{
// If the user provided a hash, skip hashing.
if (isset($args['metadata']['md5Hash']) || isset($args['metadata']['crc32c'])) {
return false;
}
$validate = $args['validate'];
if (in_array($validate, [false, 'crc32', 'md5'], true)) {
return $validate;
}
// not documented, but the feature is called crc32c, so let's accept that as input anyways.
if ($validate === 'crc32c') {
return 'crc32';
}
// is the extension loaded?
if ($this->crc32cExtensionLoaded()) {
return 'crc32';
}
// is crc32c available in `hash()`?
if ($this->supportsBuiltinCrc32c()) {
return 'crc32';
}
return 'md5';
}
/**
* Generate a CRC32c checksum from a stream.
*
* @param StreamInterface $data
* @return string
*/
private function crcFromStream(StreamInterface $data)
{
$pos = $data->tell();
$data->rewind();
$crc32c = hash_init('crc32c');
while (!$data->eof()) {
$buffer = $data->read(1048576);
hash_update($crc32c, $buffer);
}
$data->seek($pos);
$hash = hash_final($crc32c, true);
return base64_encode($hash);
}
/**
* Check if the crc32c extension is available.
*
* Protected access for unit testing.
*
* @return bool
*/
protected function crc32cExtensionLoaded()
{
return extension_loaded('crc32c');
}
/**
* Check if hash() supports crc32c.
*
* @return bool
* @deprecated
*/
protected function supportsBuiltinCrc32c()
{
return extension_loaded('hash') && in_array('crc32c', hash_algos());
}
/**
* Add the required retry function and send the request.
*
* @param string $resource resource name, eg: buckets.
* @param string $method method name, eg: get
* @param array $options [optional] Options used to build out the request.
* @param array $whitelisted [optional]
*/
public function send($resource, $method, array $options = [], $whitelisted = false)
{
$retryMap = [
'projects.resources.serviceAccount' => 'serviceaccount',
'projects.resources.hmacKeys' => 'hmacKey',
'bucketAccessControls' => 'bucket_acl',
'defaultObjectAccessControls' => 'default_object_acl',
'objectAccessControls' => 'object_acl'
];
$retryResource = isset($retryMap[$resource]) ? $retryMap[$resource] : $resource;
$options['restRetryFunction'] = $this->restRetryFunction ?? $this->getRestRetryFunction(
$retryResource,
$method,
$options
);
$options += array_filter([
'retryStrategy' => $this->retryStrategy,
'restDelayFunction' => $this->restDelayFunction,
'restCalcDelayFunction' => $this->restCalcDelayFunction,
'restRetryListener' => $this->restRetryListener,
]);
$options = $this->addRetryHeaderLogic($options);
return $this->traitSend($resource, $method, $options);
}
/**
* Adds the retry headers to $args which amends retry hash and attempt
* count to the required header.
* @param array $args
* @return array
*/
private function addRetryHeaderLogic(array $args)
{
$invocationId = Uuid::uuid4()->toString();
$args['retryHeaders'] = self::getRetryHeaders($invocationId, 1);
$userListener = $args['restRetryListener'] ?? null;
// Adding callback logic to update headers while retrying
$args['restRetryListener'] = function (
\Exception $e,
$retryAttempt,
&$arguments
) use (
$invocationId,
$userListener
) {
$arguments[0] = $this->modifyRequestForRetry(
$arguments[0],
$retryAttempt,
$invocationId
);
if ($userListener) {
$userListener($e, $retryAttempt, $arguments);
}
};
return $args;
}
private function modifyRequestForRetry(
RequestInterface $request,
int $retryAttempt,
string $invocationId
) {
$changes = self::getRetryHeaders($invocationId, $retryAttempt + 1);
$headerLine = $request->getHeaderLine(Retry::RETRY_HEADER_KEY);
// An associative array to contain final header values as
// $headerValueKey => $headerValue
$headerElements = [];
// Adding existing values
$headerLineValues = explode(' ', $headerLine);
foreach ($headerLineValues as $value) {
$key = explode('/', $value)[0];
$headerElements[$key] = $value;
}
// Adding changes with replacing value if $key already present
foreach ($changes as $change) {
$key = explode('/', $change)[0];
$headerElements[$key] = $change;
}
return $request->withHeader(
Retry::RETRY_HEADER_KEY,
implode(' ', $headerElements)
);
}
/**
* Util function to compute the bytes requested for a download request.
*
* @param array $options Request options
* @return array
*/
private function getRequestedBytes(array $options)
{
$startByte = 0;
$endByte = '';
if (isset($options['restOptions']) && isset($options['restOptions']['headers'])) {
$headers = $options['restOptions']['headers'];
if (isset($headers['Range']) || isset($headers['range'])) {
$header = isset($headers['Range']) ? $headers['Range'] : $headers['range'];
$range = explode('=', $header);
$bytes = explode('-', $range[1]);
$startByte = $bytes[0];
$endByte = $bytes[1];
}
}
return compact('startByte', 'endByte');
}
}

View File

@@ -0,0 +1,247 @@
<?php
/**
* Copyright 2022 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage\Connection;
use Google\Cloud\Storage\StorageClient;
/**
* Trait which provides helper methods for retry logic.
*
* @internal
*/
trait RetryTrait
{
/**
* The HTTP codes that will be retried by our custom retry function.
* @var array
*/
private static $httpRetryCodes = [
0, // connection-refused OR connection-reset gives status code of 0
200, // partial download cases
408,
429,
500,
502,
503,
504
];
/**
* The operations which can be retried without any conditions
* (Idempotent)
* @var array
*/
private static $idempotentOps = [
'bucket_acl.get',
'bucket_acl.list',
'buckets.delete',
'buckets.get',
'buckets.getIamPolicy',
'buckets.insert',
'buckets.list',
'buckets.lockRetentionPolicy',
'buckets.testIamPermissions',
'default_object_acl.get',
'default_object_acl.list',
'hmacKey.delete',
'hmacKey.get',
'hmacKey.list',
'notifications.delete',
'notifications.get',
'notifications.list',
'object_acl.get',
'object_acl.list',
'objects.get',
'objects.list',
'serviceaccount.get',
'signBlob.execute'
];
/**
* The operations which can be retried with specific conditions
* (Conditionally idempotent)
* @var array
*/
private static $condIdempotentOps = [
'buckets.patch' => ['ifMetagenerationMatch', 'etag'],
// Currently etag is not supported, so this preCondition never available
'buckets.setIamPolicy' => ['etag'],
'buckets.update' => ['ifMetagenerationMatch', 'etag'],
'hmacKey.update' => ['etag'],
'objects.compose' => ['ifGenerationMatch'],
'objects.copy' => ['ifGenerationMatch'],
'objects.delete' => ['ifGenerationMatch'],
'objects.insert' => ['ifGenerationMatch', 'ifGenerationNotMatch'],
'objects.patch' => ['ifMetagenerationMatch', 'etag'],
'objects.rewrite' => ['ifGenerationMatch'],
'objects.update' => ['ifMetagenerationMatch']
];
/**
* Retry strategies which enforce certain behaviour like:
* - Always retrying a call when an exception occurs(within the limits of 'max retries').
* - Never retrying a call when an exception occurs.
* - Retrying only when the operation is considered idempotent(default).
* These configurations are supplied for per api call basis.
*
*/
/**
* Header that identifies a specific request hash. The
* hash needs to stay the same for multiple retries.
*/
private static $INVOCATION_ID_HEADER = 'gccl-invocation-id';
/**
* Header that identifies the attempt count for a request. The
* value will increment by 1 with every retry.
*/
private static $ATTEMPT_COUNT_HEADER = 'gccl-attempt-count';
/**
* Return a retry decider function.
*
* @param string $resource resource name, eg: buckets.
* @param string $method method name, eg: get
* @param array $args
* @return callable
*/
private function getRestRetryFunction($resource, $method, array $args)
{
if (isset($args['restRetryFunction'])) {
return $args['restRetryFunction'];
}
$methodName = sprintf('%s.%s', $resource, $method);
$isOpIdempotent = in_array($methodName, self::$idempotentOps);
$preconditionNeeded = array_key_exists($methodName, self::$condIdempotentOps);
$preconditionSupplied = $this->isPreConditionSupplied($methodName, $args);
$retryStrategy = isset($args['retryStrategy']) ?
$args['retryStrategy'] :
StorageClient::RETRY_IDEMPOTENT;
return function (
\Exception $exception,
$currentAttempt = 0,
$maxRetries = null
) use (
$isOpIdempotent,
$preconditionNeeded,
$preconditionSupplied,
$retryStrategy
) {
return $this->retryDeciderFunction(
$exception,
$isOpIdempotent,
$preconditionNeeded,
$preconditionSupplied,
$retryStrategy,
$currentAttempt,
$maxRetries
);
};
}
/**
* This function returns true when the user given
* precondtions ($preConditions) has values that are present
* in the precondition map ($this->condIdempotentMap) for that method.
* eg: condIdempotentMap has entry 'objects.copy' => ['ifGenerationMatch'],
* if the user has given 'ifGenerationMatch' in the 'objects.copy' operation,
* it will be available in the $preConditions
* as an array ['ifGenerationMatch']. This makes the array_intersect
* function return a non empty result and this function returns true.
*
* @param string $methodName method name, eg: buckets.get.
* @param array $args arguments which include preconditions provided,
* eg: ['ifGenerationMatch' => 0].
* @return bool
*/
private function isPreConditionSupplied($methodName, array $args)
{
if (isset(self::$condIdempotentOps[$methodName])) {
// return true if required precondition are given.
return !empty(array_intersect(
self::$condIdempotentOps[$methodName],
array_keys($args)
));
}
return false;
}
/**
* Decide whether the op needs to be retried or not.
*
* @param \Exception $exception The exception object received
* while sending the request.
* @param int $currentAttempt Current retry attempt.
* @param bool $isIdempotent
* @param bool $preconditionNeeded
* @param bool $preconditionSupplied
* @param int|null $maxRetries The maximum number of retries allowed.
* Null for no limit.
* @return bool
*/
private function retryDeciderFunction(
\Exception $exception,
$isIdempotent,
$preconditionNeeded,
$preconditionSupplied,
$retryStrategy,
$currentAttempt = 0,
$maxRetries = null
) {
// If maxRetries is specified, ensure we don't exceed it
if ($maxRetries !== null && $currentAttempt >= $maxRetries) {
return false;
}
if ($retryStrategy == StorageClient::RETRY_NEVER) {
return false;
}
$statusCode = $exception->getCode();
// Retry if the exception status code matches
// with one of the retriable status code and
// the operation is either idempotent or conditionally
// idempotent with preconditions supplied.
if (in_array($statusCode, self::$httpRetryCodes)) {
if ($retryStrategy == StorageClient::RETRY_ALWAYS) {
return true;
} elseif ($isIdempotent) {
return true;
} elseif ($preconditionNeeded) {
return $preconditionSupplied;
}
}
return false;
}
/**
* Utility func that returns the list of headers that need to be
* attached to every request and its retries.
*/
private static function getRetryHeaders($invocationId, $attemptCount)
{
return [
sprintf('%s/%s', self::$INVOCATION_ID_HEADER, $invocationId),
sprintf('%s/%d', self::$ATTEMPT_COUNT_HEADER, $attemptCount)
];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
<?php
/**
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
/**
* Represents a newly created HMAC key. Provides access to the key metadata and
* secret.
*
* Example:
* ```
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
* $response = $storage->createHmacKey($serviceAccountEmail);
* ```
*/
class CreatedHmacKey
{
/**
* @var HmacKey
*/
private $hmacKey;
/**
* @var string
*/
private $secret;
/**
* @param HmacKey $hmacKey The HMAC Key object.
* @param string $secret The HMAC key secret.
*/
public function __construct(HmacKey $hmacKey, $secret)
{
$this->hmacKey = $hmacKey;
$this->secret = $secret;
}
/**
* Get the HMAC key object.
*
* Example:
* ```
* $key = $response->hmacKey();
* ```
*
* @return HmacKey
*/
public function hmacKey()
{
return $this->hmacKey;
}
/**
* Get the HMAC key secret.
*
* This value will never be returned from the API after first creation. Make
* sure to record it for later use immediately upon key creation.
*
* Example:
* ```
* $secret = $response->secret();
* ```
*
* @return string
*/
public function secret()
{
return $this->secret;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use phpseclib\Crypt\RSA as RSA2;
use phpseclib3\Crypt\RSA as RSA3;
/**
* Trait which provides helper methods for customer-supplied encryption.
*/
trait EncryptionTrait
{
/**
* @var array
*/
private $copySourceEncryptionHeaderNames = [
'algorithm' => 'x-goog-copy-source-encryption-algorithm',
'key' => 'x-goog-copy-source-encryption-key',
'keySHA256' => 'x-goog-copy-source-encryption-key-sha256'
];
/**
* @var array
*/
private $encryptionHeaderNames = [
'algorithm' => 'x-goog-encryption-algorithm',
'key' => 'x-goog-encryption-key',
'keySHA256' => 'x-goog-encryption-key-sha256'
];
/**
* Formats options for customer-supplied encryption headers.
*
* @param array $options
* @return array
* @access private
*/
public function formatEncryptionHeaders(array $options)
{
$encryptionHeaders = [];
$useCopySourceHeaders = $options['useCopySourceHeaders'] ?? false;
$key = $options['encryptionKey'] ?? null;
$keySHA256 = $options['encryptionKeySHA256'] ?? null;
$destinationKey = $options['destinationEncryptionKey'] ?? null;
$destinationKeySHA256 = $options['destinationEncryptionKeySHA256'] ?? null;
unset($options['useCopySourceHeaders']);
unset($options['encryptionKey']);
unset($options['encryptionKeySHA256']);
unset($options['destinationEncryptionKey']);
unset($options['destinationEncryptionKeySHA256']);
$encryptionHeaders = $this->buildHeaders($key, $keySHA256, $useCopySourceHeaders)
+ $this->buildHeaders($destinationKey, $destinationKeySHA256, false);
if (!empty($encryptionHeaders)) {
if (isset($options['restOptions']['headers'])) {
$options['restOptions']['headers'] += $encryptionHeaders;
} else {
$options['restOptions']['headers'] = $encryptionHeaders;
}
}
return $options;
}
/**
* Builds out customer-supplied encryption headers.
*
* @param string $key
* @param string $keySHA256
* @param bool $useCopySourceHeaders
* @return array
*/
private function buildHeaders($key, $keySHA256, $useCopySourceHeaders)
{
if ($key) {
$headerNames = $useCopySourceHeaders
? $this->copySourceEncryptionHeaderNames
: $this->encryptionHeaderNames;
if (!$keySHA256) {
$decodedKey = base64_decode($key);
$keySHA256 = base64_encode(hash('SHA256', $decodedKey, true));
}
return [
$headerNames['algorithm'] => 'AES256',
$headerNames['key'] => $key,
$headerNames['keySHA256'] => $keySHA256
];
}
return [];
}
/**
* Sign a string using a given private key.
*
* @deprecated Please use the {@see Google\Auth\SignBlobInterface::signBlob()}
* and implementations for signing strings.
* This method will be removed in a future release.
*
* @param string $privateKey The private key to use to sign the data.
* @param string $data The data to sign.
* @param bool $forceOpenssl If true, OpenSSL will be used regardless of
* whether phpseclib is available. **Defaults to** `false`.
* @return string The signature
*/
protected function signString($privateKey, $data, $forceOpenssl = false)
{
$signature = '';
if (class_exists(RSA3::class) && !$forceOpenssl) {
$rsa = RSA3::loadPrivateKey($privateKey);
$rsa = $rsa->withPadding(RSA3::SIGNATURE_PKCS1)
->withHash('sha256');
$signature = $rsa->sign($data);
} elseif (class_exists(RSA2::class) && !$forceOpenssl) {
$rsa = new RSA2();
$rsa->loadKey($privateKey);
$rsa->setSignatureMode(RSA2::SIGNATURE_PKCS1);
$rsa->setHash('sha256');
$signature = $rsa->sign($data);
} elseif (extension_loaded('openssl')) {
openssl_sign($data, $signature, $privateKey, 'sha256WithRSAEncryption');
} else {
// @codeCoverageIgnoreStart
throw new \RuntimeException('OpenSSL is not installed.');
}
// @codeCoverageIgnoreEnd
return $signature;
}
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Storage\Connection\ConnectionInterface;
/**
* Represents a Service Account HMAC key.
*
* Example:
* ```
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
* $hmacKey = $storage->hmacKey($accessId);
* ```
*/
class HmacKey
{
/**
* @var ConnectionInterface
* @internal
*/
private $connection;
/**
* @var string
*/
private $projectId;
/**
* @var string
*/
private $accessId;
/**
* @var array|null
*/
private $info;
/**
* @param ConnectionInterface $connection A connection to Cloud Storage.
* This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param string $projectId The current project ID.
* @param string $accessId The key identifier.
* @param array|null $info The key metadata.
*/
public function __construct(
ConnectionInterface $connection,
$projectId,
$accessId,
array $info = []
) {
$this->connection = $connection;
$this->projectId = $projectId;
$this->accessId = $accessId;
$this->info = $info;
}
/**
* Get the HMAC Key Access ID.
*
* Example:
* ```
* $accessId = $hmacKey->accessId();
* ```
*
* @return string
*/
public function accessId()
{
return $this->accessId;
}
/**
* Fetch the key metadata from Cloud Storage.
*
* Example:
* ```
* $keyMetadata = $hmacKey->reload();
* ```
*
* @param array $options {
* Configuration Options
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request. **NOTE**: This option is
* currently ignored by Cloud Storage.
* }
* @return array
*/
public function reload(array $options = [])
{
$this->info = $this->connection->getHmacKey([
'projectId' => $this->projectId,
'accessId' => $this->accessId
] + $options);
return $this->info;
}
/**
* Get the HMAC Key Metadata.
*
* If the metadata is not already available, it will be requested from Cloud
* Storage.
*
* Example:
* ```
* $keyMetadata = $hmacKey->info();
* ```
*
* @param array $options {
* Configuration Options
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request. **NOTE**: This option is
* currently ignored by Cloud Storage.
* }
* @return array
*/
public function info(array $options = [])
{
return $this->info ?: $this->reload($options);
}
/**
* Update the HMAC Key state.
*
* Example:
* ```
* $hmacKey->update('INACTIVE');
* ```
*
* @param string $state The key state. Either `ACTIVE` or `INACTIVE`.
* @param array $options {
* Configuration Options
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request. **NOTE**: This option is
* currently ignored by Cloud Storage.
* }
* @return array
*/
public function update($state, array $options = [])
{
$this->info = $this->connection->updateHmacKey([
'accessId' => $this->accessId,
'projectId' => $this->projectId,
'state' => $state
] + $options);
return $this->info;
}
/**
* Delete the HMAC Key.
*
* Key state must be set to `INACTIVE` prior to deletion. See
* {@see HmacKey::update()} for details.
*
* Example:
* ```
* $hmacKey->delete();
* ```
*
* @param array $options {
* Configuration Options
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request. **NOTE**: This option is
* currently ignored by Cloud Storage.
* }
* @return void
*/
public function delete(array $options = [])
{
$this->connection->deleteHmacKey([
'accessId' => $this->accessId,
'projectId' => $this->projectId,
] + $options);
}
}

View File

@@ -0,0 +1,477 @@
<?php
/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\Timestamp;
/**
* Object Lifecycle Management supports common use cases like setting a Time to
* Live (TTL) for objects, archiving older versions of objects, or "downgrading"
* storage classes of objects to help manage costs.
*
* This builder does not execute any network requests and is intended to be used
* in combination with either
* {@see StorageClient::createBucket()}
* or {@see Bucket::update()}.
*
* Example:
* ```
* // Access a builder preconfigured with rules already existing on a given
* // bucket.
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
* $bucket = $storage->bucket('my-bucket');
* $lifecycle = $bucket->currentLifecycle();
* ```
*
* ```
* // Or get a fresh builder by using the static factory method.
* use Google\Cloud\Storage\Bucket;
*
* $lifecycle = Bucket::lifecycle();
* ```
*
* @see https://cloud.google.com/storage/docs/lifecycle Object Lifecycle Management API Documentation
*/
class Lifecycle implements \ArrayAccess, \IteratorAggregate
{
/**
* @var array
*/
private $lifecycle;
/**
* @param array $lifecycle [optional] A lifecycle configuration. Please see
* [here](https://cloud.google.com/storage/docs/json_api/v1/buckets#lifecycle)
* for the expected structure.
*/
public function __construct(array $lifecycle = [])
{
$this->lifecycle = $lifecycle;
}
/**
* Adds an Object Lifecycle Delete Rule.
*
* Example:
* ```
* $lifecycle->addDeleteRule([
* 'age' => 50,
* 'isLive' => true
* ]);
* ```
*
* @param array $condition {
* The condition(s) where the rule will apply.
*
* @type int $age Age of an object (in days). This condition is
* satisfied when an object reaches the specified age.
* @type \DateTimeInterface|string $createdBefore This condition is
* satisfied when an object is created before midnight of the
* specified date in UTC. If a string is given, it must be a date
* in RFC 3339 format with only the date part (for instance,
* "2013-01-15").
* @type \DateTimeInterface|string $customTimeBefore This condition is
* satisfied when the custom time on an object is before this date
* in UTC. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $daysSinceCustomTime Number of days elapsed since the
* user-specified timestamp set on an object. The condition is
* satisfied if the days elapsed is at least this number. If no
* custom timestamp is specified on an object, the condition does
* not apply.
* @type int $daysSinceNoncurrentTime Number of days elapsed since the
* noncurrent timestamp of an object. The condition is satisfied
* if the days elapsed is at least this number. This condition is
* relevant only for versioned objects. The value of the field
* must be a nonnegative integer. If it's zero, the object version
* will become eligible for Lifecycle action as soon as it becomes
* noncurrent.
* @type bool $isLive Relevant only for versioned objects. If the value
* is `true`, this condition matches live objects; if the value is
* `false`, it matches archived objects.
* @type string[] $matchesStorageClass Objects having any of the storage
* classes specified by this condition will be matched. Values
* include `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`,
* `"ARCHIVE"`, `"COLDLINE"`, `"STANDARD"`, and
* `"DURABLE_REDUCED_AVAILABILITY"`.
* @type \DateTimeInterface|string $noncurrentTimeBefore This condition
* is satisfied when the noncurrent time on an object is before
* this timestamp. This condition is relevant only for versioned
* objects. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $numNewerVersions Relevant only for versioned objects. If
* the value is N, this condition is satisfied when there are at
* least N versions (including the live version) newer than this
* version of the object.
* @type string[] $matchesPrefix Objects having names which start with
* values specified by this condition will be matched.
* @type string[] $matchesSuffix Objects having names which end with
* values specified by this condition will be matched.
* }
* @return Lifecycle
*/
public function addDeleteRule(array $condition)
{
$this->lifecycle['rule'][] = [
'action' => [
'type' => 'Delete'
],
'condition' => $this->formatCondition($condition)
];
return $this;
}
/**
* Adds a AbortIncompleteMultipartUpload lifecycle rule to Object Lifecycle.
*
* Example:
* ```
* $lifecycle->addAbortIncompleteMultipartUploadRule([
* 'age' => 50,
* 'isLive' => true
* ]);
* ```
*
* @param array $condition {
* The condition(s) where the rule will apply.
*
* @type int $age Age of an object (in days). This condition is
* satisfied when an object reaches the specified age.
* @type \DateTimeInterface|string $createdBefore This condition is
* satisfied when an object is created before midnight of the
* specified date in UTC. If a string is given, it must be a date
* in RFC 3339 format with only the date part (for instance,
* "2013-01-15").
* @type \DateTimeInterface|string $customTimeBefore This condition is
* satisfied when the custom time on an object is before this date
* in UTC. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $daysSinceCustomTime Number of days elapsed since the
* user-specified timestamp set on an object. The condition is
* satisfied if the days elapsed is at least this number. If no
* custom timestamp is specified on an object, the condition does
* not apply.
* @type int $daysSinceNoncurrentTime Number of days elapsed since the
* noncurrent timestamp of an object. The condition is satisfied
* if the days elapsed is at least this number. This condition is
* relevant only for versioned objects. The value of the field
* must be a nonnegative integer. If it's zero, the object version
* will become eligible for Lifecycle action as soon as it becomes
* noncurrent.
* @type bool $isLive Relevant only for versioned objects. If the value
* is `true`, this condition matches live objects; if the value is
* `false`, it matches archived objects.
* @type string[] $matchesStorageClass Objects having any of the storage
* classes specified by this condition will be matched. Values
* include `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`,
* `"ARCHIVE"`, `"COLDLINE"`, `"STANDARD"`, and
* `"DURABLE_REDUCED_AVAILABILITY"`.
* @type \DateTimeInterface|string $noncurrentTimeBefore This condition
* is satisfied when the noncurrent time on an object is before
* this timestamp. This condition is relevant only for versioned
* objects. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $numNewerVersions Relevant only for versioned objects. If
* the value is N, this condition is satisfied when there are at
* least N versions (including the live version) newer than this
* version of the object.
* @type string[] $matchesPrefix Objects having names which start with
* values specified by this condition will be matched.
* @type string[] $matchesSuffix Objects having names which end with
* values specified by this condition will be matched.
* }
* @return Lifecycle
*/
public function addAbortIncompleteMultipartUploadRule(array $condition)
{
$this->lifecycle['rule'][] = [
'action' => [
'type' => 'AbortIncompleteMultipartUpload'
],
'condition' => $this->formatCondition($condition)
];
return $this;
}
/**
* Adds an Object Lifecycle Set Storage Class Rule.
*
* Example:
* ```
* $lifecycle->addSetStorageClassRule('COLDLINE', [
* 'age' => 50,
* 'isLive' => true
* ]);
* ```
*
* ```
* // Using customTimeBefore rule with an object's custom time setting.
* $lifecycle->addSetStorageClassRule('NEARLINE', [
* 'customTimeBefore' => (new \DateTime())->add(
* \DateInterval::createFromDateString('+10 days')
* )
* ]);
*
* $bucket->update(['lifecycle' => $lifecycle]);
*
* $object = $bucket->object($objectName);
* $object->update([
* 'metadata' => [
* 'customTime' => '2020-08-17'
* ]
* ]);
* ```
*
* @param string $storageClass The target storage class. Values include
* `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`,
* `"STANDARD"`, and `"DURABLE_REDUCED_AVAILABILITY"`.
* @param array $condition {
* The condition(s) where the rule will apply.
*
* @type int $age Age of an object (in days). This condition is
* satisfied when an object reaches the specified age.
* @type \DateTimeInterface|string $createdBefore This condition is
* satisfied when an object is created before midnight of the
* specified date in UTC. If a string is given, it must be a date
* in RFC 3339 format with only the date part (for instance,
* "2013-01-15").
* @type \DateTimeInterface|string $customTimeBefore This condition is
* satisfied when the custom time on an object is before this date
* in UTC. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $daysSinceCustomTime Number of days elapsed since the
* user-specified timestamp set on an object. The condition is
* satisfied if the days elapsed is at least this number. If no
* custom timestamp is specified on an object, the condition does
* not apply.
* @type int $daysSinceNoncurrentTime Number of days elapsed since the
* noncurrent timestamp of an object. The condition is satisfied
* if the days elapsed is at least this number. This condition is
* relevant only for versioned objects. The value of the field
* must be a nonnegative integer. If it's zero, the object version
* will become eligible for Lifecycle action as soon as it becomes
* noncurrent.
* @type bool $isLive Relevant only for versioned objects. If the value
* is `true`, this condition matches live objects; if the value is
* `false`, it matches archived objects.
* @type string[] $matchesStorageClass Objects having any of the storage
* classes specified by this condition will be matched. Values
* include `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`,
* `"ARCHIVE"`, `"COLDLINE"`, `"STANDARD"`, and
* `"DURABLE_REDUCED_AVAILABILITY"`.
* @type \DateTimeInterface|string $noncurrentTimeBefore This condition
* is satisfied when the noncurrent time on an object is before
* this timestamp. This condition is relevant only for versioned
* objects. If a string is given, it must be a date in RFC 3339
* format with only the date part (for instance, "2013-01-15").
* @type int $numNewerVersions Relevant only for versioned objects. If
* the value is N, this condition is satisfied when there are at
* least N versions (including the live version) newer than this
* version of the object.
* @type string[] $matchesPrefix Objects having names which start with
* values specified by this condition will be matched.
* @type string[] $matchesSuffix Objects having names which end with
* values specified by this condition will be matched.
* }
* @return Lifecycle
*/
public function addSetStorageClassRule($storageClass, array $condition)
{
$this->lifecycle['rule'][] = [
'action' => [
'type' => 'SetStorageClass',
'storageClass' => $storageClass
],
'condition' => $this->formatCondition($condition)
];
return $this;
}
/**
* Clear all Object Lifecycle rules or rules of a certain action type.
*
* Example:
* ```
* // Remove all rules.
* $lifecycle->clearRules();
* ```
*
* ```
* // Remove all "Delete" based rules.
* $lifecycle->clearRules('Delete');
* ```
*
* ```
* // Clear any rules which have an age equal to 50.
* $lifecycle->clearRules(function (array $rule) {
* return $rule['condition']['age'] === 50
* ? false
* : true;
* });
* ```
*
* @param string|callable $action [optional] If a string is provided, it
* must be the name of the type of rule to remove (`SetStorageClass`
* or `Delete`). All rules of this type will then be cleared. When
* providing a callable you may define a custom route for how you
* would like to remove rules. The provided callable will be run
* through
* [array_filter](http://php.net/manual/en/function.array-filter.php).
* The callable's argument will be a single lifecycle rule as an
* associative array. When returning true from the callable the rule
* will be preserved, and if false it will be removed.
* **Defaults to** `null`, clearing all assigned rules.
* @return Lifecycle
* @throws \InvalidArgumentException If a type other than a string or
* callabe is provided.
*/
public function clearRules($action = null)
{
if (!$action) {
$this->lifecycle = [];
return $this;
}
if (!is_string($action) && !is_callable($action)) {
throw new \InvalidArgumentException(
sprintf(
'Expected either a string or callable, instead got \'%s\'.',
gettype($action)
)
);
}
if (isset($this->lifecycle['rule'])) {
if (is_string($action)) {
$action = function ($rule) use ($action) {
return $rule['action']['type'] !== $action;
};
}
$this->lifecycle['rule'] = array_filter(
$this->lifecycle['rule'],
$action
);
if (!$this->lifecycle['rule']) {
$this->lifecycle = [];
}
}
return $this;
}
/**
* @access private
* @return \Generator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
if (!isset($this->lifecycle['rule'])) {
return;
}
foreach ($this->lifecycle['rule'] as $rule) {
yield $rule;
}
}
/**
* @access private
* @return array
*/
public function toArray()
{
return $this->lifecycle;
}
/**
* @access private
* @param string $offset
* @param mixed $value
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
$this->lifecycle['rule'][$offset] = $value;
}
/**
* @access private
* @param string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return isset($this->lifecycle['rule'][$offset]);
}
/**
* @access private
* @param string $offset
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->lifecycle['rule'][$offset]);
}
/**
* @access private
* @param string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return isset($this->lifecycle['rule'][$offset])
? $this->lifecycle['rule'][$offset]
: null;
}
/**
* Apply condition-specific formatting rules (such as date formatting) to
* conditions.
*
* @param array $condition
* @return array
*/
private function formatCondition(array $condition)
{
$rfc339DateFields = [
'createdBefore',
'customTimeBefore',
'noncurrentTimeBefore'
];
foreach ($rfc339DateFields as $field) {
if (isset($condition[$field]) && $condition[$field] instanceof \DateTimeInterface) {
$condition[$field] = $condition[$field]->format('Y-m-d');
}
}
return $condition;
}
}

View File

@@ -0,0 +1,208 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Storage\Connection\ConnectionInterface;
/**
* Cloud Pub/Sub Notifications sends information about changes to objects in
* your buckets to Google Cloud Pub/Sub, where the information is added to a
* Cloud Pub/Sub topic of your choice in the form of messages. For example,
* you can track objects that are created and deleted in your bucket. Each
* notification contains information describing both the event that triggered it
* and the object that changed.
*
* To utilize this class and see more examples, please see the relevant
* notifications based methods exposed on {@see Bucket}.
*
* Example:
* ```
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
*
* // Fetch an existing notification by ID.
* $bucket = $storage->bucket('my-bucket');
* $notification = $bucket->notification('2482');
* ```
*
* @see https://cloud.google.com/storage/docs/pubsub-notifications
* @experimental The experimental flag means that while we believe this method
* or class is ready for use, it may change before release in backwards-
* incompatible ways. Please use with caution, and test thoroughly when
* upgrading.
*/
class Notification
{
use ArrayTrait;
/**
* @var ConnectionInterface Represents a connection to Cloud Storage.
* @internal
*/
private $connection;
/**
* @var array The notification's identity.
*/
private $identity;
/**
* @var array The notification's metadata.
*/
private $info;
/**
* @param ConnectionInterface $connection Represents a connection to Cloud
* Storage. This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param string $id The notification's ID.
* @param string $bucket The name of the bucket associated with this
* notification.
* @param array $info [optional] The notification's metadata.
*/
public function __construct(ConnectionInterface $connection, $id, $bucket, array $info = [])
{
$this->connection = $connection;
$this->identity = [
'bucket' => $bucket,
'notification' => $id,
'userProject' => $this->pluck('requesterProjectId', $info, false)
];
$this->info = $info;
}
/**
* Check whether or not the notification exists.
*
* Example:
* ```
* if ($notification->exists()) {
* echo 'Notification exists!';
* }
* ```
* @param array $options [optional] {
* Configuration options.
* }
* @return bool
*/
public function exists(array $options = [])
{
try {
$this->connection->getNotification($options + $this->identity + ['fields' => 'id']);
} catch (NotFoundException $ex) {
return false;
}
return true;
}
/**
* Delete the notification.
*
* Example:
* ```
* $notification->delete();
* ```
*
* @codingStandardsIgnoreStart
* @see https://cloud.google.com/storage/docs/json_api/v1/notifications/delete Notifications delete API documentation.
* @codingStandardsIgnoreEnd
*
* @param array $options [optional]
* @return void
*/
public function delete(array $options = [])
{
$this->connection->deleteNotification($options + $this->identity);
}
/**
* Retrieves the notification's details. If no notification data is cached a
* network request will be made to retrieve it.
*
* Example:
* ```
* $info = $notification->info();
* echo $info['topic'];
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/notifications/get Notifications get API documentation.
*
* @param array $options [optional]
* @return array
*/
public function info(array $options = [])
{
return $this->info ?: $this->reload($options);
}
/**
* Triggers a network request to reload the notification's details.
*
* Example:
* ```
* $notification->reload();
* $info = $notification->info();
* echo $info['topic'];
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/notifications/get Notifications get API documentation.
*
* @param array $options [optional]
* @return array
*/
public function reload(array $options = [])
{
return $this->info = $this->connection->getNotification(
$options + $this->identity
);
}
/**
* Retrieves the notification's ID.
*
* Example:
* ```
* echo $notification->id();
* ```
*
* @return string
*/
public function id()
{
return $this->identity['notification'];
}
/**
* Retrieves the notification's identity.
*
* Example:
* ```
* echo $notification->identity()['bucket'];
* ```
*
* @return array
*/
public function identity()
{
return $this->identity;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\Iterator\ItemIteratorTrait;
/**
* ObjectIterator
*
* Iterates over a set of {@see StorageObject} items.
*
* @implements \Iterator<int, StorageObject>
*/
class ObjectIterator implements \Iterator
{
use ItemIteratorTrait;
/**
* Gets a list of prefixes of objects matching-but-not-listed up to and
* including the requested delimiter.
*
* @return array
*/
public function prefixes()
{
return method_exists($this->pageIterator, 'prefixes')
? $this->pageIterator->prefixes()
: [];
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\Iterator\PageIteratorTrait;
/**
* ObjectPageIterator
*
* Iterates over a set of pages containing
* {@see StorageObject} items.
*/
class ObjectPageIterator implements \Iterator
{
use PageIteratorTrait;
/**
* @var array
*/
private $prefixes = [];
/**
* Gets a list of prefixes of objects matching-but-not-listed up to and
* including the requested delimiter.
*
* @return array
*/
public function prefixes()
{
return $this->prefixes;
}
/**
* Get the current page.
*
* @return array|null
*/
#[\ReturnTypeWillChange]
public function current()
{
if (!$this->page) {
$this->page = $this->executeCall();
}
if (isset($this->page['prefixes'])) {
$this->updatePrefixes();
}
return $this->get($this->itemsPath, $this->page);
}
/**
* Add new prefixes to the list.
*
* @return void
*/
private function updatePrefixes()
{
foreach ($this->page['prefixes'] as $prefix) {
if (!in_array($prefix, $this->prefixes)) {
$this->prefixes[] = $prefix;
}
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* A Stream implementation that wraps a GuzzleHttp download stream to
* provide `getSize()` from the response headers.
*/
class ReadStream implements StreamInterface
{
use StreamDecoratorTrait;
private $stream;
/**
* Create a new ReadStream.
*
* @param StreamInterface $stream The stream interface to wrap
*/
public function __construct(StreamInterface $stream)
{
$this->stream = $stream;
}
/**
* Return the full size of the buffer. If the underlying stream does
* not report it's size, try to fetch the size from the Content-Length
* response header.
*
* @return int The size of the stream.
*/
public function getSize(): ?int
{
return $this->stream->getSize() ?: $this->getSizeFromMetadata();
}
/**
* Attempt to fetch the size from the Content-Length response header.
* If we cannot, return 0.
*
* @return int The Size of the stream
*/
private function getSizeFromMetadata(): int
{
foreach ($this->stream->getMetadata('wrapper_data') as $value) {
if (substr($value, 0, 15) == 'Content-Length:') {
return (int) substr($value, 16);
}
}
return 0;
}
/**
* Read bytes from the underlying buffer, retrying until we have read
* enough bytes or we cannot read any more. We do this because the
* internal C code for filling a buffer does not account for when
* we try to read large chunks from a user-land stream that does not
* return enough bytes.
*
* @param int $length The number of bytes to read.
* @return string Read bytes from the underlying stream.
*/
public function read($length): string
{
$data = '';
do {
$moreData = $this->stream->read($length);
$data .= $moreData;
$readLength = strlen($moreData);
$length -= $readLength;
} while ($length > 0 && $readLength > 0);
return $data;
}
}

View File

@@ -0,0 +1,925 @@
<?php
/**
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Auth\CredentialsLoader;
use Google\Auth\SignBlobInterface;
use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Core\Exception\ServiceException;
use Google\Cloud\Core\JsonTrait;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\Connection\RetryTrait;
/**
* Provides common methods for signing storage URLs.
*
* @internal
*/
class SigningHelper
{
use ArrayTrait;
use JsonTrait;
use RetryTrait;
const DEFAULT_URL_SIGNING_VERSION = 'v2';
const DEFAULT_DOWNLOAD_HOST = 'storage.googleapis.com';
const V4_ALGO_NAME = 'GOOG4-RSA-SHA256';
const V4_TIMESTAMP_FORMAT = 'Ymd\THis\Z';
const V4_DATESTAMP_FORMAT = 'Ymd';
const MAX_RETRIES = 5;
/**
* Create or fetch a SigningHelper instance.
*
* @return SigningHelper
*/
public static function getHelper()
{
static $helper;
if (!$helper) {
$helper = new static();
}
return $helper;
}
/**
* Sign using the version inferred from `$options.version`.
*
* @param ConnectionInterface $connection A connection to the Cloud Storage
* API. This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param Timestamp|\DateTimeInterface|int $expires The signed URL
* expiration.
* @param string $resource The URI to the storage resource, preceded by a
* leading slash.
* @param int|null $generation The resource generation.
* @param array $options Configuration options. See
* {@see StorageObject::signedUrl()} for
* details.
* @return string
* @throws \InvalidArgumentException
* @throws \RuntimeException If required data could not be gathered from
* credentials.
* @throws \RuntimeException If OpenSSL signing is required by user input
* and OpenSSL is not available.
*/
public function sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
{
$version = $options['version'] ?? self::DEFAULT_URL_SIGNING_VERSION;
unset($options['version']);
switch (strtolower($version)) {
case 'v2':
$method = 'v2Sign';
break;
case 'v4':
$method = 'v4Sign';
break;
default:
throw new \InvalidArgumentException('Invalid signing version.');
}
return call_user_func_array([$this, $method], [
$connection,
$expires,
$resource,
$generation,
$options
]);
}
/**
* Sign a URL using Google Signed URLs v2.
*
* This method will be deprecated in the future.
*
* @param ConnectionInterface $connection A connection to the Cloud Storage
* API. This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param Timestamp|\DateTimeInterface|int $expires The signed URL
* expiration.
* @param string $resource The URI to the storage resource, preceded by a
* leading slash.
* @param int|null $generation The resource generation.
* @param array $options Configuration options. See
* {@see StorageObject::signedUrl()} for
* details.
* @return string
* @throws \InvalidArgumentException
* @throws \RuntimeException If required data could not be gathered from
* credentials.
* @throws \RuntimeException If OpenSSL signing is required by user input
* and OpenSSL is not available.
*/
public function v2Sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
{
list($credentials, $options) = $this->getSigningCredentials($connection, $options);
$expires = $this->normalizeExpiration($expires);
list($resource, $bucket) = $this->normalizeResource($resource);
$options = $this->normalizeOptions($options);
$headers = $this->normalizeHeaders($options['headers']);
if ($options['virtualHostedStyle']) {
$options['bucketBoundHostname'] = sprintf(
'%s.storage.googleapis.com',
$bucket
);
}
// Make sure disallowed headers are not included.
$illegalHeaders = [
'x-goog-encryption-key',
'x-goog-encryption-key-sha256'
];
if ($illegal = array_intersect_key(array_flip($illegalHeaders), $headers)) {
throw new \InvalidArgumentException(sprintf(
'%s %s not allowed in Signed URL headers.',
implode(' and ', array_keys($illegal)),
count($illegal) === 1 ? 'is' : 'are'
));
}
// Sort headers by name.
ksort($headers);
$toSign = [
$options['method'],
$options['contentMd5'],
$options['contentType'],
$expires,
];
$signedHeaders = [];
foreach ($headers as $name => $value) {
$signedHeaders[] = $name . ':' . $value;
}
// Push the headers onto the end of the signing string.
if ($signedHeaders) {
$toSign = array_merge($toSign, $signedHeaders);
}
$toSign[] = $resource;
$stringToSign = $this->createV2CanonicalRequest($toSign);
// Use exponential backOff
$signature = $this->retrySignBlob(fn () => $credentials->signBlob($stringToSign, [
'forceOpenssl' => $options['forceOpenssl']
]));
// Start with user-provided query params and add required parameters.
$params = $options['queryParams'];
$params['GoogleAccessId'] = $credentials->getClientName();
$params['Expires'] = $expires;
$params['Signature'] = $signature;
// urlencode parameter values
foreach ($params as &$value) {
$value = rawurlencode($value ?? '');
}
$params = $this->addCommonParams($generation, $params, $options);
$queryString = $this->buildQueryString($params);
$resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
return 'https://' . $options['bucketBoundHostname'] . $resource . '?' . $queryString;
}
/**
* Sign a storage URL using Google Signed URLs v4.
*
* @param ConnectionInterface $connection A connection to the Cloud Storage
* API. This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param Timestamp|\DateTimeInterface|int $expires The signed URL
* expiration.
* @param string $resource The URI to the storage resource, preceded by a
* leading slash.
* @param int|null $generation The resource generation.
* @param array $options Configuration options. See
* {@see StorageObject::signedUrl()} for
* details.
* @return string
* @throws \InvalidArgumentException
* @throws \RuntimeException If required data could not be gathered from
* credentials.
* @throws \RuntimeException If OpenSSL signing is required by user input
* and OpenSSL is not available.
*/
public function v4Sign(ConnectionInterface $connection, $expires, $resource, $generation, array $options)
{
list($credentials, $options) = $this->getSigningCredentials($connection, $options);
$expires = $this->normalizeExpiration($expires);
list($resource, $bucket) = $this->normalizeResource($resource);
$options = $this->normalizeOptions($options);
$time = $options['timestamp'];
$requestTimestamp = $time->format(self::V4_TIMESTAMP_FORMAT);
$requestDatestamp = $time->format(self::V4_DATESTAMP_FORMAT);
$timeSeconds = $time->format('U');
$expireLimit = $timeSeconds + 604800;
if ($expires > $expireLimit) {
throw new \InvalidArgumentException(
'V4 Signed URLs may not have an expiration greater than seven days in the future.'
);
}
$clientEmail = $credentials->getClientName();
$credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
$credential = sprintf('%s/%s', $clientEmail, $credentialScope);
if ($options['virtualHostedStyle']) {
$options['bucketBoundHostname'] = sprintf(
'%s.storage.googleapis.com',
$bucket
);
}
// Add headers and query params based on provided options.
$params = $options['queryParams'];
$headers = $options['headers'] + [
'host' => $options['bucketBoundHostname']
];
if ($options['contentType']) {
$headers['content-type'] = $options['contentType'];
}
if ($options['contentMd5']) {
$headers['content-md5'] = $options['contentMd5'];
}
$params = $this->addCommonParams($generation, $params, $options);
$headers = $this->normalizeHeaders($headers);
// sort headers by name
ksort($headers, SORT_NATURAL | SORT_FLAG_CASE);
// Canonical headers are a list, newline separated, of keys and values,
// comma separated.
// Signed headers are a list of keys, separated by a semicolon.
$canonicalHeaders = [];
$signedHeaders = [];
foreach ($headers as $key => $val) {
$canonicalHeaders[] = sprintf('%s:%s', $key, $val);
$signedHeaders[] = $key;
}
$canonicalHeaders = implode("\n", $canonicalHeaders) . "\n";
$signedHeaders = implode(';', $signedHeaders);
// Add required query parameters.
$params = [
'X-Goog-Algorithm' => self::V4_ALGO_NAME,
'X-Goog-Credential' => $credential,
'X-Goog-Date' => $requestTimestamp,
'X-Goog-Expires' => $expires - $timeSeconds,
'X-Goog-SignedHeaders' => $signedHeaders,
] + $params;
$paramNames = [];
foreach ($params as $key => $val) {
$paramNames[] = $key;
}
sort($paramNames, SORT_REGULAR);
$sortedParams = [];
foreach ($paramNames as $name) {
$sortedParams[rawurlencode($name)] = rawurlencode($params[$name]);
}
$canonicalQueryString = $this->buildQueryString($sortedParams);
$canonicalResource = $this->normalizeCanonicalRequestResource(
$resource,
$options['bucketBoundHostname'],
$options['virtualHostedStyle']
);
$canonicalRequest = [
$options['method'],
$canonicalResource,
$canonicalQueryString,
$canonicalHeaders,
$signedHeaders,
$this->getPayloadHash($headers)
];
$requestHash = $this->createV4CanonicalRequest($canonicalRequest);
// Construct the string to sign.
$stringToSign = implode("\n", [
self::V4_ALGO_NAME,
$requestTimestamp,
$credentialScope,
$requestHash
]);
$signature = bin2hex(base64_decode($this->retrySignBlob(
fn () => $credentials->signBlob($stringToSign, [
'forceOpenssl' => $options['forceOpenssl']
])
) ?? ''));
// Construct the modified resource name. If a custom hostname is provided,
// this will remove the bucket name from the resource.
$resource = $this->normalizeUriPath($options['bucketBoundHostname'], $resource);
$scheme = $this->chooseScheme(
$options['scheme'],
$options['bucketBoundHostname'],
$options['virtualHostedStyle']
);
return sprintf(
'%s://%s%s?%s&X-Goog-Signature=%s',
$scheme,
$options['bucketBoundHostname'],
$resource,
$canonicalQueryString,
$signature
);
}
/**
* Create an HTTP POST policy using v4 signing.
*
* @param ConnectionInterface $connection A Connection to Google Cloud Storage.
* This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param Timestamp|\DateTimeInterface|int $expires The signed URL
* expiration.
* @param string $resource The URI to the storage resource, preceded by a
* leading slash.
* @param array $options Configuration options. See
* {@see Bucket::generateSignedPostPolicyV4()} for details.
* @return array An associative array, containing (string) `uri` and
* (array) `fields` keys.
*/
public function v4PostPolicy(
ConnectionInterface $connection,
$expires,
$resource,
array $options = []
) {
list($credentials, $options) = $this->getSigningCredentials($connection, $options);
$expires = $this->normalizeExpiration($expires);
list($resource, $bucket, $object) = $this->normalizeResource($resource, false);
$object = trim($object, '/');
$options = $this->normalizeOptions($options) + [
'fields' => [],
'conditions' => [],
'successActionRedirect' => null,
'successActionStatus' => null
];
$time = $options['timestamp'];
$requestTimestamp = $time->format(self::V4_TIMESTAMP_FORMAT);
$requestDatestamp = $time->format(self::V4_DATESTAMP_FORMAT);
$expiration = \DateTimeImmutable::createFromFormat('U', (string) $expires);
$expirationTimestamp = str_replace(
'+00:00',
'Z',
$expiration->format(\DateTime::RFC3339)
);
$clientEmail = $credentials->getClientName();
$credentialScope = sprintf('%s/auto/storage/goog4_request', $requestDatestamp);
$credential = sprintf('%s/%s', $clientEmail, $credentialScope);
if ($options['virtualHostedStyle']) {
$options['bucketBoundHostname'] = sprintf(
'%s.storage.googleapis.com',
$bucket
);
}
$fields = array_merge($options['fields'], [
'key' => $object,
'x-goog-algorithm' => self::V4_ALGO_NAME,
'x-goog-credential' => $credential,
'x-goog-date' => $requestTimestamp
]);
$conditions = $options['conditions'];
foreach ($options['fields'] as $key => $value) {
$conditions[] = [$key => $value];
}
foreach ($conditions as $key => $value) {
$key = $key;
$value = $value;
$conditions[$key] = $value;
}
$conditions = array_merge($conditions, [
['bucket' => $bucket],
['key' => $object],
['x-goog-date' => $requestTimestamp],
['x-goog-credential' => $credential],
['x-goog-algorithm' => self::V4_ALGO_NAME],
]);
$policy = [
'conditions' => $conditions,
'expiration' => $expirationTimestamp
];
$json = str_replace('\\\u', '\\u', json_encode($policy, JSON_UNESCAPED_SLASHES));
$stringToSign = base64_encode($json);
$signature = bin2hex(base64_decode($credentials->signBlob($stringToSign, [
'forceOpenssl' => $options['forceOpenssl']
])));
$fields['x-goog-signature'] = $signature;
$fields['policy'] = $stringToSign;
// Construct the modified resource name. If a custom hostname is provided,
// this will remove the bucket name from the resource.
$resource = $this->normalizeUriPath($options['bucketBoundHostname'], '/' . $bucket, true);
$scheme = $this->chooseScheme(
$options['scheme'],
$options['bucketBoundHostname'],
$options['virtualHostedStyle']
);
return [
'url' => sprintf(
'%s://%s%s',
$scheme,
$options['bucketBoundHostname'],
$resource
),
'fields' => $fields
];
}
/**
* Creates a canonical request hash for a V4 Signed URL.
*
* NOTE: While in most cases `PHP_EOL` is preferable to a system-specific
* character, in this case `\n` is required.
*
* @param array $canonicalRequest The canonical request, with each element
* representing a line in the request.
* @return string
*/
private function createV4CanonicalRequest(array $canonicalRequest)
{
$canonicalRequestString = implode("\n", $canonicalRequest);
return bin2hex(hash('sha256', $canonicalRequestString, true));
}
/**
* Creates a canonical request for a V2 Signed URL.
*
* NOTE: While in most cases `PHP_EOL` is preferable to a system-specific
* character, in this case `\n` is required.
*
* @param array $canonicalRequest The canonical request, with each element
* representing a line in the request.
* @return string
*/
private function createV2CanonicalRequest(array $canonicalRequest)
{
return implode("\n", $canonicalRequest);
}
/**
* Choose the correct URL scheme.
*
* @param string $scheme The scheme provided by the user or defaults.
* @param string $bucketBoundHostname The bucketBoundHostname provided by the user or defaults.
* @param bool $virtualHostedStyle Whether virtual host style is enabled.
* @return string
*/
private function chooseScheme($scheme, $bucketBoundHostname, $virtualHostedStyle = false)
{
// bucketBoundHostname not used -- always https.
if ($bucketBoundHostname === self::DEFAULT_DOWNLOAD_HOST) {
return 'https';
}
// virtualHostedStyle enabled -- always https.
if ($virtualHostedStyle) {
return 'https';
}
// not virtual hosted style, and custom hostname -- use default (http) or user choice.
return $scheme;
}
/**
* If `X-Goog-Content-SHA256` header is provided, use that as the payload.
* Otherwise, `UNSIGNED-PAYLOAD`.
*
* @param array $headers
* @return string
*/
private function getPayloadHash(array $headers)
{
if (!isset($headers['x-goog-content-sha256'])) {
return 'UNSIGNED-PAYLOAD';
}
return $headers['x-goog-content-sha256'];
}
/**
* Normalizes and validates an expiration.
*
* @param Timestamp|\DateTimeInterface|int $expires The expiration
* @return int
* @throws \InvalidArgumentException If an invalid value is given.
*/
private function normalizeExpiration($expires)
{
if ($expires instanceof Timestamp) {
$seconds = $expires->get()->format('U');
} elseif ($expires instanceof \DateTimeInterface) {
$seconds = $expires->format('U');
} elseif (is_numeric($expires)) {
$seconds = (int) $expires;
} else {
throw new \InvalidArgumentException('Invalid expiration.');
}
return $seconds;
}
/**
* Normalizes and encodes the resource identifier.
*
* @param string $resource The resource identifier. In form
* `[/]$bucket/$object`.
* @return array A list, where index 0 is the resource path, with pieces
* encoded and prefixed with a forward slash, index 1 is the bucket
* name, and index 2 is the object name, relative to the bucket.
*/
private function normalizeResource($resource, $urlencode = true)
{
$pieces = explode('/', trim($resource, '/'));
if ($urlencode) {
array_walk($pieces, function (&$piece) {
$piece = rawurlencode($piece);
});
}
$bucket = $pieces[0];
$relative = $pieces;
array_shift($relative);
return [
'/' . implode('/', $pieces),
$bucket,
'/' . implode('/', $relative),
];
}
/**
* Fixes the user input options, filters and validates data.
*
* @param array $options Signed URL configuration options.
* @return array
* @throws \InvalidArgumentException
*/
private function normalizeOptions(array $options)
{
$options += [
'allowPost' => false,
'cname' => null, //@deprecated
'bucketBoundHostname' => self::DEFAULT_DOWNLOAD_HOST,
'contentMd5' => null,
'contentType' => null,
'forceOpenssl' => false,
'headers' => [],
'keyFile' => null,
'keyFilePath' => null,
'credentialsFetcher' => null,
'method' => 'GET',
'queryParams' => [],
'responseDisposition' => null,
'responseType' => null,
'saveAsName' => null,
// note that in almost every case this default will be overridden.
'scheme' => 'http',
'timestamp' => null,
'virtualHostedStyle' => false,
];
$allowedMethods = ['GET', 'PUT', 'POST', 'DELETE'];
$options['method'] = strtoupper($options['method']);
if (!in_array($options['method'], $allowedMethods)) {
throw new \InvalidArgumentException('$options.method must be one of `GET`, `PUT` or `DELETE`.');
}
if ($options['method'] === 'POST' && !$options['allowPost']) {
throw new \InvalidArgumentException(
'Invalid method. To create an upload URI, use StorageObject::signedUploadUrl().'
);
}
// Rewrite deprecated `cname` to new `bucketBoundHostname`.
if ($options['cname'] && $options['bucketBoundHostname'] === self::DEFAULT_DOWNLOAD_HOST) {
$options['bucketBoundHostname'] = $options['cname'];
}
// strip protocol from hostname.
$hostnameParts = explode('//', $options['bucketBoundHostname']);
if (count($hostnameParts) > 1) {
$options['bucketBoundHostname'] = $hostnameParts[1];
}
$options['bucketBoundHostname'] = trim($options['bucketBoundHostname'], '/');
// If a timestamp is provided, use it in place of `now` for v4 URLs only..
// This option exists for testing purposes, and should not generally be provided by users.
if ($options['timestamp']) {
if (!($options['timestamp'] instanceof \DateTimeInterface)) {
if (!is_string($options['timestamp'])) {
throw new \InvalidArgumentException(
'User-provided timestamps must be a string or instance of `\DateTimeInterface`.'
);
}
$options['timestamp'] = \DateTimeImmutable::createFromFormat(
\DateTime::RFC3339,
$options['timestamp'],
new \DateTimeZone('UTC')
);
if (!$options['timestamp']) {
throw new \InvalidArgumentException(
'Given timestamp string is in an invalid format. Provide timestamp formatted as follows: `' .
\DateTime::RFC3339 .
'`. Note that timestamps MUST be in UTC.'
);
}
}
} else {
$options['timestamp'] = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
}
unset(
$options['cname'],
$options['allowPost']
);
return $options;
}
/**
* Cleans and normalizes header values.
*
* Arrays of values are collapsed into a comma-separated list, trailing and
* leading spaces are removed, newlines are replaced by empty strings, and
* multiple whitespace chars are replaced by a single space.
*
* @param array $headers Input headers
* @return array
*/
private function normalizeHeaders(array $headers)
{
$out = [];
foreach ($headers as $name => $value) {
$name = strtolower(trim($name));
// collapse arrays of values into a comma-separated list.
if (!is_array($value)) {
$value = [$value];
}
foreach ($value as &$headerValue) {
// strip trailing and leading spaces.
$headerValue = trim($headerValue);
// replace newlines with empty strings.
$headerValue = str_replace(PHP_EOL, '', $headerValue);
// collapse multiple whitespace chars to a single space.
$headerValue = preg_replace('/[\s]+/', ' ', $headerValue);
}
$out[$name] = implode(', ', $value);
}
return $out;
}
/**
* Returns a resource formatted for use in a URI.
*
* If the bucketBoundHostname is other than the default, will omit the bucket name.
*
* @param string $bucketBoundHostname The bucketBoundHostname provided by the user, or the default
* value.
* @param string $resource The GCS resource path (i.e. /bucket/object).
* @return string
*/
private function normalizeUriPath($bucketBoundHostname, $resource, $withTrailingSlash = false)
{
if ($bucketBoundHostname !== self::DEFAULT_DOWNLOAD_HOST) {
$resourceParts = explode('/', trim($resource, '/'));
array_shift($resourceParts);
// Resource is a Bucket.
if (empty($resourceParts)) {
$resource = '/';
} else {
$resource = '/' . implode('/', $resourceParts);
}
}
$resource = rtrim($resource, '/');
return $withTrailingSlash
? $resource . '/'
: $resource;
}
/**
* Normalize the resource provided to the canonical request string.
*
* @param string $resource
* @param string $bucketBoundHostname
* @param boolean $virtualHostedStyle
* @return string
*/
private function normalizeCanonicalRequestResource($resource, $bucketBoundHostname, $virtualHostedStyle = false)
{
if ($bucketBoundHostname === self::DEFAULT_DOWNLOAD_HOST && !$virtualHostedStyle) {
return $resource;
}
$pieces = explode('/', trim($resource, '/'));
array_shift($pieces);
return '/' . implode('/', $pieces);
}
/**
* Get the credentials for use with signing.
*
* @param ConnectionInterface $connection A Storage connection object.
* This object is created by StorageClient,
* and should not be instantiated outside of this client.
* @param array $options Configuration options.
* @return array A list containing a credentials object at index 0 and the
* modified options at index 1.
* @throws \RuntimeException If the credentials type is not valid for signing.
* @throws \InvalidArgumentException If a keyfile is given and is not valid.
*/
private function getSigningCredentials(ConnectionInterface $connection, array $options)
{
$keyFilePath = $options['keyFilePath'] ?? null;
if ($keyFilePath) {
if (!file_exists($keyFilePath)) {
throw new \InvalidArgumentException(sprintf(
'Keyfile path %s does not exist.',
$keyFilePath
));
}
$options['keyFile'] = self::jsonDecode(file_get_contents($keyFilePath), true);
}
$rw = $connection->requestWrapper();
$keyFile = $options['keyFile'] ?? null;
if ($keyFile) {
$scopes = $options['scopes'] ?? $rw->scopes();
$credentials = CredentialsLoader::makeCredentials($scopes, $keyFile);
} elseif (isset($options['credentialsFetcher'])) {
$credentials = $options['credentialsFetcher'];
} else {
$credentials = $rw->getCredentialsFetcher();
}
//@codeCoverageIgnoreStart
if (!($credentials instanceof SignBlobInterface)) {
throw new \RuntimeException(sprintf(
'Credentials object is of type `%s` and is not valid for signing.',
get_class($credentials)
));
}
//@codeCoverageIgnoreEnd
unset(
$options['keyFilePath'],
$options['keyFile'],
$options['credentialsFetcher'],
$options['scopes']
);
return [$credentials, $options];
}
/**
* Add parameters common to all signed URL versions.
*
* @param int|null $generation
* @param array $params
* @param array $options
* @return array
*/
private function addCommonParams($generation, array $params, array $options)
{
if ($options['responseType']) {
$params['response-content-type'] = $options['responseType'];
}
if ($options['responseDisposition']) {
$params['response-content-disposition'] = $options['responseDisposition'];
} elseif ($options['saveAsName']) {
$params['response-content-disposition'] = 'attachment; filename='
. '"' . $options['saveAsName'] . '"';
}
if ($generation) {
$params['generation'] = $generation;
}
return $params;
}
/**
* Create a query string from an array.
*
* Note that this method does NOT urlencode keys or values.
*
* @param array $input
* @return string
*/
private function buildQueryString(array $input)
{
$q = [];
foreach ($input as $key => $val) {
$q[] = $key . '=' . $val;
}
return implode('&', $q);
}
/**
* Retry logic for signBlob
*
* @param callable $signBlobFn A callable that perform the actual signBlob operation.
* @param string $resourceName The resource name for logging or retry strategy determination.
* @param array $args Arguments for the operations, include preconditions
* @return string The signature genarated by signBlob.
* @throws ServiceException If non-retryable error occur.
* @throws \RuntimeException If retries are exhausted.
*/
private function retrySignBlob(callable $signBlobFn, string $resourceName = 'signBlob', array $args = [])
{
$attempt = 0;
// Generate a retry decider function using the RetryTrait logic.
$retryDecider = $this->getRestRetryFunction($resourceName, 'execute', $args);
while (true) {
++$attempt;
try {
// Attempt the operation
return $signBlobFn();
} catch (\Exception $exception) {
if (!$retryDecider($exception, $attempt, self::MAX_RETRIES)) {
// Non-retryable error
throw $exception;
}
}
}
}
}

View File

@@ -0,0 +1,732 @@
<?php
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Auth\FetchAuthTokenInterface;
use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Core\ClientTrait;
use Google\Cloud\Core\Exception\GoogleException;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Core\Upload\SignedUrlUploader;
use Google\Cloud\Storage\Connection\ConnectionInterface;
use Google\Cloud\Storage\Connection\Rest;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\StreamInterface;
/**
* Google Cloud Storage allows you to store and retrieve data on Google's
* infrastructure. Find more information at the
* [Google Cloud Storage API docs](https://developers.google.com/storage).
*
* Example:
* ```
* use Google\Cloud\Storage\StorageClient;
*
* $storage = new StorageClient();
* ```
*/
class StorageClient
{
use ArrayTrait;
use ClientTrait;
const VERSION = '1.49.0';
const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control';
const READ_ONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only';
const READ_WRITE_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_write';
/**
* Retry strategy to signify that we never want to retry an operation
* even if the error is retryable.
*
* We can set $options['retryStrategy'] to one of "always", "never" and
* "idempotent".
*/
const RETRY_NEVER = 'never';
/**
* Retry strategy to signify that we always want to retry an operation.
*/
const RETRY_ALWAYS = 'always';
/**
* This is the default. This signifies that we want to retry an operation
* only if it is retryable and the error is retryable.
*/
const RETRY_IDEMPOTENT = 'idempotent';
/**
* @var ConnectionInterface Represents a connection to Storage.
* @internal
*/
protected $connection;
/**
* Create a Storage client.
*
* @param array $config [optional] {
* Configuration options.
*
* @type string $apiEndpoint The hostname with optional port to use in
* place of the default service endpoint. Example:
* `foobar.com` or `foobar.com:1234`.
* @type string $projectId The project ID from the Google Developer's
* Console.
* @type CacheItemPoolInterface $authCache A cache used storing access
* tokens. **Defaults to** a simple in memory implementation.
* @type array $authCacheOptions Cache configuration options.
* @type callable $authHttpHandler A handler used to deliver Psr7
* requests specifically for authentication.
* @type FetchAuthTokenInterface $credentialsFetcher A credentials
* fetcher instance.
* @type callable $httpHandler A handler used to deliver Psr7 requests.
* Only valid for requests sent over REST.
* @type array $keyFile [DEPRECATED]
* This option is being deprecated because of a potential security risk.
* This option does not validate the credential configuration. The security
* risk occurs when a credential configuration is accepted from a source
* that is not under your control and used without validation on your side.
* If you know that you will be loading credential configurations of a
* specific type, it is recommended to create the credentials directly and
* configure them using the `credentialsFetcher` option instead.
* ```
* use Google\Auth\Credentials\ServiceAccountCredentials;
* $credentialsFetcher = new ServiceAccountCredentials($scopes, $json);
* $creds = new StorageClient(['credentialsFetcher' => $creds]);
* ```
* This will ensure that an unexpected credential type with potential for
* malicious intent is not loaded unintentionally. You might still have to do
* validation for certain credential types.
* If you are loading your credential configuration from an untrusted source and have
* not mitigated the risks (e.g. by validating the configuration yourself), make
* these changes as soon as possible to prevent security risks to your environment.
* Regardless of the method used, it is always your responsibility to validate
* configurations received from external sources.
* @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
* @type string $keyFilePath [DEPRECATED]
* This option is being deprecated because of a potential security risk.
* This option does not validate the credential configuration. The security
* risk occurs when a credential configuration is accepted from a source
* that is not under your control and used without validation on your side.
* If you know that you will be loading credential configurations of a
* specific type, it is recommended to create the credentials directly and
* configure them using the `credentialsFetcher` option instead.
* ```
* use Google\Auth\Credentials\ServiceAccountCredentials;
* $credentialsFetcher = new ServiceAccountCredentials($scopes, $json);
* $creds = new StorageClient(['credentialsFetcher' => $creds]);
* ```
* This will ensure that an unexpected credential type with potential for
* malicious intent is not loaded unintentionally. You might still have to do
* validation for certain credential types.
* If you are loading your credential configuration from an untrusted source and have
* not mitigated the risks (e.g. by validating the configuration yourself), make
* these changes as soon as possible to prevent security risks to your environment.
* Regardless of the method used, it is always your responsibility to validate
* configurations received from external sources.
* @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
* @type float $requestTimeout Seconds to wait before timing out the
* request. **Defaults to** `0` with REST and `60` with gRPC.
* @type int $retries Number of retries for a failed request.
* **Defaults to** `3`.
* @type string $retryStrategy Retry strategy to signify that we never
* want to retry an operation even if the error is retryable.
* **Defaults to** `StorageClient::RETRY_IDEMPOTENT`.
* @type callable $restDelayFunction Executes a delay, defaults to
* utilizing `usleep`. Function signature should match:
* `function (int $delay) : void`.
* @type callable $restCalcDelayFunction Sets the conditions for
* determining how long to wait between attempts to retry. Function
* signature should match: `function (int $attempt) : int`.
* @type callable $restRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex) : bool`.
* @type callable $restRetryListener Runs after the restRetryFunction.
* This might be used to simply consume the exception and
* $arguments b/w retries. This returns the new $arguments thus
* allowing modification on demand for $arguments. For ex:
* changing the headers in b/w retries.
* @type array $scopes Scopes to be used for the request.
* @type string $quotaProject Specifies a user project to bill for
* access charges associated with the request.
* }
*/
public function __construct(array $config = [])
{
if (!isset($config['scopes'])) {
$config['scopes'] = [
'https://www.googleapis.com/auth/iam',
self::FULL_CONTROL_SCOPE,
];
}
$this->connection = new Rest($this->configureAuthentication($config) + [
'projectId' => $this->projectId
]);
}
/**
* Lazily instantiates a bucket.
*
* There are no network requests made at this point. To see the operations
* that can be performed on a bucket please see {@see Bucket}.
*
* If `$userProject` is set to true, the current project ID (used to
* instantiate the client) will be billed for all requests. If
* `$userProject` is a project ID, given as a string, that project
* will be billed for all requests. This only has an effect when the bucket
* is not owned by the current or given project ID.
*
* Example:
* ```
* $bucket = $storage->bucket('my-bucket');
* ```
*
* @param string $name The name of the bucket to request.
* @param string|bool $userProject If true, the current Project ID
* will be used. If a string, that string will be used as the
* userProject argument, and that project will be billed for the
* request. **Defaults to** `false`.
* @param array $options [optional] {
* Configuration Options.
*
* @type bool $softDeleted If set to true, only soft-deleted bucket versions
* are listed as distinct results in order of bucket name and generation
* number. The default value is false.
* @type string $generation If present, selects a specific soft-deleted version
* of this bucket instead of the live version. This parameter is required if
* softDeleted is set to true.
* }
* @return Bucket
*/
public function bucket($name, $userProject = false, array $options = [])
{
if (!$userProject) {
$userProject = null;
} elseif (!is_string($userProject)) {
$userProject = $this->projectId;
}
return new Bucket($this->connection, $name, $options + [
'requesterProjectId' => $userProject
]);
}
/**
* Fetches all buckets in the project.
*
* Example:
* ```
* $buckets = $storage->buckets();
* ```
*
* ```
* // Get all buckets beginning with the prefix 'album'.
* $buckets = $storage->buckets([
* 'prefix' => 'album'
* ]);
*
* foreach ($buckets as $bucket) {
* echo $bucket->name() . PHP_EOL;
* }
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/buckets/list Buckets list API documentation.
*
* @param array $options [optional] {
* Configuration options.
*
* @type int $maxResults Maximum number of results to return per
* requested page.
* @type int $resultLimit Limit the number of results returned in total.
* **Defaults to** `0` (return all results).
* @type string $pageToken A previously-returned page token used to
* resume the loading of results from a specific point.
* @type string $prefix Filter results with this prefix.
* @type string $projection Determines which properties to return. May
* be either 'full' or 'noAcl'.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request.
* @type bool $softDeleted If set to true, only soft-deleted bucket versions
* are listed as distinct results in order of bucket name and generation
* number. The default value is false.
* @type bool $bucketUserProject If true, each returned instance will
* have `$userProject` set to the value of `$options.userProject`.
* If false, `$options.userProject` will be used ONLY for the
* listBuckets operation. If `$options.userProject` is not set,
* this option has no effect. **Defaults to** `true`.
* @type bool $returnPartialSuccess If true, the returned iterator will contain an
* `unreachable` property with a list of buckets that were not retrieved.
* **Note:** If set to false (default) and unreachable buckets are found,
* the operation will throw an exception.
*
* }
* @return BucketIterator<Bucket>
* @throws GoogleException When a project ID has not been detected.
*/
public function buckets(array $options = [])
{
$this->requireProjectId();
$resultLimit = $this->pluck('resultLimit', $options, false);
$bucketUserProject = $this->pluck('bucketUserProject', $options, null) ?? true;
$userProject = $bucketUserProject ? ($options['userProject'] ?? null) : null;
$unreachable = new \ArrayObject();
$apiCall = [$this->connection, 'listBuckets'];
$callDelegate = function (array $args) use ($apiCall, $unreachable) {
$response = call_user_func($apiCall, $args);
if (isset($response['unreachable']) && is_array($response['unreachable'])) {
$current = $unreachable->getArrayCopy();
$updated = array_unique(array_merge($current, $response['unreachable']));
$unreachable->exchangeArray($updated);
}
return $response;
};
// Return the new BucketIterator with the wrapped unreachable bucket
return new BucketIterator(
new PageIterator(
function (array $bucket) use ($userProject) {
return new Bucket(
$this->connection,
$bucket['name'],
$bucket + ['requesterProjectId' => $userProject]
);
},
$callDelegate,
$options + ['project' => $this->projectId],
['resultLimit' => $resultLimit]
),
$unreachable
);
}
/**
* Restores a soft-deleted bucket.
*
* Example:
* ```
* $bucket = $storage->bucket->restore('my-bucket');
* ```
*
* @param string $name The name of the bucket to restore.
* @param string $generation The specific version of the bucket to be restored.
* @param array $options [optional] {
* Configuration Options.
*
* @type string $projection Determines which properties to return. May
* be either `"full"` or `"noAcl"`. **Defaults to** `"noAcl"`,
* unless the bucket resource specifies acl or defaultObjectAcl
* properties, when it defaults to `"full"`.
* }
* @return Bucket
*/
public function restore(string $name, string $generation, array $options = [])
{
$res = $this->connection->restoreBucket([
'bucket' => $name,
'generation' => $generation,
] + $options);
return new Bucket(
$this->connection,
$name
);
}
/**
* Create a bucket. Bucket names must be unique as Cloud Storage uses a flat
* namespace. For more information please see
* [bucket name requirements](https://cloud.google.com/storage/docs/naming#requirements)
*
* Example:
* ```
* $bucket = $storage->createBucket('bucket');
* ```
*
* ```
* // Create a bucket with logging enabled.
* $bucket = $storage->createBucket('myBeautifulBucket', [
* 'logging' => [
* 'logBucket' => 'bucketToLogTo',
* 'logObjectPrefix' => 'myPrefix'
* ]
* ]);
* ```
*
* @see https://cloud.google.com/storage/docs/json_api/v1/buckets/insert Buckets insert API documentation.
*
* @param string $name Name of the bucket to be created.
* @codingStandardsIgnoreStart
* @param array $options [optional] {
* Configuration options.
*
* @type string $predefinedAcl Predefined ACL to apply to the bucket.
* Acceptable values include, `"authenticatedRead"`,
* `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
* `"projectPrivate"`, and `"publicRead"`.
* @type string $predefinedDefaultObjectAcl Apply a predefined set of
* default object access controls to this bucket.
* @type bool $enableObjectRetention Whether object retention should
* be enabled on this bucket. For more information, refer to the
* [Object Retention Lock](https://cloud.google.com/storage/docs/object-lock)
* documentation.
* @type string $projection Determines which properties to return. May
* be either `"full"` or `"noAcl"`. **Defaults to** `"noAcl"`,
* unless the bucket resource specifies acl or defaultObjectAcl
* properties, when it defaults to `"full"`.
* @type string $fields Selector which will cause the response to only
* return the specified fields.
* @type array $acl Access controls on the bucket.
* @type array $cors The bucket's Cross-Origin Resource Sharing (CORS)
* configuration.
* @type array $defaultObjectAcl Default access controls to apply to new
* objects when no ACL is provided.
* @type array|Lifecycle $lifecycle The bucket's lifecycle configuration.
* @type string $location The location of the bucket. If specifying
* a dual-region, the `customPlacementConfig` property should be
* set in conjunction. For more information, see
* [Bucket Locations](https://cloud.google.com/storage/docs/locations).
* **Defaults to** `"US"`.
* @type array $hierarchicalNamespace The hierarchical namespace configuration
* on this bucket.
* @type array $customPlacementConfig The bucket's dual regions. For more
* information, see
* [Bucket Locations](https://cloud.google.com/storage/docs/locations).
* @type array $logging The bucket's logging configuration, which
* defines the destination bucket and optional name prefix for the
* current bucket's logs.
* @type string $storageClass The bucket's storage class. This defines
* how objects in the bucket are stored and determines the SLA and
* the cost of storage. Acceptable values include the following
* strings: `"STANDARD"`, `"NEARLINE"`, `"COLDLINE"` and
* `"ARCHIVE"`. Legacy values including `"MULTI_REGIONAL"`,
* `"REGIONAL"` and `"DURABLE_REDUCED_AVAILABILITY"` are also
* available, but should be avoided for new implementations. For
* more information, refer to the
* [Storage Classes](https://cloud.google.com/storage/docs/storage-classes)
* documentation. **Defaults to** `"STANDARD"`.
* @type array $autoclass The bucket's autoclass configuration.
* Buckets can have either StorageClass OLM rules or Autoclass,
* but not both. When Autoclass is enabled on a bucket, adding
* StorageClass OLM rules will result in failure.
* For more information, refer to
* [Storage Autoclass](https://cloud.google.com/storage/docs/autoclass)
* @type array $versioning The bucket's versioning configuration.
* @type array $website The bucket's website configuration.
* @type array $billing The bucket's billing configuration.
* @type bool $billing.requesterPays When `true`, requests to this bucket
* and objects within it must provide a project ID to which the
* request will be billed.
* @type array $labels The Bucket labels. Labels are represented as an
* array of keys and values. To remove an existing label, set its
* value to `null`.
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request.
* @type bool $bucketUserProject If true, the returned instance will
* have `$userProject` set to the value of `$options.userProject`.
* If false, `$options.userProject` will be used ONLY for the
* createBucket operation. If `$options.userProject` is not set,
* this option has no effect. **Defaults to** `true`.
* @type array $encryption Encryption configuration used by default for
* newly inserted objects.
* @type string $encryption.defaultKmsKeyName A Cloud KMS Key used to
* encrypt objects uploaded into this bucket. Should be in the
* format
* `projects/my-project/locations/kr-location/keyRings/my-kr/cryptoKeys/my-key`.
* Please note the KMS key ring must use the same location as the
* bucket.
* @type bool $defaultEventBasedHold When `true`, newly created objects
* in this bucket will be retained indefinitely until an event
* occurs, signified by the hold's release.
* @type array $retentionPolicy Defines the retention policy for a
* bucket. In order to lock a retention policy, please see
* {@see Bucket::lockRetentionPolicy()}.
* @type int $retentionPolicy.retentionPeriod Specifies the retention
* period for objects in seconds. During the retention period an
* object cannot be overwritten or deleted. Retention period must
* be greater than zero and less than 100 years.
* @type array $iamConfiguration The bucket's IAM configuration.
* @type bool $iamConfiguration.bucketPolicyOnly.enabled this is an alias
* for $iamConfiguration.uniformBucketLevelAccess.
* @type bool $iamConfiguration.uniformBucketLevelAccess.enabled If set and
* true, access checks only use bucket-level IAM policies or
* above. When enabled, requests attempting to view or manipulate
* ACLs will fail with error code 400. **NOTE**: Before using
* Uniform bucket-level access, please review the
* [feature documentation](https://cloud.google.com/storage/docs/uniform-bucket-level-access),
* as well as
* [Should You Use uniform bucket-level access](https://cloud.google.com/storage/docs/uniform-bucket-level-access#should-you-use)
* @type string $rpo Specifies the Turbo Replication setting for a dual-region bucket.
* The possible values are DEFAULT and ASYNC_TURBO. Trying to set the rpo for a non dual-region
* bucket will throw an exception. Non existence of this parameter is equivalent to it being DEFAULT.
* }
* @codingStandardsIgnoreEnd
* @return Bucket
* @throws GoogleException When a project ID has not been detected.
*/
public function createBucket($name, array $options = [])
{
$this->requireProjectId();
if (isset($options['lifecycle']) && $options['lifecycle'] instanceof Lifecycle) {
$options['lifecycle'] = $options['lifecycle']->toArray();
}
$bucketUserProject = $this->pluck('bucketUserProject', $options, false);
$bucketUserProject = !is_null($bucketUserProject)
? $bucketUserProject
: true;
$userProject = (isset($options['userProject']) && $bucketUserProject)
? $options['userProject']
: null;
$response = $this->connection->insertBucket($options + ['name' => $name, 'project' => $this->projectId]);
return new Bucket(
$this->connection,
$name,
$response + ['requesterProjectId' => $userProject]
);
}
/**
* Registers this StorageClient as the handler for stream reading/writing.
*
* @param string $protocol The name of the protocol to use. **Defaults to** `gs`.
* @throws \RuntimeException
*/
public function registerStreamWrapper($protocol = null)
{
return StreamWrapper::register($this, $protocol);
}
/**
* Unregisters the SteamWrapper
*
* @param string $protocol The name of the protocol to unregister. **Defaults to** `gs`.
*/
public function unregisterStreamWrapper($protocol = null)
{
StreamWrapper::unregister($protocol);
}
/**
* Create an uploader to handle a Signed URL.
*
* Example:
* ```
* $uploader = $storage->signedUrlUploader($uri, fopen('/path/to/myfile.doc', 'r'));
* ```
*
* @param string $uri The URI to accept an upload request.
* @param string|resource|StreamInterface $data The data to be uploaded
* @param array $options [optional] Configuration Options. Refer to
* {@see \Google\Cloud\Core\Upload\AbstractUploader::__construct()}.
* @return SignedUrlUploader
*/
public function signedUrlUploader($uri, $data, array $options = [])
{
return new SignedUrlUploader($this->connection->requestWrapper(), $data, $uri, $options);
}
/**
* Create a Timestamp object.
*
* Example:
* ```
* $timestamp = $storage->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z'));
* ```
*
* @param \DateTimeInterface $timestamp The timestamp value.
* @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp.
* @return Timestamp
*/
public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null)
{
return new Timestamp($timestamp, $nanoSeconds);
}
/**
* Get the service account email associated with this client.
*
* Example:
* ```
* $serviceAccount = $storage->getServiceAccount();
* ```
*
* @param array $options [optional] {
* Configuration options.
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request.
* }
* @return string
*/
public function getServiceAccount(array $options = [])
{
$resp = $this->connection->getServiceAccount($options + ['projectId' => $this->projectId]);
return $resp['email_address'];
}
/**
* List Service Account HMAC keys in the project.
*
* Example:
* ```
* $hmacKeys = $storage->hmacKeys();
* ```
*
* ```
* // Get the HMAC keys associated with a Service Account email
* $hmacKeys = $storage->hmacKeys([
* 'serviceAccountEmail' => $serviceAccountEmail
* ]);
* ```
*
* @param array $options {
* Configuration Options
*
* @type string $serviceAccountEmail If present, only keys for the given
* service account are returned.
* @type bool $showDeletedKeys Whether or not to show keys in the
* DELETED state.
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request.
* @type string $projectId The project ID to use, if different from that
* with which the client was created.
* }
* @return ItemIterator<HmacKey>
*/
public function hmacKeys(array $options = [])
{
$options += [
'projectId' => $this->projectId
];
if (!$options['projectId']) {
$this->requireProjectId();
}
$resultLimit = $this->pluck('resultLimit', $options, false);
return new ItemIterator(
new PageIterator(
function (array $metadata) use ($options) {
return $this->hmacKey(
$metadata['accessId'],
$options['projectId'],
$metadata
);
},
[$this->connection, 'listHmacKeys'],
$options,
['resultLimit' => $resultLimit]
)
);
}
/**
* Lazily instantiate an HMAC Key instance using an Access ID.
*
* Example:
* ```
* $hmacKey = $storage->hmacKey($accessId);
* ```
*
* @param string $accessId The ID of the HMAC Key.
* @param string $projectId [optional] The project ID to use, if different
* from that with which the client was created.
* @param array $metadata [optional] HMAC key metadata.
* @return HmacKey
*/
public function hmacKey($accessId, $projectId = null, array $metadata = [])
{
if (!$projectId) {
$this->requireProjectId();
}
return new HmacKey($this->connection, $projectId ?: $this->projectId, $accessId, $metadata);
}
/**
* Creates a new HMAC key for the specified service account.
*
* Please note that the HMAC secret is only available at creation. Make sure
* to note the secret after creation.
*
* Example:
* ```
* $response = $storage->createHmacKey('account@myProject.iam.gserviceaccount.com');
* $secret = $response->secret();
* ```
*
* @param string $serviceAccountEmail Email address of the service account.
* @param array $options {
* Configuration Options
*
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request. **NOTE**: This option is
* currently ignored by Cloud Storage.
* @type string $projectId The project ID to use, if different from that
* with which the client was created.
* }
* @return CreatedHmacKey
*/
public function createHmacKey($serviceAccountEmail, array $options = [])
{
$options += [
'projectId' => $this->projectId
];
if (!$options['projectId']) {
$this->requireProjectId();
}
$res = $this->connection->createHmacKey([
'projectId' => $options['projectId'],
'serviceAccountEmail' => $serviceAccountEmail
] + $options);
$key = new HmacKey(
$this->connection,
$options['projectId'],
$res['metadata']['accessId'],
$res['metadata']
);
return new CreatedHmacKey($key, $res['secret']);
}
/**
* Throw an exception if no project ID available.
*
* @return void
* @throws GoogleException
*/
private function requireProjectId()
{
if (!$this->projectId) {
throw new GoogleException(
'No project ID was provided, ' .
'and we were unable to detect a default project ID.'
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,880 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;
use GuzzleHttp\Psr7\CachingStream;
/**
* A streamWrapper implementation for handling `gs://bucket/path/to/file.jpg`.
* Note that you can only open a file with mode 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', or 'at'.
*
* See: http://php.net/manual/en/class.streamwrapper.php
*/
class StreamWrapper
{
const DEFAULT_PROTOCOL = 'gs';
const FILE_WRITABLE_MODE = 33206; // 100666 in octal
const FILE_READABLE_MODE = 33060; // 100444 in octal
const DIRECTORY_WRITABLE_MODE = 16895; // 40777 in octal
const DIRECTORY_READABLE_MODE = 16676; // 40444 in octal
const TAIL_NAME_SUFFIX = '~';
/**
* @var resource|null Must be public according to the PHP documentation.
*
* Contains array of context options in form ['protocol' => ['option' => value]].
* Options used by StreamWrapper:
*
* flush (bool) `true`: fflush() will flush output buffer; `false`: fflush() will do nothing
*/
public $context;
/**
* @var \Psr\Http\Message\StreamInterface
*/
private $stream;
/**
* @var string Protocol used to open this stream
*/
private $protocol;
/**
* @var Bucket Reference to the bucket the opened file
* lives in or will live in.
*/
private $bucket;
/**
* @var string Name of the file opened by this stream.
*/
private $file;
/**
* @var StorageClient[] $clients The default clients to use if using
* global methods such as fopen on a stream wrapper. Keyed by protocol.
*/
private static $clients = [];
/**
* @var ObjectIterator Used for iterating through a directory
*/
private $directoryIterator;
/**
* @var StorageObject
*/
private $object;
/**
* @var array Context options passed to stream_open(), used for append mode and flushing.
*/
private $options = [];
/**
* @var bool `true`: fflush() will flush output buffer and redirect output to the "tail" object.
*/
private $flushing = false;
/**
* @var string|null Content type for composed object. Will be filled on first composing.
*/
private $contentType = null;
/**
* @var bool `true`: writing the "tail" object, next fflush() or fclose() will compose.
*/
private $composing = false;
/**
* @var bool `true`: data has been written to the stream.
*/
private $dirty = false;
/**
* Ensure we close the stream when this StreamWrapper is destroyed.
*/
public function __destruct()
{
$this->stream_close();
}
/**
* This is called when include/require is used on a stream.
*/
public function stream_set_option()
{
return false;
}
/**
* This is called when touch is used on a stream. See:
* https://www.php.net/manual/en/streamwrapper.stream-metadata.php
*/
public function stream_metadata($path, $option, $value)
{
if ($option == STREAM_META_TOUCH) {
$this->openPath($path);
return $this->touch();
}
return false;
}
/**
* Creates an empty file if it does not exist.
* @return bool Returns true if file exists or has been created, false otherwise.
*/
private function touch()
{
$object = $this->bucket->object($this->file);
try {
if (!$object->exists()) {
$this->bucket->upload('', [
'name' => $this->file
]);
}
return true;
} catch (NotFoundException $e) {
}
return false;
}
/**
* Register a StreamWrapper for reading and writing to Google Storage
*
* @param StorageClient $client The StorageClient configuration to use.
* @param string $protocol The name of the protocol to use. **Defaults to**
* `gs`.
* @throws \RuntimeException
*/
public static function register(StorageClient $client, $protocol = null)
{
$protocol = $protocol ?: self::DEFAULT_PROTOCOL;
if (!in_array($protocol, stream_get_wrappers())) {
if (!stream_wrapper_register($protocol, StreamWrapper::class, STREAM_IS_URL)) {
throw new \RuntimeException("Failed to register '$protocol://' protocol");
}
self::$clients[$protocol] = $client;
return true;
}
return false;
}
/**
* Unregisters the SteamWrapper
*
* @param string $protocol The name of the protocol to unregister. **Defaults
* to** `gs`.
*/
public static function unregister($protocol = null)
{
$protocol = $protocol ?: self::DEFAULT_PROTOCOL;
stream_wrapper_unregister($protocol);
unset(self::$clients[$protocol]);
}
/**
* Get the default client to use for streams.
*
* @param string $protocol The name of the protocol to get the client for.
* **Defaults to** `gs`.
* @return StorageClient
*/
public static function getClient($protocol = null)
{
$protocol = $protocol ?: self::DEFAULT_PROTOCOL;
return self::$clients[$protocol];
}
/**
* Callback handler for when a stream is opened. For reads, we need to
* download the file to see if it can be opened.
*
* @param string $path The path of the resource to open
* @param string $mode The fopen mode. Currently supports ('r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at')
* @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK
* @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set
* @return bool
*/
public function stream_open($path, $mode, $flags, &$openedPath)
{
$this->openPath($path);
// strip off 'b' or 't' from the mode
$mode = rtrim($mode, 'bt');
$options = [];
if ($this->context) {
$contextOptions = stream_context_get_options($this->context);
if (array_key_exists($this->protocol, $contextOptions)) {
$options = $contextOptions[$this->protocol] ?: [];
}
if (isset($options['flush'])) {
$this->flushing = (bool) $options['flush'];
unset($options['flush']);
}
$this->options = $options;
}
if ($mode == 'w') {
$this->stream = new WriteStream(null, $options);
$this->stream->setUploader(
$this->bucket->getStreamableUploader(
$this->stream,
$options + ['name' => $this->file]
)
);
} elseif ($mode == 'a') {
try {
$info = $this->bucket->object($this->file)->info();
$this->composing = ($info['size'] > 0);
} catch (NotFoundException $e) {
}
$this->stream = new WriteStream(null, $options);
$name = $this->file;
if ($this->composing) {
$name .= self::TAIL_NAME_SUFFIX;
}
$this->stream->setUploader(
$this->bucket->getStreamableUploader(
$this->stream,
$options + ['name' => $name]
)
);
} elseif ($mode == 'r') {
try {
// Lazy read from the source
$options['restOptions']['stream'] = true;
$this->stream = new ReadStream(
$this->bucket->object($this->file)->downloadAsStream($options)
);
// Wrap the response in a caching stream to make it seekable
if (!$this->stream->isSeekable() && ($flags & STREAM_MUST_SEEK)) {
$this->stream = new CachingStream($this->stream);
}
} catch (ServiceException $ex) {
return $this->returnError($ex->getMessage(), $flags);
}
} else {
return $this->returnError('Unknown stream_open mode.', $flags);
}
if ($flags & STREAM_USE_PATH) {
$openedPath = $path;
}
return true;
}
/**
* Callback handler for when we try to read a certain number of bytes.
*
* @param int $count The number of bytes to read.
*
* @return string
*/
public function stream_read($count)
{
return $this->stream->read($count);
}
/**
* Callback handler for when we try to write data to the stream.
*
* @param string $data The data to write
*
* @return int The number of bytes written.
*/
public function stream_write($data)
{
$result = $this->stream->write($data);
$this->dirty = $this->dirty || (bool) $result;
return $result;
}
/**
* Callback handler for getting data about the stream.
*
* @return array
*/
public function stream_stat()
{
$mode = $this->stream->isWritable()
? self::FILE_WRITABLE_MODE
: self::FILE_READABLE_MODE;
return $this->makeStatArray([
'mode' => $mode,
'size' => $this->stream->getSize()
]);
}
/**
* Callback handler for checking to see if the stream is at the end of file.
*
* @return bool
*/
public function stream_eof()
{
return $this->stream->eof();
}
/**
* Callback handler for trying to close the stream.
*/
public function stream_close()
{
if (isset($this->stream)) {
$this->stream->close();
}
if ($this->composing) {
if ($this->dirty) {
$this->compose();
$this->dirty = false;
}
try {
$this->bucket->object($this->file . self::TAIL_NAME_SUFFIX)->delete();
} catch (NotFoundException $e) {
}
$this->composing = false;
}
}
/**
* Callback handler for trying to seek to a certain location in the stream.
*
* @param int $offset The stream offset to seek to
* @param int $whence Flag for what the offset is relative to. See:
* http://php.net/manual/en/streamwrapper.stream-seek.php
* @return bool
*/
public function stream_seek($offset, $whence = SEEK_SET)
{
if ($this->stream->isSeekable()) {
$this->stream->seek($offset, $whence);
return true;
}
return false;
}
/**
* Callhack handler for inspecting our current position in the stream
*
* @return int
*/
public function stream_tell()
{
return $this->stream->tell();
}
/**
* Callback handler for trying to close an opened directory.
*
* @return bool
*/
public function dir_closedir()
{
return false;
}
/**
* Callback handler for trying to open a directory.
*
* @param string $path The url directory to open
* @param int $options Whether or not to enforce safe_mode
* @return bool
*/
public function dir_opendir($path, $options)
{
$this->openPath($path);
return $this->dir_rewinddir();
}
/**
* Callback handler for reading an entry from a directory handle.
*
* @return string|bool
*/
public function dir_readdir()
{
$name = $this->directoryIterator->current();
if ($name) {
$this->directoryIterator->next();
return $name;
}
return false;
}
/**
* Callback handler for rewind the directory handle.
*
* @return bool
*/
public function dir_rewinddir()
{
try {
$iterator = $this->bucket->objects([
'prefix' => $this->file,
'fields' => 'items/name,nextPageToken'
]);
// The delimiter options do not give us what we need, so instead we
// list all results matching the given prefix, enumerate the
// iterator, filter and transform results, and yield a fresh
// generator containing only the directory listing.
$this->directoryIterator = call_user_func(function () use ($iterator) {
$yielded = [];
$pathLen = strlen($this->makeDirectory($this->file));
foreach ($iterator as $object) {
$name = substr($object->name(), $pathLen);
$parts = explode('/', $name);
// since the service call returns nested results and we only
// want to yield results directly within the requested directory,
// check if we've already yielded this value.
if ($parts[0] === '' || in_array($parts[0], $yielded)) {
continue;
}
$yielded[] = $parts[0];
yield $name => $parts[0];
}
});
} catch (ServiceException $e) {
return false;
}
return true;
}
/**
* Callback handler for trying to create a directory. If no file path is specified,
* or STREAM_MKDIR_RECURSIVE option is set, then create the bucket if it does not exist.
*
* @param string $path The url directory to create
* @param int $mode The permissions on the directory
* @param int $options Bitwise mask of options. STREAM_MKDIR_RECURSIVE
* @return bool
*/
public function mkdir($path, $mode, $options)
{
$path = $this->makeDirectory($path);
$client = $this->openPath($path);
$predefinedAcl = $this->determineAclFromMode($mode);
try {
if ($options & STREAM_MKDIR_RECURSIVE || $this->file == '') {
if (!$this->bucket->exists()) {
$client->createBucket($this->bucket->name(), [
'predefinedAcl' => $predefinedAcl,
'predefinedDefaultObjectAcl' => $predefinedAcl
]);
}
}
// If the file name is empty, we were trying to create a bucket. In this case,
// don't create the placeholder file.
if ($this->file != '') {
$bucketInfo = $this->bucket->info();
$ublEnabled = isset($bucketInfo['iamConfiguration']['uniformBucketLevelAccess']) &&
$bucketInfo['iamConfiguration']['uniformBucketLevelAccess']['enabled'] === true;
// if bucket has uniform bucket level access enabled, don't set ACLs.
$acl = [];
if (!$ublEnabled) {
$acl = [
'predefinedAcl' => $predefinedAcl
];
}
// Fake a directory by creating an empty placeholder file whose name ends in '/'
$this->bucket->upload('', [
'name' => $this->file,
] + $acl);
}
} catch (ServiceException $e) {
return false;
}
return true;
}
/**
* Callback handler for trying to move a file or directory.
*
* @param string $from The URL to the current file
* @param string $to The URL of the new file location
* @return bool
*/
public function rename($from, $to)
{
$this->openPath($from);
$destination = (array) parse_url($to) + [
'path' => '',
'host' => ''
];
$destinationBucket = $destination['host'];
$destinationPath = substr($destination['path'], 1);
// loop through to rename file and children, if given path is a directory.
$objects = $this->bucket->objects([
'prefix' => $this->file
]);
foreach ($objects as $obj) {
$oldName = $obj->name();
$newPath = str_replace($this->file, $destinationPath, $oldName);
try {
$obj->rename($newPath, [
'destinationBucket' => $destinationBucket
]);
} catch (ServiceException $e) {
return false;
}
}
return true;
}
/**
* Callback handler for trying to remove a directory or a bucket. If the path is empty
* or '/', the bucket will be deleted.
*
* Note that the STREAM_MKDIR_RECURSIVE flag is ignored because the option cannot
* be set via the `rmdir()` function.
*
* @param string $path The URL directory to remove. If the path is empty or is '/',
* This will attempt to destroy the bucket.
* @param int $options Bitwise mask of options.
* @return bool
*/
public function rmdir($path, $options)
{
$path = $this->makeDirectory($path);
$this->openPath($path);
try {
if ($this->file == '') {
$this->bucket->delete();
return true;
} else {
return $this->unlink($path);
}
} catch (ServiceException $e) {
return false;
}
}
/**
* Callback handler for retrieving the underlaying resource
*
* @param int $castAs STREAM_CAST_FOR_SELECT|STREAM_CAST_AS_STREAM
* @return resource|bool
*/
public function stream_cast($castAs)
{
return false;
}
/**
* Callback handler for deleting a file
*
* @param string $path The URL of the file to delete
* @return bool
*/
public function unlink($path)
{
$client = $this->openPath($path);
$object = $this->bucket->object($this->file);
try {
$object->delete();
return true;
} catch (ServiceException $e) {
return false;
}
}
/**
* Callback handler for retrieving information about a file
*
* @param string $path The URI to the file
* @param int $flags Bitwise mask of options
* @return array|bool
*/
public function url_stat($path, $flags)
{
$client = $this->openPath($path);
// if directory
$dir = $this->getDirectoryInfo($this->file);
if ($dir) {
return $this->urlStatDirectory($dir);
}
return $this->urlStatFile();
}
/**
* Callback handler for fflush() function.
*
* @return bool
*/
public function stream_flush()
{
if (!$this->flushing) {
return false;
}
if (!$this->dirty) {
return true;
}
if (isset($this->stream)) {
$this->stream->close();
}
if ($this->composing) {
$this->compose();
}
$options = $this->options;
$this->stream = new WriteStream(null, $options);
$this->stream->setUploader(
$this->bucket->getStreamableUploader(
$this->stream,
$options + ['name' => $this->file . self::TAIL_NAME_SUFFIX]
)
);
$this->composing = true;
$this->dirty = false;
return true;
}
/**
* Parse the URL and set protocol, filename and bucket.
*
* @param string $path URL to open
* @return StorageClient
*/
private function openPath($path)
{
$url = (array) parse_url($path) + [
'scheme' => '',
'path' => '',
'host' => ''
];
$this->protocol = $url['scheme'];
$this->file = ltrim($url['path'], '/');
$client = self::getClient($this->protocol);
$this->bucket = $client->bucket($url['host']);
return $client;
}
/**
* Given a path, ensure that we return a path that looks like a directory
*
* @param string $path
* @return string
*/
private function makeDirectory($path)
{
if ($path == '' or $path == '/') {
return '';
}
if (substr($path, -1) == '/') {
return $path;
}
return $path . '/';
}
/**
* Calculate the `url_stat` response for a directory
*
* @return array|bool
*/
private function urlStatDirectory(StorageObject $object)
{
$stats = [];
$info = $object->info();
// equivalent to 40777 and 40444 in octal
$stats['mode'] = $this->bucket->isWritable()
? self::DIRECTORY_WRITABLE_MODE
: self::DIRECTORY_READABLE_MODE;
$this->statsFromFileInfo($info, $stats);
return $this->makeStatArray($stats);
}
/**
* Calculate the `url_stat` response for a file
*
* @return array|bool
*/
private function urlStatFile()
{
try {
$this->object = $this->bucket->object($this->file);
$info = $this->object->info();
} catch (ServiceException $e) {
// couldn't stat file
return false;
}
// equivalent to 100666 and 100444 in octal
$stats = [
'mode' => $this->bucket->isWritable()
? self::FILE_WRITABLE_MODE
: self::FILE_READABLE_MODE
];
$this->statsFromFileInfo($info, $stats);
return $this->makeStatArray($stats);
}
/**
* Given a `StorageObject` info array, extract the available fields into the
* provided `$stats` array.
*
* @param array $info Array provided from a `StorageObject`.
* @param array $stats Array to put the calculated stats into.
*/
private function statsFromFileInfo(array &$info, array &$stats)
{
$stats['size'] = (isset($info['size']))
? (int) $info['size']
: null;
$stats['mtime'] = (isset($info['updated']))
? strtotime($info['updated'])
: null;
$stats['ctime'] = (isset($info['timeCreated']))
? strtotime($info['timeCreated'])
: null;
}
/**
* Get the given path as a directory.
*
* In list objects calls, directories are returned with a trailing slash. By
* providing the given path with a trailing slash as a list prefix, we can
* check whether the given path exists as a directory.
*
* If the path does not exist or is not a directory, return null.
*
* @param string $path
* @return StorageObject|null
*/
private function getDirectoryInfo($path)
{
$scan = $this->bucket->objects([
'prefix' => $this->makeDirectory($path),
'resultLimit' => 1,
'fields' => 'items/name,items/size,items/updated,items/timeCreated,nextPageToken'
]);
return $scan->current();
}
/**
* Returns the associative array that a `stat()` response expects using the
* provided stats. Defaults the remaining fields to 0.
*
* @param array $stats Sparse stats entries to set.
* @return array
*/
private function makeStatArray($stats)
{
return array_merge(
array_fill_keys([
'dev',
'ino',
'mode',
'nlink',
'uid',
'gid',
'rdev',
'size',
'atime',
'mtime',
'ctime',
'blksize',
'blocks'
], 0),
$stats
);
}
/**
* Helper for whether or not to trigger an error or just return false on an error.
*
* @param string $message The PHP error message to emit.
* @param int $flags Bitwise mask of options (STREAM_REPORT_ERRORS)
* @return bool Returns false
*/
private function returnError($message, $flags)
{
if ($flags & STREAM_REPORT_ERRORS) {
trigger_error($message, E_USER_WARNING);
}
return false;
}
/**
* Helper for determining which predefinedAcl to use given a mode.
*
* @param int $mode Decimal representation of the file system permissions
* @return string
*/
private function determineAclFromMode($mode)
{
if ($mode & 0004) {
// If any user can read, assume it should be publicRead.
return 'publicRead';
} elseif ($mode & 0040) {
// If any group user can read, assume it should be projectPrivate.
return 'projectPrivate';
}
// Otherwise, assume only the project/bucket owner can use the bucket.
return 'private';
}
private function compose()
{
if (!isset($this->contentType)) {
$info = $this->bucket->object($this->file)->info();
$this->contentType = $info['contentType'] ?: 'application/octet-stream';
}
$options = ['destination' => ['contentType' => $this->contentType]];
$this->bucket->compose([$this->file, $this->file . self::TAIL_NAME_SUFFIX], $this->file, $options);
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Google\Cloud\Storage;
use Google\Cloud\Core\Upload\AbstractUploader;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* A Stream implementation that uploads in chunks to a provided uploader when
* we reach a certain chunkSize. Upon `close`, we will upload the remaining chunk.
*/
class WriteStream implements StreamInterface
{
use StreamDecoratorTrait;
private $uploader;
private $stream;
private $chunkSize = 262144;
private $hasWritten = false;
/**
* Create a new WriteStream instance
*
* @param AbstractUploader $uploader The uploader to use.
* @param array $options [optional] {
* Configuration options.
*
* @type int $chunkSize The size of the buffer above which we attempt to
* upload data
* }
*/
public function __construct(?AbstractUploader $uploader = null, $options = [])
{
if ($uploader) {
$this->setUploader($uploader);
}
if (array_key_exists('chunkSize', $options)) {
$this->chunkSize = $options['chunkSize'];
}
$this->stream = new BufferStream($this->chunkSize);
}
/**
* Close the stream. Uploads any remaining data.
*/
public function close(): void
{
if ($this->uploader && $this->hasWritten) {
$this->uploader->upload();
$this->uploader = null;
}
}
/**
* Write to the stream. If we pass the chunkable size, upload the available chunk.
*
* @param string $data Data to write
* @return int The number of bytes written
* @throws \RuntimeException
*/
public function write($data): int
{
if (!isset($this->uploader)) {
throw new \RuntimeException('No uploader set.');
}
// Ensure we have a resume uri here because we need to create the streaming
// upload before we have data (size of 0).
$this->uploader->getResumeUri();
$this->hasWritten = true;
if (!$this->stream->write($data)) {
$this->uploader->upload($this->getChunkedWriteSize());
}
return strlen($data);
}
/**
* Set the uploader for this class. You may need to set this after initialization
* if the uploader depends on this stream.
*
* @param AbstractUploader $uploader The new uploader to use.
*/
public function setUploader($uploader): void
{
$this->uploader = $uploader;
}
private function getChunkedWriteSize(): int
{
return (int) floor($this->getSize() / $this->chunkSize) * $this->chunkSize;
}
}