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,43 @@
# Contributor Code of Conduct
As contributors and maintainers of this project,
and in the interest of fostering an open and welcoming community,
we pledge to respect all people who contribute through reporting issues,
posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project
a harassment-free experience for everyone,
regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information,
such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct.
By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently
applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct
may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue
or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)

202
vendor/Gcp/google/cloud-storage/LICENSE vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View File

@@ -0,0 +1,7 @@
# Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.

View File

@@ -0,0 +1 @@
1.39.0

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;
}
}