feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)

Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.

Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
  - Provider key: s3compatible
  - Reads user-configured endpoint URL from settings
  - Uses path-style URL access (required by most S3-compatible services)
  - Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
    AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
  - Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,160 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\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 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 copyObject(array $args = []);
/**
* @param array $args
*/
public function rewriteObject(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,66 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection;
use DeliciousBrains\WP_Offload_Media\Gcp\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,618 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Auth\GetUniverseDomainInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\RequestBuilder;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\RequestWrapper;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\RestTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Retry;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection\RetryTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\AbstractUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\MultipartUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\ResumableUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\StreamableUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\UriTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection\ConnectionInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\StorageClient;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Exception\RequestException;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\MimeType;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\Request;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\Utils;
use DeliciousBrains\WP_Offload_Media\Gcp\Psr\Http\Message\RequestInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Psr\Http\Message\ResponseInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Psr\Http\Message\StreamInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\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;
/**
* @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;
}
/**
* @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 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 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 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;
list($request, $requestOptions) = $this->buildDownloadObjectParams($args);
$invocationId = Uuid::uuid4()->toString();
$requestOptions['retryHeaders'] = self::getRetryHeaders($invocationId, 1);
$requestOptions['restRetryFunction'] = $this->getRestRetryFunction('objects', 'get', $requestOptions);
// 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;
}
};
$requestOptions['restRetryListener'] = function (\Exception $e, $retryAttempt, &$arguments) use($resultStream, $requestedBytes, $invocationId) {
// if the exception has a response for us to use
if ($e instanceof RequestException && $e->hasResponse()) {
$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);
}
};
$fetchedStream = $this->requestWrapper->send($request, $requestOptions)->getBody();
// 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['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]);
$uri = $this->expandUri($this->apiEndpoint . self::DOWNLOAD_PATH, ['bucket' => $args['bucket'], 'object' => $args['object'], 'query' => ['generation' => $args['generation'], 'alt' => 'media', 'userProject' => $args['userProject']]]);
return [new Request('GET', Utils::uriFor($uri)), $requestOptions];
}
/**
* Choose a 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.
*
* @deprecated
* @return bool
*/
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 = $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);
// Adding callback logic to update headers while retrying
$args['restRetryListener'] = function (\Exception $e, $retryAttempt, &$arguments) use($invocationId) {
$arguments[0] = $this->modifyRequestForRetry($arguments[0], $retryAttempt, $invocationId);
};
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,173 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection;
use DeliciousBrains\WP_Offload_Media\Gcp\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,
// connetion-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'];
/**
* 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) use($isOpIdempotent, $preconditionNeeded, $preconditionSupplied, $retryStrategy) {
return $this->retryDeciderFunction($exception, $isOpIdempotent, $preconditionNeeded, $preconditionSupplied, $retryStrategy);
};
}
/**
* 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 $maxRetries
* @return bool
*/
private function retryDeciderFunction(\Exception $exception, $isIdempotent, $preconditionNeeded, $preconditionSupplied, $retryStrategy)
{
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,82 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\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,120 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\phpseclib\Crypt\RSA as RSA2;
use DeliciousBrains\WP_Offload_Media\Gcp\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,173 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\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,357 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\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 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,190 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\ArrayTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Exception\NotFoundException;
use DeliciousBrains\WP_Offload_Media\Gcp\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,39 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Iterator\ItemIteratorTrait;
/**
* ObjectIterator
*
* Iterates over a set of {@see StorageObject} items.
*/
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,73 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\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,86 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\StreamDecoratorTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\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,622 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Auth\CredentialsLoader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Auth\SignBlobInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\ArrayTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\JsonTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Timestamp;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection\ConnectionInterface;
/**
* Provides common methods for signing storage URLs.
*
* @internal
*/
class SigningHelper
{
use ArrayTrait;
use JsonTrait;
const DEFAULT_URL_SIGNING_VERSION = 'v2';
const DEFAULT_DOWNLOAD_HOST = 'storage.googleapis.com';
const V4_ALGO_NAME = 'GOOG4-RSA-SHA256';
const V4_TIMESTAMP_FORMAT = 'DeliciousBrains\\WP_Offload_Media\\Gcp\\Ymd\\THis\\Z';
const V4_DATESTAMP_FORMAT = 'Ymd';
/**
* 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);
$signature = $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($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,
'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);
} 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['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);
}
}

View File

@@ -0,0 +1,528 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Auth\FetchAuthTokenInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\ArrayTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\ClientTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Exception\GoogleException;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Iterator\ItemIterator;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Iterator\PageIterator;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Timestamp;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\SignedUrlUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection\ConnectionInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Connection\Rest;
use DeliciousBrains\WP_Offload_Media\Gcp\Psr\Cache\CacheItemPoolInterface;
use DeliciousBrains\WP_Offload_Media\Gcp\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.39.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 The contents of the service account credentials
* .json file retrieved from the Google Developer's Console.
* Ex: `json_decode(file_get_contents($path), true)`.
* @type string $keyFilePath The full path to your service account
* credentials .json file retrieved from the Google Developers
* Console.
* @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 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`.
* @return Bucket
*/
public function bucket($name, $userProject = \false)
{
if (!$userProject) {
$userProject = null;
} elseif (!\is_string($userProject)) {
$userProject = $this->projectId;
}
return new Bucket($this->connection, $name, ['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 $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`.
* }
* @return ItemIterator<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, \false);
$bucketUserProject = !\is_null($bucketUserProject) ? $bucketUserProject : \true;
$userProject = isset($options['userProject']) && $bucketUserProject ? $options['userProject'] : null;
return new ItemIterator(new PageIterator(function (array $bucket) use($userProject) {
return new Bucket($this->connection, $bucket['name'], $bucket + ['requesterProjectId' => $userProject]);
}, [$this->connection, 'listBuckets'], $options + ['project' => $this->projectId], ['resultLimit' => $resultLimit]));
}
/**
* 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 $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,687 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Exception\NotFoundException;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Exception\ServiceException;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Bucket;
use DeliciousBrains\WP_Offload_Media\Gcp\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();
}
/**
* Starting PHP 7.4, this is called when include/require is used on a stream.
* Absence of this method presents a warning.
* https://www.php.net/manual/en/migration74.incompatible.php
*/
public function stream_set_option()
{
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)
{
$client = $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 = array('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 & 04) {
// If any user can read, assume it should be publicRead.
return 'publicRead';
} elseif ($mode & 040) {
// 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,101 @@
<?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 DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Upload\AbstractUploader;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\StreamDecoratorTrait;
use DeliciousBrains\WP_Offload_Media\Gcp\GuzzleHttp\Psr7\BufferStream;
use DeliciousBrains\WP_Offload_Media\Gcp\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;
}
}