diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9b66c3d9567..3998b3c57e85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,4 +97,4 @@ Your pull request is where we (and anyone else who is interested) will discuss y Be sure to check your pull request for a `cla:yes` label. If you see a `cla:no` label, verify that you have [signed the CLA](#signing-the-contributor-license-agreement-cla) using a Google Account that matches your Git email. Once your pull request has the `cla:yes` label, look out for an email notification that either your pull request has been merged, or someone has requested a little more work on it. If more work is needed, repeat **steps 5**, **7-11**, and **14**. Then, let us know when you're done and we'll take another look. -Happy contributing! And, once again, thank you. \ No newline at end of file +Happy contributing! And, once again, thank you. diff --git a/README.md b/README.md index 16f2c465cc5d..c73ed7396b2d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Google Cloud PHP Client +[![Travis Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-php.svg)](https://travis-ci.org/GoogleCloudPlatform/gcloud-php/) + > Idiomatic PHP client for [Google Cloud Platform](https://cloud.google.com/) services. -[![Travis Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-php.svg)](https://travis-ci.org/GoogleCloudPlatform/gcloud-php/) +Please note this library is currently under active development. Any release versioned 0.x.y is subject to backwards incompatiable changes at any time. ## Contributing diff --git a/composer.json b/composer.json index 182c93b2da19..7c9d4dab4271 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "google/gcloud", + "name": "google/cloud", "type": "library", "description": "Google Cloud Client Library", "keywords": [ @@ -35,7 +35,7 @@ }, "autoload": { "psr-4": { - "Google\\Gcloud\\": "src" + "Google\\Cloud\\": "src" } } } diff --git a/docs/toc.json b/docs/toc.json new file mode 100644 index 000000000000..3b2000b7edb0 --- /dev/null +++ b/docs/toc.json @@ -0,0 +1,50 @@ +{ + "overview": "overview.html", + "guides": [{ + "title": "Authentication", + "id": "authentication", + "edit": "https://github.com/GoogleCloudPlatform/gcloud-common/edit/master/authentication/readme.md", + "contents": [ + "https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-common/master/authentication/readme.md", + "authentication.md" + ] + }, { + "title": "FAQ", + "id": "faq", + "edit": "https://github.com/GoogleCloudPlatform/gcloud-common/edit/master/faq/readme.md", + "contents": [ + "https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-common/master/faq/readme.md", + "faq.md" + ] + }, { + "title": "Troubleshooting", + "id": "troubleshooting", + "edit": "https://github.com/GoogleCloudPlatform/gcloud-common/edit/master/troubleshooting/readme.md", + "contents": [ + "https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-common/master/troubleshooting/readme.md", + "troubleshooting.md" + ] + }, { + "title": "Contributing", + "id": "contributing", + "edit": "https://github.com/GoogleCloudPlatform/gcloud-common/edit/master/contributing/readme.md", + "contents": "https://raw.githubusercontent.com/GoogleCloudPlatform/gcloud-common/master/contributing/readme.md" + }], + "services": [{ + "title": "Gcloud", + "type": "gcloud", + }, { + "title": "Storage", + "type": "storage", + "nav": [{ + "title": "ACL", + "type": "acl", + }, { + "title": "Bucket", + "type": "bucket", + }, { + "title": "Object", + "type": "object", + }] + }] +} diff --git a/scripts/DocGenerator.php b/scripts/DocGenerator.php index 5ed625c5888f..8070f338bbc3 100644 --- a/scripts/DocGenerator.php +++ b/scripts/DocGenerator.php @@ -22,7 +22,7 @@ use phpDocumentor\Reflection\FileReflector; /** - * Parses given files and builds JSON documentation. + * Parses given files and builds documentation for our common docs site. */ class DocGenerator { @@ -47,14 +47,20 @@ public function __construct(array $files, $outputPath) */ public function generate() { + $types = []; + foreach ($this->files as $file) { $this->currentFile = substr(str_replace(__DIR__, '', $file), 3); $jsonOutputPath = $this->buildOutputPath(); $fileReflector = new FileReflector($file); $fileReflector->process(); - $reflector = isset($fileReflector->getClasses()[0]) ? $fileReflector->getClasses()[0] : $fileReflector->getInterfaces()[0]; + $document = $this->buildDocument($this->getReflector($fileReflector)); - $document = $this->buildDocument($reflector); + $types[] = [ + 'id' => $document['id'], + 'title' => $document['title'], + 'contents' => $document['id'] . '.json' + ]; if (!is_dir(dirname($jsonOutputPath))) { mkdir(dirname($jsonOutputPath), 0777, true); @@ -62,21 +68,39 @@ public function generate() file_put_contents($jsonOutputPath, json_encode($document)); } + + file_put_contents($this->outputPath . '/types.json', json_encode($types)); + } + + private function getReflector($fileReflector) + { + if (isset($fileReflector->getClasses()[0])) { + return $fileReflector->getClasses()[0]; + } + + if (isset($fileReflector->getInterfaces()[0])) { + return $fileReflector->getInterfaces()[0]; + } + + if (isset($fileReflector->getTraits()[0])) { + return $fileReflector->getTraits()[0]; + } } private function buildDocument($reflector) { $name = $reflector->getShortName(); - $title = explode('\\', $reflector->getNamespace()); - $title[] = $name; + $id = substr($reflector->getName(), 14); + $id = str_replace('\\', '/', $id); + // @todo see if there is a better way to determine the type + $type = end(explode('_', get_class($reflector->getNode()))); return [ - 'id' => strtolower($name), - 'metadata' => [ - 'name' => $name, - 'title' => $title, - 'description' => $this->buildDescription($reflector->getDocBlock()) - ], + 'id' => strtolower($id), + 'type' => strtolower($type), + 'title' => $reflector->getNamespace() . '\\' . $name, + 'name' => $name, + 'description' => $this->buildDescription($reflector->getDocBlock()), 'methods' => $this->buildMethods($reflector->getMethods()) ]; } @@ -94,7 +118,7 @@ private function buildDescription($docBlock, $content = null) foreach ($parsedContents as &$content) { if ($content instanceof Seetag) { $reference = $content->getReference(); - if (substr_compare($reference, 'Google\Gcloud', 0, 13) === 0) { + if (substr_compare($reference, 'Google\Cloud', 0, 12) === 0) { $content = $this->buildLink($reference); } } @@ -133,14 +157,13 @@ private function buildMethod($method) } return [ - 'metadata' => [ - 'constructor' => $method->getName() === '__construct' ? true : false, - 'name' => $method->getName(), - 'source' => $this->currentFile . '#L' . $method->getLineNumber(), - 'description' => $this->buildDescription($docBlock, $docText), - 'examples' => $this->buildExamples($examples), - 'resources' => $this->buildResources($resources) - ], + 'id' => $method->getName(), + 'type' => $method->getName() === '__construct' ? 'constructor' : 'instance', + 'name' => $method->getName(), + 'source' => $this->currentFile . '#L' . $method->getLineNumber(), + 'description' => $this->buildDescription($docBlock, $docText), + 'examples' => $this->buildExamples($examples), + 'resources' => $this->buildResources($resources), 'params' => $this->buildParams($params), 'exceptions' => $this->buildExceptions($exceptions), 'returns' => $this->buildReturns($returns) @@ -314,7 +337,7 @@ private function buildReturns($returns) private function handleTypes($types) { foreach ($types as &$type) { - if (substr_compare($type, '\Google\Gcloud', 0, 14) === 0) { + if (substr_compare($type, '\Google\Cloud', 0, 13) === 0) { $type = $this->buildLink($type); } } diff --git a/src/ClientInterface.php b/src/ClientInterface.php new file mode 100644 index 000000000000..21f5a3528fac --- /dev/null +++ b/src/ClientInterface.php @@ -0,0 +1,36 @@ +connection = $connection; + } +} diff --git a/src/Compute/Metadata.php b/src/Compute/Metadata.php index 4ddcdd3d1859..721ca5c2ca72 100644 --- a/src/Compute/Metadata.php +++ b/src/Compute/Metadata.php @@ -15,9 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -namespace Google\Gcloud\Compute; +namespace Google\Cloud\Compute; -use Google\Gcloud\Compute\Metadata\Readers\StreamReader; +use Google\Cloud\Compute\Metadata\Readers\StreamReader; /** * A library for accessing the Google Compute Engine (GCE) metadata. @@ -27,7 +27,7 @@ * * You can get the GCE metadata values very easily like: * - * use Google\Gcloud\Compute\Metadata; + * use Google\Cloud\Compute\Metadata; * * $metadata = new Metadata(); * $project_id = $metadata->getProjectId(); diff --git a/src/Compute/Metadata/Readers/StreamReader.php b/src/Compute/Metadata/Readers/StreamReader.php index 0777d8e6215a..cdc5e61242bf 100644 --- a/src/Compute/Metadata/Readers/StreamReader.php +++ b/src/Compute/Metadata/Readers/StreamReader.php @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -namespace Google\Gcloud\Compute\Metadata\Readers; +namespace Google\Cloud\Compute\Metadata\Readers; /** * A class only reading the metadata URL with an appropriate header. diff --git a/src/Exception/GoogleException.php b/src/Exception/GoogleException.php new file mode 100644 index 000000000000..f6bb9e7a328a --- /dev/null +++ b/src/Exception/GoogleException.php @@ -0,0 +1,26 @@ +retries = $retries !== null ? (int) $retries : 3; + // @todo revisit this approach + // @codeCoverageIgnoreStart + $this->delayFunction = function ($delay) { + usleep($delay); + }; + // @codeCoverageIgnoreEnd + } + + /** + * Executes the retry process. + * + * @param callable $function + * @param array $arguments + * @return mixed + * @throws \Exception The last exception caught while retrying. + */ + public function execute(callable $function, array $arguments = []) + { + $delayFunction = $this->delayFunction; + $retryAttempt = 0; + $exception = null; + + do { + try { + return call_user_func_array($function, $arguments); + } catch (\Exception $exception) { + if (!$this->shouldRetry($exception)) { + throw $exception; + } + + $delayFunction($this->calculateDelay($retryAttempt)); + $retryAttempt++; + } + } while ($this->retries >= $retryAttempt); + + throw $exception; + } + + /** + * @param callable $delayFunction + * @return void + */ + public function setDelayFunction(callable $delayFunction) + { + $this->delayFunction = $delayFunction; + } + + /** + * Calculates exponential delay. + * + * @param int $attempt + * @return int + */ + private function calculateDelay($attempt) + { + return min( + mt_rand(0, 1000000) + (pow(2, $attempt) * 1000000), + self::MAX_DELAY_MICROSECONDS + ); + } + + /** + * Determines whether or not the request should be retried. + * + * @param \Exception $ex + * @return bool + */ + private function shouldRetry(\Exception $ex) + { + $statusCode = $ex->getCode(); + $message = json_decode($ex->getMessage(), true); + + if (in_array($statusCode, $this->httpRetryCodes)) { + return true; + } + + if (!isset($message['error']['errors'])) { + return false; + } + + foreach ($message['error']['errors'] as $error) { + if (in_array($error['reason'], $this->httpRetryMessages)) { + return true; + } + } + + return false; + } +} diff --git a/src/HttpRequestWrapper.php b/src/HttpRequestWrapper.php deleted file mode 100644 index 0346ae52f9ef..000000000000 --- a/src/HttpRequestWrapper.php +++ /dev/null @@ -1,159 +0,0 @@ -httpHandler = $httpHandler ?: HttpHandlerFactory::build(); - $this->authHttpHandler = $authHttpHandler ?: $this->httpHandler; - $this->scopes = $scopes; - - if ($keyFile || $keyFilePath) { - $this->keyFileStream = Psr7\stream_for($keyFile ?: fopen($keyFilePath, 'r')); - } - } - - /** - * Deliver the request. - * - * @param RequestInterface $request Psr7 request. - * @param array $options HTTP specific configuration options. - * @return ResponseInterface - */ - public function send(RequestInterface $request, array $options = []) - { - $httpHandler = $this->httpHandler; - $signedRequest = $this->signRequest($request); - - try { - return $httpHandler($signedRequest, $options); - } catch (\Exception $ex) { - $this->handleException($ex); - } - } - - /** - * Sign the request. - * - * @param RequestInterface $request Psr7 request. - * @return RequestInterface - */ - public function signRequest(RequestInterface $request) - { - if (!$this->credentials || $this->credentials['expiry'] < time()) { - $this->credentials = $this->fetchCredentials(); - } - - return Psr7\modify_request($request, [ - 'set_headers' => [ - 'Authorization' => 'Bearer ' . $this->credentials['access_token'] - ] - ]); - } - - /** - * Fetches credentials. - * - * @return array - */ - private function fetchCredentials() - { - if ($this->keyFileStream) { - $credentialsFetcher = CredentialsLoader::makeCredentials($this->scopes, $this->keyFileStream); - } else { - $credentialsFetcher = ApplicationDefaultCredentials::getCredentials($this->scopes); - } - - $credentials = $credentialsFetcher->fetchAuthToken($this->authHttpHandler); - $credentials['expiry'] = time() + $this->credentials['expires_in']; - - return $credentials; - } - - /** - * Maps encountered exceptions to local exceptions. - * - * @param \Exception $ex - * @throws \Exception - * @todo map to custom exceptions - */ - private function handleException(\Exception $ex) - { - switch ($ex->getCode()) { - case 409: - throw new \Exception('Conflict'); - default: - throw new \Exception('Default'); - } - } -} diff --git a/src/RequestBuilder.php b/src/RequestBuilder.php new file mode 100644 index 000000000000..0a24244ddc5f --- /dev/null +++ b/src/RequestBuilder.php @@ -0,0 +1,117 @@ +service = $this->loadServiceDefinition($servicePath); + $this->baseUri = $baseUri; + } + + /** + * Build the request. + * + * @param string $resource + * @param string $method + * @param array $options + * @return RequestInterface + * @todo complexity high, revisit + * @todo consider validating against the schemas + */ + public function build($resource, $method, array $options = []) + { + if (!isset($this->service['resources'][$resource]['methods'][$method])) { + throw new \InvalidArgumentException('Provided action ' . $method . ' does not exist.'); + } + + $action = $this->service['resources'][$resource]['methods'][$method]; + $path = []; + $query = []; + $body = []; + + foreach ($action['parameters'] as $parameter => $parameterOptions) { + if ($parameterOptions['location'] === 'path' && array_key_exists($parameter, $options)) { + $path[$parameter] = $options[$parameter]; + } + + if ($parameterOptions['location'] === 'query' && array_key_exists($parameter, $options)) { + $query[$parameter] = $options[$parameter]; + } + } + + if (isset($action['request'])) { + $schema = $action['request']['$ref']; + + foreach ($this->service['schemas'][$schema]['properties'] as $property => $propertyOptions) { + if (array_key_exists($property, $options)) { + $body[$property] = $options[$property]; + } + } + } + + $uri = $this->buildUriWithQuery( + $this->expandUri($this->baseUri . $action['path'], $path), + $query + ); + + return new Request( + $action['httpMethod'], + $uri, + ['Content-Type' => 'application/json'], + $body ? json_encode($body) : [] + ); + } + + /** + * @param string $servicePath + * @return array + */ + private function loadServiceDefinition($servicePath) + { + return json_decode( + file_get_contents($servicePath, true), + true + ); + } +} diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php new file mode 100644 index 000000000000..27a8ca78d229 --- /dev/null +++ b/src/RequestWrapper.php @@ -0,0 +1,234 @@ + null, + 'authHttpHandler' => null, + 'credentialsFetcher' => null, + 'httpHandler' => null, + 'httpOptions' => [], + 'keyFile' => null, + 'keyFilePath' => null, + 'retries' => null, + 'scopes' => null + ]; + + if ($config['credentialsFetcher'] && !$config['credentialsFetcher'] instanceof FetchAuthTokenInterface) { + throw new \InvalidArgumentException('credentialsFetcher must implement FetchAuthTokenInterface.'); + } + + $this->accessToken = $config['accessToken']; + $this->credentialsFetcher = $config['credentialsFetcher']; + $this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build(); + $this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler; + $this->httpOptions = $config['httpOptions']; + $this->retries = $config['retries']; + $this->scopes = $config['scopes']; + + if ($config['keyFile'] || $config['keyFilePath']) { + $this->keyFileStream = Psr7\stream_for($config['keyFile'] ?: fopen($config['keyFilePath'], 'r')); + } + } + + /** + * Deliver the request. + * + * @param RequestInterface $request Psr7 request. + * @param array $options { + * Request options. + * + * @type int $retries Number of retries for a failed request. Defaults + * to 3. + * @type array $httpOptions HTTP client specific configuration options. + * } + * @return ResponseInterface + */ + public function send(RequestInterface $request, array $options = []) + { + $retries = isset($options['retries']) ? $options['retries'] : $this->retries; + $httpOptions = isset($options['httpOptions']) ? $options['httpOptions'] : $this->httpOptions; + $backoff = new ExponentialBackoff($retries); + + $signedRequest = $this->signRequest($request); + + try { + return $backoff->execute($this->httpHandler, [$signedRequest, $httpOptions]); + } catch (\Exception $ex) { + throw new GoogleException($ex->getMessage(), $ex->getCode(), $ex); + } + } + + /** + * Gets the credentials fetcher. Precedence begins with user supplied + * credentials fetcher instance, followed by a reference to a key file + * stream, and finally the application default credentials. + * + * @return FetchAuthTokenInterface + */ + public function getCredentialsFetcher() + { + if ($this->credentialsFetcher) { + return $this->credentialsFetcher; + } + + if ($this->keyFileStream) { + return CredentialsLoader::makeCredentials($this->scopes, $this->keyFileStream); + } + + return ApplicationDefaultCredentials::getCredentials($this->scopes, $this->authHttpHandler); + } + + /** + * Sign the request. + * + * @param RequestInterface $request Psr7 request. + * @return RequestInterface + */ + private function signRequest(RequestInterface $request) + { + $headers = [ + 'User-Agent' => 'gcloud-php ' . ClientInterface::VERSION, + 'Authorization' => 'Bearer ' . $this->getToken() + ]; + + return Psr7\modify_request($request, ['set_headers' => $headers]); + } + + /** + * Gets the access token. + * + * @return string + * @todo Investigate refreshing tokens + */ + private function getToken() + { + if ($this->accessToken) { + return $this->accessToken; + } + + if (!$this->credentials || $this->credentials['expiry'] < time()) { + $this->credentials = $this->fetchCredentials(); + } + + return $this->credentials['access_token']; + } + + /** + * Fetches credentials. + * + * @return array + */ + private function fetchCredentials() + { + try { + $credentials = $this->getCredentialsFetcher()->fetchAuthToken($this->authHttpHandler); + } catch (\Exception $ex) { + throw new GoogleException($ex->getMessage(), $ex->getCode(), $ex); + } + + $credentials['expiry'] = time() + $this->credentials['expires_in']; + + return $credentials; + } +} diff --git a/src/RestTrait.php b/src/RestTrait.php new file mode 100644 index 000000000000..6b6820d07944 --- /dev/null +++ b/src/RestTrait.php @@ -0,0 +1,84 @@ +requestBuilder = $requestBuilder; + } + + /** + * Sets the request wrapper. + * + * @param RequestWrapper $requestWrapper Wrapper used to handle sending + * requests to the JSON API. + */ + public function setRequestWrapper(RequestWrapper $requestWrapper) + { + $this->requestWrapper = $requestWrapper; + } + + /** + * Delivers a request built from the service definition. + * + * @param string $resource The resource type used for the request. + * @param string $method The method used for the request. + * @param array $options Options used to build out the request. + * @return array + */ + public function send($resource, $method, array $options = []) + { + $requestOptions = array_intersect_key($options, [ + 'httpOptions' => null, + 'retries' => null + ]); + + return json_decode( + $this->requestWrapper->send( + $this->requestBuilder->build($resource, $method, $options), + $requestOptions + )->getBody(), + true + ); + } +} diff --git a/src/Gcloud.php b/src/ServiceBuilder.php similarity index 52% rename from src/Gcloud.php rename to src/ServiceBuilder.php index 0fa3c57520b1..8c99e1a7053c 100644 --- a/src/Gcloud.php +++ b/src/ServiceBuilder.php @@ -15,29 +15,28 @@ * limitations under the License. */ -namespace Google\Gcloud; +namespace Google\Cloud; -use Google\Gcloud\Storage\Connection\REST; -use Google\Gcloud\Storage\StorageClient; +use Google\Auth\HttpHandler\HttpHandlerFactory; +use Google\Cloud\Storage\StorageClient; /** - * Gcloud is the official way to interact with the - * [Google Cloud Platform](https://cloud.google.com/). Google Cloud Platform is - * a set of modular cloud-based services that allow you to create anything from - * simple websites to complex applications. + * Google Cloud Platform is a set of modular cloud-based services that allow you + * to create anything from simple websites to complex applications. * * This API aims to expose access to these services in a way that is intuitive - * and easy to use for PHP enthusiasts. The Gcloud instance exposes factory - * methods which grant access to the various services contained within the API. + * and easy to use for PHP enthusiasts. The ServiceBuilder instance exposes + * factory methods which grant access to the various services contained within + * the API. * * Configuration is simple. Pass in an array of configuration options to the * constructor up front which can be shared between clients or specify the * options for the specific services you wish to access, e.g. Datastore, or * Storage. */ -class Gcloud +class ServiceBuilder { - const VERSION = '0.0.0'; + const VERSION = '0.1.0'; /** * @var array Configuration options to be used between clients. @@ -50,9 +49,9 @@ class Gcloud * * Example: * ``` - * use Google\Gcloud\Gcloud; + * use Google\Cloud\ServiceBuilder; * - * $gcloud = new Gcloud([ + * $builder = new ServiceBuilder([ * 'keyFilePath' => '/path/to/key/file.json', * 'projectId' => 'myAwesomeProject' * ]); @@ -61,21 +60,25 @@ class Gcloud * @param array $config { * Configuration options. * - * @type callable $httpHandler Override the default http handler. - * @type callable $authHttpHandler Override the default http handler - * used to fetch credentials. + * @type string $projectId The project ID from the Google Developer's + * Console. + * @type callable $authHttpHandler A handler used to deliver Psr7 + * requests specifically for authentication. + * @type callable $httpHandler A handler used to deliver Psr7 requests. + * @type string $keyFile The contents of the service account + * credentials .json file retrieved from the Google Developer's + * Console. * @type string $keyFilePath The full path to your service account * credentials .json file retrieved from the Google Developers * Console. - * @type string $keyFile The contents of the service account credentials - * .json file retrieved from the Google Developers Console. - * @type string $projectId The project ID created in the Google - * Developers Console. + * @type int $retries Number of retries for a failed request. Defaults + * to 3. + * @type array $scopes Scopes to be used for the request. * } */ public function __construct(array $config = []) { - $this->config = $config; + $this->config = $this->resolveConfig($config); } /** @@ -85,48 +88,39 @@ public function __construct(array $config = []) * * Example: * ``` - * use Google\Gcloud\Gcloud; + * use Google\Cloud\ServiceBuilder; * - * // Create a Gcloud instance using application default credentials. - * $gcloud = new Gcloud([ + * // Create a storage client using application default credentials. + * $builder = new ServiceBuilder([ * 'projectId' => 'myAwesomeProject' * ]); * - * $storage = $gcloud->storage(); + * $storage = $builder->storage(); * ``` * * @param array $config Configuration options. See - * {@see Google\Gcloud\Gcloud::__construct()} for the available options. + * {@see Google\Cloud\ServiceBuilder::__construct()} for the available options. * @return StorageClient */ public function storage(array $config = []) { - if (!$config) { - $config = $this->config; - } + $config = $config ? $this->resolveConfig($config) : $this->config; - if (!isset($config['projectId'])) { - throw new \InvalidArgumentException('A projectId is required.'); - } - - $config = $config + [ - 'keyFile' => null, - 'keyFilePath' => null, - 'httpHandler' => null, - 'authHttpHandler' => null - ]; + return new StorageClient($config); + } - $httpWrapper = new HttpRequestWrapper( - $config['keyFile'], - $config['keyFilePath'], - [StorageClient::DEFAULT_SCOPE], - $config['httpHandler'], - $config['authHttpHandler'] - ); + /** + * Resolves configuration options. + * + * @param array $config + * @return array + */ + private function resolveConfig(array $config) + { + if (!isset($config['httpHandler'])) { + $config['httpHandler'] = HttpHandlerFactory::build(); + } - return new StorageClient( - new REST($httpWrapper), - $config['projectId'] - ); + return $config; } } diff --git a/src/Storage/Acl.php b/src/Storage/Acl.php index 58238bb6bc6e..55a04f51a56f 100644 --- a/src/Storage/Acl.php +++ b/src/Storage/Acl.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Gcloud\Storage; +namespace Google\Cloud\Storage; -use Google\Gcloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\Storage\Connection\ConnectionInterface; /** * Google Cloud Storage uses access control lists (ACLs) to manage bucket and @@ -66,14 +66,21 @@ public function __construct(ConnectionInterface $connection, $type, array $ident } /** - * Delete access controls on a {@see Google\Gcloud\Storage\Bucket} or - * {@see Google\Gcloud\Storage\Object} for a specified entity. + * Delete access controls on a {@see Google\Cloud\Storage\Bucket} or + * {@see Google\Cloud\Storage\Object} 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 Configuration options. * @return void @@ -85,8 +92,8 @@ public function delete($entity, array $options = []) } /** - * Get access controls on a {@see Google\Gcloud\Storage\Bucket} or - * {@see Google\Gcloud\Storage\Object}. By default this will return all available + * Get access controls on a {@see Google\Cloud\Storage\Bucket} or + * {@see Google\Cloud\Storage\Object}. By default this will return all available * access controls. You may optionally specify a single entity to return * details for as well. * @@ -95,6 +102,13 @@ public function delete($entity, array $options = []) * $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 { * Configuration options. * @@ -113,14 +127,21 @@ public function get(array $options = []) } /** - * Add access controls on a {@see Google\Gcloud\Storage\Bucket} or - * {@see Google\Gcloud\Storage\Object}. + * Add access controls on a {@see Google\Cloud\Storage\Bucket} or + * {@see Google\Cloud\Storage\Object}. * * 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'. @@ -138,14 +159,21 @@ public function add($entity, $role, array $options = []) } /** - * Update access controls on a {@see Google\Gcloud\Storage\Bucket} or - * {@see Google\Gcloud\Storage\Object}. + * Update access controls on a {@see Google\Cloud\Storage\Bucket} or + * {@see Google\Cloud\Storage\Object}. * * 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'. diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index 02ff775a9d86..c3deb90ec2c8 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -15,9 +15,11 @@ * limitations under the License. */ -namespace Google\Gcloud\Storage; +namespace Google\Cloud\Storage; -use Google\Gcloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\Upload\ResumableUploader; +use GuzzleHttp\Psr7; use Psr\Http\Message\StreamInterface; /** @@ -71,13 +73,14 @@ public function __construct(ConnectionInterface $connection, $name, array $data * * Example: * ``` - * use Google\Gcloud\Storage\Acl; + * use Google\Cloud\Storage\Acl; * * $acl = $bucket->acl(); * $acl->add('allAuthenticatedUsers', Acl::ROLE_READER); * ``` * * @see https://cloud.google.com/storage/docs/access-control More about Access Control Lists + * * @return Acl An ACL instance configured to handle the bucket's access * control policies. */ @@ -91,7 +94,7 @@ public function acl() * * Example: * ``` - * use Google\Gcloud\Storage\Acl; + * use Google\Cloud\Storage\Acl; * * $acl = $bucket->defaultAcl(); * $acl->add('allAuthenticatedUsers', Acl::ROLE_READER); @@ -128,57 +131,143 @@ public function exists() } /** - * Upload data. + * Upload your data in a simple fashion. Uploads will default to being + * resumable if the file size is greater than 5mb. * * Example: * ``` * $bucket->upload( + * fopen('image.jpg', 'r') + * ); + * ``` + * + * ``` + * $options = [ + * 'resumable' => true, + * 'name' => '/images/image.jpg', + * 'metadata' => [ + * 'contentLanguage' => 'en' + * ] + * ]; + * + * $bucket->upload( * fopen('image.jpg', 'r'), - * 'image.jpg', - * ['contentType' => 'image/jpeg'] + * $options * ); * ``` * - * @param string|resource|StreamInterface $data - * @param string $destination Name of where the file will be stored. + * @see https://cloud.google.com/storage/docs/json_api/v1/how-tos/upload#resumable Learn more about resumable + * uploads. + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation. + * + * @param string|resource|StreamInterface $data The data to be uploaded. * @param array $options { - * Configuration options. + * Configuration options. * - * @type string $contentType The content type. + * @type string $name The name of the destination. + * @type bool $resumable Indicates whether or not the upload will be + * performed in a resumable fashion. + * @type bool $validate Indicates whether or not validation will be + * applied using md5 hashing functionality. If true and the + * calculated hash does not match that of the upstream server the + * upload will be rejected. + * @type int $chunkSize If provided the upload will be done in chunks. + * The size must be in multiples of 262144 bytes. With chunking + * you have increased reliability at the risk of higher overhead. + * It is recommended to not use chunking. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Defaults to private. Acceptable values include, + * authenticatedRead, bucketOwnerFullControl, bucketOwnerRead, + * private, projectPrivate, and publicRead. + * @type array $metadata The available options for metadata are outlined + * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request) * } - * @return \Google\Gcloud\Storage\Object + * @return \Google\Cloud\Storage\Object + * @throws \InvalidArgumentException */ - public function upload($data, $destination, array $options = []) + public function upload($data, array $options = []) { - $response = $this->connection->uploadObject($options + [ + if (is_string($data) && !isset($options['name'])) { + throw new \InvalidArgumentException('A name is required when data is of type string.'); + } + + $response = $this->connection->insertObject($options + [ 'bucket' => $this->identity['bucket'], - 'data' => $data, - 'name' => $destination - ]); + 'data' => $data + ])->upload(); - return new Object($this->connection, $destination, $this->identity['bucket'], null, $response); + return new Object( + $this->connection, + $response['name'], + $this->identity['bucket'], + $response['generation'], + $response + ); } /** - * Upload + * Get a resumable uploader which can provide greater control over the + * upload process. This is recommended when dealing with large files where + * reliability is key. * * Example: * ``` + * $uploader = $bucket->getResumableUploader( + * fopen('image.jpg', 'r') + * ); + * + * try { + * $uploader->upload(); + * } catch (GoogleException $ex) { + * $resumeUri = $uploader->getResumeUri(); + * $uploader->resume($resumeUri); + * } * ``` * - * @param string $path Path to the file to be uploaded. - * @param array $options Configuration options. - * @return \Google\Gcloud\Storage\Object + * @see https://cloud.google.com/storage/docs/json_api/v1/how-tos/upload#resumable Learn more about resumable + * uploads. + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation. + * + * @param string|resource|StreamInterface $data The data to be uploaded. + * @param array $options { + * Configuration options. + * + * @type string $name The name of the destination. + * @type bool $validate Indicates whether or not validation will be + * applied using md5 hashing functionality. If true and the + * calculated hash does not match that of the upstream server the + * upload will be rejected. + * @type int $chunkSize If provided the upload will be done in chunks. + * The size must be in multiples of 262144 bytes. With chunking + * you have increased reliability at the risk of higher overhead. + * It is recommended to not use chunking. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Defaults to private. Acceptable values include + * authenticatedRead, bucketOwnerFullControl, bucketOwnerRead, + * private, projectPrivate, and publicRead. + * @type array $metadata The available options for metadata are outlined + * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request) + * } + * @return ResumableUploader + * @throws \InvalidArgumentException */ - public function uploadFromPath($path, array $options = []) + public function getResumableUploader($data, array $options = []) { + if (is_string($data) && !isset($options['name'])) { + throw new \InvalidArgumentException('A name is required when data is of type string.'); + } + return $this->connection->insertObject($options + [ + 'bucket' => $this->identity['bucket'], + 'data' => $data, + 'resumable' => true + ]); } /** * Lazily instantiates an object. There are no network requests made at this * point. To see the operations that can be performed on an object please - * see {@see Google\Gcloud\Storage\Object}. + * see {@see Google\Cloud\Storage\Object}. * * Example: * ``` @@ -191,7 +280,7 @@ public function uploadFromPath($path, array $options = []) * * @type string $generation Request a specific revision of the object. * } - * @return \Google\Gcloud\Storage\Object + * @return \Google\Cloud\Storage\Object */ public function object($name, array $options = []) { @@ -216,6 +305,8 @@ public function object($name, array $options = []) * } * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/list Objects list API documentation. + * * @param array $options { * Configuration options. * @@ -240,13 +331,14 @@ public function object($name, array $options = []) public function objects(array $options = []) { $options['pageToken'] = null; + $includeVersions = isset($options['versions']) ? $options['versions'] : false; do { $response = $this->connection->listObjects($options + $this->identity); foreach ($response['items'] as $object) { - // @todo when versions === true pass generation - yield new Object($this->connection, $object['name'], $this->identity['bucket'], null, $object); + $generation = $includeVersions ? $object['generation'] : null; + yield new Object($this->connection, $object['name'], $this->identity['bucket'], $generation, $object); } $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null; @@ -262,6 +354,8 @@ public function objects(array $options = []) * $bucket->delete(); * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/buckets/delete Buckets delete API documentation. + * * @param array $options { * Configuration options. * @type string $ifMetagenerationMatch If set, only deletes the bucket @@ -291,8 +385,8 @@ public function delete(array $options = []) * ]); * ``` * - * @see https://goo.gl/KgufNr Learn more about configuring request options - * at the bucket patch API documentation. + * @see https://cloud.google.com/storage/docs/json_api/v1/buckets/patch Buckets patch API documentation. + * * @param array $options { * Configuration options. * @@ -340,6 +434,8 @@ public function update(array $options = []) * echo $info['location']; * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/buckets/get Buckets get API documentation. + * * @param array $options { * Configuration options. * diff --git a/src/Storage/Connection/ConnectionInterface.php b/src/Storage/Connection/ConnectionInterface.php index 2b588c2064c3..713c25dd6a1f 100644 --- a/src/Storage/Connection/ConnectionInterface.php +++ b/src/Storage/Connection/ConnectionInterface.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Gcloud\Storage\Connection; +namespace Google\Cloud\Storage\Connection; /** * Represents a connection to @@ -24,82 +24,82 @@ interface ConnectionInterface { /** - * @param array $options + * @param array $args */ - public function deleteAcl(array $options = []); + public function deleteAcl(array $args = []); /** - * @param array $options + * @param array $args */ - public function getAcl(array $options = []); + public function getAcl(array $args = []); /** - * @param array $options + * @param array $args */ - public function listAcl(array $options = []); + public function listAcl(array $args = []); /** - * @param array $options + * @param array $args */ - public function insertAcl(array $options = []); + public function insertAcl(array $args = []); /** - * @param array $options + * @param array $args */ - public function patchAcl(array $options = []); + public function patchAcl(array $args = []); /** - * @param array $options + * @param array $args */ - public function deleteBucket(array $options = []); + public function deleteBucket(array $args = []); /** - * @param array $options + * @param array $args */ - public function getBucket(array $options = []); + public function getBucket(array $args = []); /** - * @param array $options + * @param array $args */ - public function listBuckets(array $options = []); + public function listBuckets(array $args = []); /** - * @param array $options + * @param array $args */ - public function createBucket(array $options = []); + public function insertBucket(array $args = []); /** - * @param array $options + * @param array $args */ - public function patchBucket(array $options = []); + public function patchBucket(array $args = []); /** - * @param array $options + * @param array $args */ - public function deleteObject(array $options = []); + public function deleteObject(array $args = []); /** - * @param array $options + * @param array $args */ - public function getObject(array $options = []); + public function getObject(array $args = []); /** - * @param array $options + * @param array $args */ - public function listObjects(array $options = []); + public function listObjects(array $args = []); /** - * @param array $options + * @param array $args */ - public function patchObject(array $options = []); + public function patchObject(array $args = []); /** - * @param array $options + * @param array $args */ - public function downloadObject(array $options = []); + public function downloadObject(array $args = []); /** - * @param array $options + * @param array $args */ - public function uploadObject(array $options = []); + public function insertObject(array $args = []); } diff --git a/src/Storage/Connection/REST.php b/src/Storage/Connection/REST.php deleted file mode 100644 index 26f8e99ea7fe..000000000000 --- a/src/Storage/Connection/REST.php +++ /dev/null @@ -1,299 +0,0 @@ -httpWrapper = $httpWrapper ?: new HttpRequestWrapper(); - $this->service = $this->loadServiceDefinition(); - } - - // @todo use __call to handle the methods below? - // @todo createBucket should be insertBucket - review names - /** - * @param array $config Configuration options. - */ - public function deleteAcl(array $options = []) - { - return $this->sendRequest($options['type'], 'delete', $options); - } - - /** - * @param array $config Configuration options. - */ - public function getAcl(array $options = []) - { - return $this->sendRequest($options['type'], 'get', $options); - } - - /** - * @param array $config Configuration options. - */ - public function listAcl(array $options = []) - { - return $this->sendRequest($options['type'], 'list', $options); - } - - /** - * @param array $config Configuration options. - */ - public function insertAcl(array $options = []) - { - return $this->sendRequest($options['type'], 'insert', $options); - } - - /** - * @param array $config Configuration options. - */ - public function patchAcl(array $options = []) - { - return $this->sendRequest($options['type'], 'patch', $options); - } - - /** - * @param array $config Configuration options. - */ - public function deleteBucket(array $options = []) - { - return $this->sendRequest('buckets', 'delete', $options); - } - - /** - * @param array $config Configuration options. - */ - public function getBucket(array $options = []) - { - return $this->sendRequest('buckets', 'get', $options); - } - - /** - * @param array $config Configuration options. - */ - public function listBuckets(array $options = []) - { - return $this->sendRequest('buckets', 'list', $options); - } - - /** - * @param array $config Configuration options. - */ - public function createBucket(array $options = []) - { - return $this->sendRequest('buckets', 'insert', $options); - } - - /** - * @param array $config Configuration options. - */ - public function patchBucket(array $options = []) - { - return $this->sendRequest('buckets', 'patch', $options); - } - - /** - * @param array $config Configuration options. - */ - public function deleteObject(array $options = []) - { - return $this->sendRequest('objects', 'delete', $options); - } - - /** - * @param array $config Configuration options. - */ - public function getObject(array $options = []) - { - return $this->sendRequest('objects', 'get', $options); - } - - /** - * @param array $config Configuration options. - */ - public function listObjects(array $options = []) - { - return $this->sendRequest('objects', 'list', $options); - } - - /** - * @param array $config Configuration options. - */ - public function patchObject(array $options = []) - { - return $this->sendRequest('objects', 'patch', $options); - } - - /** - * @param array $config Configuration options. - */ - public function downloadObject(array $options = []) - { - // @todo investigate using mediaLink to download versions - $template = new UriTemplate(); - $uri = $template->expand(self::DOWNLOAD_URI, [ - 'bucket' => $options['bucket'], - 'object' => $options['object'] - ]); - - return $this->httpWrapper->send( - new Request( - 'GET', - Psr7\uri_for($uri) - ) - )->getBody(); - } - - // @todo finish upload - /** - * @param array $config Configuration options. - */ - public function uploadObject(array $options = []) - { - $template = new UriTemplate(); - $headers = [ - 'Content-Type' => isset($options['contentType']) ? $options['contentType'] : 'application/octet-stream' - ]; - $uri = $this->buildUri( - $template->expand(self::UPLOAD_URI, ['bucket' => $options['bucket']]), - [ - 'uploadType' => 'media', - 'name' => $options['name'] - ] - ); - - return json_decode( - $this->httpWrapper->send( - new Request( - 'POST', - $uri, - $headers, - $options['data'] - ) - )->getBody(), - true - ); - } - - /** - * @return array - */ - private function loadServiceDefinition() - { - return json_decode( - file_get_contents(__DIR__ . '/ServiceDefinition/storage-v1.json', true), - true - ); - } - - /** - * @param string $resource - * @param string $method - * @param array $options - * @return array - */ - private function sendRequest($resource, $method, $options) - { - // @todo quick POC. need to tighten this up - $action = $this->service[$resource]['methods'][$method]; - $template = new UriTemplate(self::BASE_URI); - $path = []; - $query = []; - $body = []; - - foreach ($action['parameters'] as $parameter => $parameterOptions) { - if ($parameterOptions['location'] === 'path' && array_key_exists($parameter, $options)) { - $path[$parameter] = $options[$parameter]; - } - - if ($parameterOptions['location'] === 'query' && array_key_exists($parameter, $options)) { - $query[$parameter] = $options[$parameter]; - } - - if ($parameterOptions['location'] === 'body' && array_key_exists($parameter, $options)) { - $body[$parameter] = $options[$parameter]; - } - } - - $uri = $this->buildUri( - $template->expand($action['path'], $path), - $query - ); - - return json_decode( - $this->httpWrapper->send( - new Request( - $action['httpMethod'], - $uri, - ['Content-Type' => 'application/json'], - $body ? json_encode($body) : null - ) - )->getBody(), - true - ); - } - - /** - * @param string $uri - * @param array $query - * @return UriInterface - */ - private function buildUri($uri, array $query = []) - { - // @todo fix this hack. when using build_query booleans are converted to - // 1 or 0 which the API does not accept. this casts bools to their - // string representation - $query = array_filter($query); - foreach ($query as $k => &$v) { - if (is_bool($v)) { - $v = $v ? 'true' : 'false'; - } - } - - return Psr7\uri_for($uri)->withQuery(Psr7\build_query($query)); - } -} diff --git a/src/Storage/Connection/Rest.php b/src/Storage/Connection/Rest.php new file mode 100644 index 000000000000..3dc87324a802 --- /dev/null +++ b/src/Storage/Connection/Rest.php @@ -0,0 +1,282 @@ +setRequestWrapper(new RequestWrapper($config)); + $this->setRequestBuilder(new RequestBuilder( + __DIR__ . '/ServiceDefinition/storage-v1.json', + self::BASE_URI + )); + } + + /** + * @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 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 = []) + { + $args += [ + 'bucket' => null, + 'object' => null, + 'generation' => null + ]; + + $requestOptions = array_intersect_key($args, [ + 'httpOptions' => null, + 'retries' => null + ]); + + $uri = $this->expandUri(self::DOWNLOAD_URI, [ + 'bucket' => $args['bucket'], + 'object' => $args['object'], + 'query' => [ + 'generation' => $args['generation'], + 'alt' => 'media' + ] + ]); + + return $this->requestWrapper->send( + new Request('GET', Psr7\uri_for($uri)), + $requestOptions + )->getBody(); + } + + /** + * @param array $args + */ + public function insertObject(array $args = []) + { + $args = $this->resolveUploadOptions($args); + $isResumable = $args['resumable']; + + $uriParams = [ + 'bucket' => $args['bucket'], + 'query' => [ + 'predefinedAcl' => $args['predefinedAcl'], + 'uploadType' => $isResumable ? self::UPLOAD_TYPE_RESUMABLE : self::UPLOAD_TYPE_MULTIPART + ] + ]; + + if ($isResumable) { + return new ResumableUploader( + $this->requestWrapper, + $args['data'], + $this->expandUri(self::UPLOAD_URI, $uriParams), + $args['uploaderOptions'] + ); + } + + return new MultipartUploader( + $this->requestWrapper, + $args['data'], + $this->expandUri(self::UPLOAD_URI, $uriParams), + $args['uploaderOptions'] + ); + } + + /** + * @param array $args + */ + private function resolveUploadOptions(array $args) + { + $args += [ + 'bucket' => null, + 'name' => null, + 'validate' => true, + 'resumable' => null, + 'predefinedAcl' => 'private', + 'metadata' => [] + ]; + + $args['data'] = Psr7\stream_for($args['data']); + + if ($args['resumable'] === null) { + $args['resumable'] = $args['data']->getSize() > self::RESUMABLE_LIMIT; + } + + if (!$args['name']) { + $args['name'] = basename($args['data']->getMetadata('uri')); + } + + // @todo add support for rolling hash + if ($args['validate'] && !isset($args['metadata']['md5Hash'])) { + $args['metadata']['md5Hash'] = base64_encode(Psr7\hash($args['data'], 'md5', true)); + } + + $args['metadata']['name'] = $args['name']; + unset($args['name']); + $args['contentType'] = isset($args['metadata']['contentType']) + ? $args['metadata']['contentType'] + : null; + + $uploaderOptionKeys = [ + 'httpOptions', + 'retries', + 'chunkSize', + 'contentType', + 'metadata' + ]; + + $args['uploaderOptions'] = array_intersect_key($args, array_flip($uploaderOptionKeys)); + $args = array_diff_key($args, array_flip($uploaderOptionKeys)); + + return $args; + } +} diff --git a/src/Storage/Connection/ServiceDefinition/storage-v1.json b/src/Storage/Connection/ServiceDefinition/storage-v1.json index 2d93b255b21a..7519ba848fbc 100644 --- a/src/Storage/Connection/ServiceDefinition/storage-v1.json +++ b/src/Storage/Connection/ServiceDefinition/storage-v1.json @@ -1,753 +1,2817 @@ { - "bucketAccessControls": { - "methods": { - "delete": { - "path": "b/{bucket}/acl/{entity}", - "httpMethod": "DELETE", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" + "parameters": { + "alt": { + "type": "string", + "description": "Data format for the response.", + "default": "json", + "enum": [ + "json" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json" + ], + "location": "query" + }, + "fields": { + "type": "string", + "description": "Selector specifying which fields to include in a partial response.", + "location": "query" + }, + "key": { + "type": "string", + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location": "query" + }, + "oauth_token": { + "type": "string", + "description": "OAuth 2.0 token for the current user.", + "location": "query" + }, + "prettyPrint": { + "type": "boolean", + "description": "Returns response with indentations and line breaks.", + "default": "true", + "location": "query" + }, + "quotaUser": { + "type": "string", + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location": "query" + }, + "userIp": { + "type": "string", + "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location": "query" + } + }, + "schemas": { + "Bucket": { + "id": "Bucket", + "type": "object", + "description": "A bucket.", + "properties": { + "acl": { + "type": "array", + "description": "Access controls on the bucket.", + "items": { + "$ref": "BucketAccessControl" + }, + "annotations": { + "required": [ + "storage.buckets.update" + ] } - } - }, - "get": { - "path": "b/{bucket}/acl/{entity}", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" + }, + "cors": { + "type": "array", + "description": "The bucket's Cross-Origin Resource Sharing (CORS) configuration.", + "items": { + "type": "object", + "properties": { + "maxAgeSeconds": { + "type": "integer", + "description": "The value, in seconds, to return in the Access-Control-Max-Age header used in preflight responses.", + "format": "int32" + }, + "method": { + "type": "array", + "description": "The list of HTTP methods on which to include CORS response headers, (GET, OPTIONS, POST, etc) Note: \"*\" is permitted in the list of methods, and means \"any method\".", + "items": { + "type": "string" + } + }, + "origin": { + "type": "array", + "description": "The list of Origins eligible to receive CORS response headers. Note: \"*\" is permitted in the list of origins, and means \"any Origin\".", + "items": { + "type": "string" + } + }, + "responseHeader": { + "type": "array", + "description": "The list of HTTP headers other than the simple response headers to give permission for the user-agent to share across domains.", + "items": { + "type": "string" + } + } + } } - } - }, - "insert": { - "path": "b/{bucket}/acl", - "httpMethod": "POST", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "body" - }, - "role": { - "type": "string", - "required": true, - "location": "body" + }, + "defaultObjectAcl": { + "type": "array", + "description": "Default access controls to apply to new objects when no ACL is provided.", + "items": { + "$ref": "ObjectAccessControl" } - } - }, - "list": { - "path": "b/{bucket}/acl", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" + }, + "etag": { + "type": "string", + "description": "HTTP 1.1 Entity tag for the bucket." + }, + "id": { + "type": "string", + "description": "The ID of the bucket." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For buckets, this is always storage#bucket.", + "default": "storage#bucket" + }, + "lifecycle": { + "type": "object", + "description": "The bucket's lifecycle configuration. See lifecycle management for more information.", + "properties": { + "rule": { + "type": "array", + "description": "A lifecycle management rule, which is made of an action to take and the condition(s) under which the action will be taken.", + "items": { + "type": "object", + "properties": { + "action": { + "type": "object", + "description": "The action to take.", + "properties": { + "type": { + "type": "string", + "description": "Type of the action. Currently, only Delete is supported." + } + } + }, + "condition": { + "type": "object", + "description": "The condition(s) under which the action will be taken.", + "properties": { + "age": { + "type": "integer", + "description": "Age of an object (in days). This condition is satisfied when an object reaches the specified age.", + "format": "int32" + }, + "createdBefore": { + "type": "string", + "description": "A date in RFC 3339 format with only the date part (for instance, \"2013-01-15\"). This condition is satisfied when an object is created before midnight of the specified date in UTC.", + "format": "date" + }, + "isLive": { + "type": "boolean", + "description": "Relevant only for versioned objects. If the value is true, this condition matches live objects; if the value is false, it matches archived objects." + }, + "numNewerVersions": { + "type": "integer", + "description": "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.", + "format": "int32" + } + } + } + } + } + } } - } - }, - "patch": { - "path": "b/{bucket}/acl/{entity}", - "httpMethod": "PATCH", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" - }, - "role": { - "type": "string", - "location": "body" + }, + "location": { + "type": "string", + "description": "The location of the bucket. Object data for objects in the bucket resides in physical storage within this region. Defaults to US. See the developer's guide for the authoritative list." + }, + "logging": { + "type": "object", + "description": "The bucket's logging configuration, which defines the destination bucket and optional name prefix for the current bucket's logs.", + "properties": { + "logBucket": { + "type": "string", + "description": "The destination bucket where the current bucket's logs should be placed." + }, + "logObjectPrefix": { + "type": "string", + "description": "A prefix for log object names." + } + } + }, + "metageneration": { + "type": "string", + "description": "The metadata generation of this bucket.", + "format": "int64" + }, + "name": { + "type": "string", + "description": "The name of the bucket.", + "annotations": { + "required": [ + "storage.buckets.insert" + ] + } + }, + "owner": { + "type": "object", + "description": "The owner of the bucket. This is always the project team's owner group.", + "properties": { + "entity": { + "type": "string", + "description": "The entity, in the form project-owner-projectId." + }, + "entityId": { + "type": "string", + "description": "The ID for the entity." + } + } + }, + "projectNumber": { + "type": "string", + "description": "The project number of the project the bucket belongs to.", + "format": "uint64" + }, + "selfLink": { + "type": "string", + "description": "The URI of this bucket." + }, + "storageClass": { + "type": "string", + "description": "The bucket's storage class. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include STANDARD, NEARLINE and DURABLE_REDUCED_AVAILABILITY. Defaults to STANDARD. For more information, see storage classes." + }, + "timeCreated": { + "type": "string", + "description": "The creation time of the bucket in RFC 3339 format.", + "format": "date-time" + }, + "updated": { + "type": "string", + "description": "The modification time of the bucket in RFC 3339 format.", + "format": "date-time" + }, + "versioning": { + "type": "object", + "description": "The bucket's versioning configuration.", + "properties": { + "enabled": { + "type": "boolean", + "description": "While set to true, versioning is fully enabled for this bucket." + } + } + }, + "website": { + "type": "object", + "description": "The bucket's website configuration.", + "properties": { + "mainPageSuffix": { + "type": "string", + "description": "Behaves as the bucket's directory index where missing objects are treated as potential directories." + }, + "notFoundPage": { + "type": "string", + "description": "The custom object to return when a requested resource is not found." + } } } } - } - }, - "buckets": { - "methods": { - "delete": { - "path": "b/{bucket}", - "httpMethod": "DELETE", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" + }, + "BucketAccessControl": { + "id": "BucketAccessControl", + "type": "object", + "description": "An access-control entry.", + "properties": { + "bucket": { + "type": "string", + "description": "The name of the bucket." + }, + "domain": { + "type": "string", + "description": "The domain associated with the entity, if any." + }, + "email": { + "type": "string", + "description": "The email address associated with the entity, if any." + }, + "entity": { + "type": "string", + "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com.", + "annotations": { + "required": [ + "storage.bucketAccessControls.insert" + ] } - } - }, - "get": { - "path": "b/{bucket}", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" + }, + "entityId": { + "type": "string", + "description": "The ID for the entity, if any." + }, + "etag": { + "type": "string", + "description": "HTTP 1.1 Entity tag for the access-control entry." + }, + "id": { + "type": "string", + "description": "The ID of the access-control entry." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For bucket access control entries, this is always storage#bucketAccessControl.", + "default": "storage#bucketAccessControl" + }, + "projectTeam": { + "type": "object", + "description": "The project team associated with the entity, if any.", + "properties": { + "projectNumber": { + "type": "string", + "description": "The project number." + }, + "team": { + "type": "string", + "description": "The team. Can be owners, editors, or viewers." + } } - } - }, - "insert": { - "path": "b", - "httpMethod": "POST", - "parameters": { - "predefinedAcl": { - "type": "string", - "location": "query" - }, - "predefinedDefaultObjectAcl": { - "type": "string", - "location": "query" - }, - "project": { - "type": "string", - "required": true, - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" - }, - "name": { - "type": "string", - "required": true, - "location": "body" - }, - "acl": { - "type": "array", - "location": "body" - }, - "cors": { - "type": "array", - "location": "body" - }, - "defaultObjectAcl": { - "type": "array", - "location": "body" - }, - "lifecycle": { - "type": "object", - "location": "body" - }, - "location": { - "type": "string", - "location": "body" - }, - "logging": { - "type": "object", - "location": "body" - }, - "storageClass": { - "type": "string", - "location": "body" - }, - "versioning": { - "type": "object", - "location": "body" - }, - "website": { - "type": "object", - "location": "body" + }, + "role": { + "type": "string", + "description": "The access permission for the entity. Can be READER, WRITER, or OWNER.", + "annotations": { + "required": [ + "storage.bucketAccessControls.insert" + ] } + }, + "selfLink": { + "type": "string", + "description": "The link to this access-control entry." } - }, - "list": { - "path": "b", - "httpMethod": "GET", - "parameters": { - "maxResults": { - "type": "integer", - "minimum": "0", - "location": "query" - }, - "pageToken": { - "type": "string", - "location": "query" - }, - "prefix": { - "type": "string", - "location": "query" - }, - "project": { - "type": "string", - "required": true, - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" + } + }, + "BucketAccessControls": { + "id": "BucketAccessControls", + "type": "object", + "description": "An access-control list.", + "properties": { + "items": { + "type": "array", + "description": "The list of items.", + "items": { + "$ref": "BucketAccessControl" } + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For lists of bucket access control entries, this is always storage#bucketAccessControls.", + "default": "storage#bucketAccessControls" } - }, - "patch": { - "path": "b/{bucket}", - "httpMethod": "PATCH", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "predefinedAcl": { - "type": "string", - "location": "query" - }, - "predefinedDefaultObjectAcl": { - "type": "string", - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" - }, - "acl": { - "type": "array", - "location": "body" - }, - "cors": { - "type": "array", - "location": "body" - }, - "defaultObjectAcl": { - "type": "array", - "location": "body" - }, - "lifecycle": { - "type": "object", - "location": "body" - }, - "logging": { - "type": "object", - "location": "body" - }, - "versioning": { - "type": "object", - "location": "body" - }, - "website": { - "type": "object", - "location": "body" + } + }, + "Buckets": { + "id": "Buckets", + "type": "object", + "description": "A list of buckets.", + "properties": { + "items": { + "type": "array", + "description": "The list of items.", + "items": { + "$ref": "Bucket" } + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For lists of buckets, this is always storage#buckets.", + "default": "storage#buckets" + }, + "nextPageToken": { + "type": "string", + "description": "The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results." } } - } - }, - "defaultObjectAccessControls": { - "methods": { - "delete": { - "path": "b/{bucket}/defaultObjectAcl/{entity}", - "httpMethod": "DELETE", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" + }, + "Channel": { + "id": "Channel", + "type": "object", + "description": "An notification channel used to watch for resource changes.", + "properties": { + "address": { + "type": "string", + "description": "The address where notifications are delivered for this channel." + }, + "expiration": { + "type": "string", + "description": "Date and time of notification channel expiration, expressed as a Unix timestamp, in milliseconds. Optional.", + "format": "int64" + }, + "id": { + "type": "string", + "description": "A UUID or similar unique string that identifies this channel." + }, + "kind": { + "type": "string", + "description": "Identifies this as a notification channel used to watch for changes to a resource. Value: the fixed string \"api#channel\".", + "default": "api#channel" + }, + "params": { + "type": "object", + "description": "Additional parameters controlling delivery channel behavior. Optional.", + "additionalProperties": { + "type": "string", + "description": "Declares a new parameter by name." } + }, + "payload": { + "type": "boolean", + "description": "A Boolean value to indicate whether payload is wanted. Optional." + }, + "resourceId": { + "type": "string", + "description": "An opaque ID that identifies the resource being watched on this channel. Stable across different API versions." + }, + "resourceUri": { + "type": "string", + "description": "A version-specific identifier for the watched resource." + }, + "token": { + "type": "string", + "description": "An arbitrary string delivered to the target address with each notification delivered over this channel. Optional." + }, + "type": { + "type": "string", + "description": "The type of delivery mechanism used for this channel." } - }, - "get": { - "path": "b/{bucket}/defaultObjectAcl/{entity}", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" + } + }, + "ComposeRequest": { + "id": "ComposeRequest", + "type": "object", + "description": "A Compose request.", + "properties": { + "destination": { + "$ref": "Object", + "description": "Properties of the resulting object." + }, + "kind": { + "type": "string", + "description": "The kind of item this is.", + "default": "storage#composeRequest" + }, + "sourceObjects": { + "type": "array", + "description": "The list of source objects that will be concatenated into a single object.", + "items": { + "type": "object", + "properties": { + "generation": { + "type": "string", + "description": "The generation of this object to use as the source.", + "format": "int64" + }, + "name": { + "type": "string", + "description": "The source object's name. The source object's bucket is implicitly the destination bucket.", + "annotations": { + "required": [ + "storage.objects.compose" + ] + } + }, + "objectPreconditions": { + "type": "object", + "description": "Conditions that must be met for this operation to execute.", + "properties": { + "ifGenerationMatch": { + "type": "string", + "description": "Only perform the composition if the generation of the source object that would be used matches this value. If this value and a generation are both specified, they must be the same value or the call will fail.", + "format": "int64" + } + } + } + } + }, + "annotations": { + "required": [ + "storage.objects.compose" + ] } } - }, - "insert": { - "path": "b/{bucket}/defaultObjectAcl", - "httpMethod": "POST", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "body" - }, - "role": { - "type": "string", - "required": true, - "location": "body" + } + }, + "Object": { + "id": "Object", + "type": "object", + "description": "An object.", + "properties": { + "acl": { + "type": "array", + "description": "Access controls on the object.", + "items": { + "$ref": "ObjectAccessControl" + }, + "annotations": { + "required": [ + "storage.objects.update" + ] } - } - }, - "list": { - "path": "b/{bucket}/defaultObjectAcl", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" + }, + "bucket": { + "type": "string", + "description": "The name of the bucket containing this object." + }, + "cacheControl": { + "type": "string", + "description": "Cache-Control directive for the object data." + }, + "componentCount": { + "type": "integer", + "description": "Number of underlying components that make up this object. Components are accumulated by compose operations.", + "format": "int32" + }, + "contentDisposition": { + "type": "string", + "description": "Content-Disposition of the object data." + }, + "contentEncoding": { + "type": "string", + "description": "Content-Encoding of the object data." + }, + "contentLanguage": { + "type": "string", + "description": "Content-Language of the object data." + }, + "contentType": { + "type": "string", + "description": "Content-Type of the object data." + }, + "crc32c": { + "type": "string", + "description": "CRC32c checksum, as described in RFC 4960, Appendix B; encoded using base64 in big-endian byte order. For more information about using the CRC32c checksum, see Hashes and ETags: Best Practices." + }, + "customerEncryption": { + "type": "object", + "description": "Metadata of customer-supplied encryption key, if the object is encrypted by such a key.", + "properties": { + "encryptionAlgorithm": { + "type": "string", + "description": "The encryption algorithm." + }, + "keySha256": { + "type": "string", + "description": "SHA256 hash value of the encryption key." + } } - } - }, - "patch": { - "path": "b/{bucket}/defaultObjectAcl/{entity}", - "httpMethod": "PATCH", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" - }, - "role": { - "type": "string", - "location": "body" + }, + "etag": { + "type": "string", + "description": "HTTP 1.1 Entity tag for the object." + }, + "generation": { + "type": "string", + "description": "The content generation of this object. Used for object versioning.", + "format": "int64" + }, + "id": { + "type": "string", + "description": "The ID of the object." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For objects, this is always storage#object.", + "default": "storage#object" + }, + "md5Hash": { + "type": "string", + "description": "MD5 hash of the data; encoded using base64. For more information about using the MD5 hash, see Hashes and ETags: Best Practices." + }, + "mediaLink": { + "type": "string", + "description": "Media download link." + }, + "metadata": { + "type": "object", + "description": "User-provided metadata, in key/value pairs.", + "additionalProperties": { + "type": "string", + "description": "An individual metadata entry." + } + }, + "metageneration": { + "type": "string", + "description": "The version of the metadata for this object at this generation. Used for preconditions and for detecting changes in metadata. A metageneration number is only meaningful in the context of a particular generation of a particular object.", + "format": "int64" + }, + "name": { + "type": "string", + "description": "The name of this object. Required if not specified by URL parameter." + }, + "owner": { + "type": "object", + "description": "The owner of the object. This will always be the uploader of the object.", + "properties": { + "entity": { + "type": "string", + "description": "The entity, in the form user-userId." + }, + "entityId": { + "type": "string", + "description": "The ID for the entity." + } } + }, + "selfLink": { + "type": "string", + "description": "The link to this object." + }, + "size": { + "type": "string", + "description": "Content-Length of the data in bytes.", + "format": "uint64" + }, + "storageClass": { + "type": "string", + "description": "Storage class of the object." + }, + "timeCreated": { + "type": "string", + "description": "The creation time of the object in RFC 3339 format.", + "format": "date-time" + }, + "timeDeleted": { + "type": "string", + "description": "The deletion time of the object in RFC 3339 format. Will be returned if and only if this version of the object has been deleted.", + "format": "date-time" + }, + "updated": { + "type": "string", + "description": "The modification time of the object metadata in RFC 3339 format.", + "format": "date-time" } } - } - }, - "objectAccessControls": { - "methods": { - "delete": { - "path": "b/{bucket}/o/{object}/acl/{entity}", - "httpMethod": "DELETE", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" + }, + "ObjectAccessControl": { + "id": "ObjectAccessControl", + "type": "object", + "description": "An access-control entry.", + "properties": { + "bucket": { + "type": "string", + "description": "The name of the bucket." + }, + "domain": { + "type": "string", + "description": "The domain associated with the entity, if any." + }, + "email": { + "type": "string", + "description": "The email address associated with the entity, if any." + }, + "entity": { + "type": "string", + "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com." + }, + "entityId": { + "type": "string", + "description": "The ID for the entity, if any." + }, + "etag": { + "type": "string", + "description": "HTTP 1.1 Entity tag for the access-control entry." + }, + "generation": { + "type": "string", + "description": "The content generation of the object.", + "format": "int64" + }, + "id": { + "type": "string", + "description": "The ID of the access-control entry." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For object access control entries, this is always storage#objectAccessControl.", + "default": "storage#objectAccessControl" + }, + "object": { + "type": "string", + "description": "The name of the object." + }, + "projectTeam": { + "type": "object", + "description": "The project team associated with the entity, if any.", + "properties": { + "projectNumber": { + "type": "string", + "description": "The project number." + }, + "team": { + "type": "string", + "description": "The team. Can be owners, editors, or viewers." + } } + }, + "role": { + "type": "string", + "description": "The access permission for the entity. Can be READER or OWNER." + }, + "selfLink": { + "type": "string", + "description": "The link to this access-control entry." } - }, - "get": { - "path": "b/{bucket}/o/{object}/acl/{entity}", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" + } + }, + "ObjectAccessControls": { + "id": "ObjectAccessControls", + "type": "object", + "description": "An access-control list.", + "properties": { + "items": { + "type": "array", + "description": "The list of items.", + "items": { + "type": "any" } + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For lists of object access control entries, this is always storage#objectAccessControls.", + "default": "storage#objectAccessControls" } - }, - "insert": { - "path": "b/{bucket}/o/{object}/acl", - "httpMethod": "POST", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "body" - }, - "role": { - "type": "string", - "required": true, - "location": "body" + } + }, + "Objects": { + "id": "Objects", + "type": "object", + "description": "A list of objects.", + "properties": { + "items": { + "type": "array", + "description": "The list of items.", + "items": { + "$ref": "Object" } - } - }, - "list": { - "path": "b/{bucket}/o/{object}/acl", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For lists of objects, this is always storage#objects.", + "default": "storage#objects" + }, + "nextPageToken": { + "type": "string", + "description": "The continuation token, used to page through large result sets. Provide this value in a subsequent request to return the next page of results." + }, + "prefixes": { + "type": "array", + "description": "The list of prefixes of objects matching-but-not-listed up to and including the requested delimiter.", + "items": { + "type": "string" } } - }, - "patch": { - "path": "b/{bucket}/o/{object}/acl/{entity}", - "httpMethod": "PATCH", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "entity": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "format": "int64", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" - }, - "role": { - "type": "string", - "location": "body" - } + } + }, + "RewriteResponse": { + "id": "RewriteResponse", + "type": "object", + "description": "A rewrite response.", + "properties": { + "done": { + "type": "boolean", + "description": "true if the copy is finished; otherwise, false if the copy is in progress. This property is always present in the response." + }, + "kind": { + "type": "string", + "description": "The kind of item this is.", + "default": "storage#rewriteResponse" + }, + "objectSize": { + "type": "string", + "description": "The total size of the object being copied in bytes. This property is always present in the response.", + "format": "uint64" + }, + "resource": { + "$ref": "Object", + "description": "A resource containing the metadata for the copied-to object. This property is present in the response only when copying completes." + }, + "rewriteToken": { + "type": "string", + "description": "A token to use in subsequent requests to continue copying data. This token is present in the response only when there is more data to copy." + }, + "totalBytesRewritten": { + "type": "string", + "description": "The total bytes written so far, which can be used to provide a waiting user with a progress indicator. This property is always present in the response.", + "format": "uint64" } } } }, - "objects": { - "methods": { - "delete": { - "path": "b/{bucket}/o/{object}", - "httpMethod": "DELETE", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "ifGenerationMatch": { - "type": "string", - "location": "query" - }, - "ifGenerationNotMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" - } + "resources": { + "bucketAccessControls": { + "methods": { + "delete": { + "id": "storage.bucketAccessControls.delete", + "path": "b/{bucket}/acl/{entity}", + "httpMethod": "DELETE", + "description": "Permanently deletes the ACL entry for the specified entity on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "get": { + "id": "storage.bucketAccessControls.get", + "path": "b/{bucket}/acl/{entity}", + "httpMethod": "GET", + "description": "Returns the ACL entry for the specified entity on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "response": { + "$ref": "BucketAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "insert": { + "id": "storage.bucketAccessControls.insert", + "path": "b/{bucket}/acl", + "httpMethod": "POST", + "description": "Creates a new ACL entry on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "BucketAccessControl" + }, + "response": { + "$ref": "BucketAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "list": { + "id": "storage.bucketAccessControls.list", + "path": "b/{bucket}/acl", + "httpMethod": "GET", + "description": "Retrieves ACL entries on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket" + ], + "response": { + "$ref": "BucketAccessControls" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "patch": { + "id": "storage.bucketAccessControls.patch", + "path": "b/{bucket}/acl/{entity}", + "httpMethod": "PATCH", + "description": "Updates an ACL entry on the specified bucket. This method supports patch semantics.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "request": { + "$ref": "BucketAccessControl" + }, + "response": { + "$ref": "BucketAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "update": { + "id": "storage.bucketAccessControls.update", + "path": "b/{bucket}/acl/{entity}", + "httpMethod": "PUT", + "description": "Updates an ACL entry on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "request": { + "$ref": "BucketAccessControl" + }, + "response": { + "$ref": "BucketAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] } - }, - "get": { - "path": "b/{bucket}/o/{object}", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "ifGenerationMatch": { - "type": "string", - "location": "query" - }, - "ifGenerationNotMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" - }, - "projection": { - "type": "string", - "location": "query" - } + } + }, + "buckets": { + "methods": { + "delete": { + "id": "storage.buckets.delete", + "path": "b/{bucket}", + "httpMethod": "DELETE", + "description": "Permanently deletes an empty bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "If set, only deletes the bucket if its metageneration matches this value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "If set, only deletes the bucket if its metageneration does not match this value.", + "format": "int64", + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "get": { + "id": "storage.buckets.get", + "path": "b/{bucket}", + "httpMethod": "GET", + "description": "Returns metadata for the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit acl and defaultObjectAcl properties." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "response": { + "$ref": "Bucket" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "insert": { + "id": "storage.buckets.insert", + "path": "b", + "httpMethod": "POST", + "description": "Creates a new bucket.", + "parameters": { + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this bucket.", + "enum": [ + "authenticatedRead", + "private", + "projectPrivate", + "publicRead", + "publicReadWrite" + ], + "enumDescriptions": [ + "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", + "Project team owners get OWNER access.", + "Project team members get access according to their roles.", + "Project team owners get OWNER access, and allUsers get READER access.", + "Project team owners get OWNER access, and allUsers get WRITER access." + ], + "location": "query" + }, + "predefinedDefaultObjectAcl": { + "type": "string", + "description": "Apply a predefined set of default object access controls to this bucket.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "project": { + "type": "string", + "description": "A valid API project identifier.", + "required": true, + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl, unless the bucket resource specifies acl or defaultObjectAcl properties, when it defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit acl and defaultObjectAcl properties." + ], + "location": "query" + } + }, + "parameterOrder": [ + "project" + ], + "request": { + "$ref": "Bucket" + }, + "response": { + "$ref": "Bucket" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "list": { + "id": "storage.buckets.list", + "path": "b", + "httpMethod": "GET", + "description": "Retrieves a list of buckets for a given project.", + "parameters": { + "maxResults": { + "type": "integer", + "description": "Maximum number of buckets to return.", + "format": "uint32", + "minimum": "0", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "A previously-returned page token representing part of the larger set of results to view.", + "location": "query" + }, + "prefix": { + "type": "string", + "description": "Filter results to buckets whose names begin with this prefix.", + "location": "query" + }, + "project": { + "type": "string", + "description": "A valid API project identifier.", + "required": true, + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit acl and defaultObjectAcl properties." + ], + "location": "query" + } + }, + "parameterOrder": [ + "project" + ], + "response": { + "$ref": "Buckets" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "patch": { + "id": "storage.buckets.patch", + "path": "b/{bucket}", + "httpMethod": "PATCH", + "description": "Updates a bucket. This method supports patch semantics.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this bucket.", + "enum": [ + "authenticatedRead", + "private", + "projectPrivate", + "publicRead", + "publicReadWrite" + ], + "enumDescriptions": [ + "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", + "Project team owners get OWNER access.", + "Project team members get access according to their roles.", + "Project team owners get OWNER access, and allUsers get READER access.", + "Project team owners get OWNER access, and allUsers get WRITER access." + ], + "location": "query" + }, + "predefinedDefaultObjectAcl": { + "type": "string", + "description": "Apply a predefined set of default object access controls to this bucket.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit acl and defaultObjectAcl properties." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "Bucket" + }, + "response": { + "$ref": "Bucket" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "update": { + "id": "storage.buckets.update", + "path": "b/{bucket}", + "httpMethod": "PUT", + "description": "Updates a bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this bucket.", + "enum": [ + "authenticatedRead", + "private", + "projectPrivate", + "publicRead", + "publicReadWrite" + ], + "enumDescriptions": [ + "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", + "Project team owners get OWNER access.", + "Project team members get access according to their roles.", + "Project team owners get OWNER access, and allUsers get READER access.", + "Project team owners get OWNER access, and allUsers get WRITER access." + ], + "location": "query" + }, + "predefinedDefaultObjectAcl": { + "type": "string", + "description": "Apply a predefined set of default object access controls to this bucket.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit acl and defaultObjectAcl properties." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "Bucket" + }, + "response": { + "$ref": "Bucket" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] } - }, - "insert": { - "path": "b/{bucket}/o", - "httpMethod": "POST", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "contentEncoding": { - "type": "string", - "location": "query" - }, - "ifGenerationMatch": { - "type": "string", - "location": "query" - }, - "ifGenerationNotMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "name": { - "type": "string", - "location": "query" - }, - "predefinedAcl": { - "type": "string", - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" - } + } + }, + "channels": { + "methods": { + "stop": { + "id": "storage.channels.stop", + "path": "channels/stop", + "httpMethod": "POST", + "description": "Stop watching resources through this channel", + "request": { + "$ref": "Channel", + "parameterName": "resource" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ] } - }, - "list": { - "path": "b/{bucket}/o", - "httpMethod": "GET", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "delimiter": { - "type": "string", - "location": "query" - }, - "maxResults": { - "type": "integer", - "location": "query" - }, - "pageToken": { - "type": "string", - "location": "query" - }, - "prefix": { - "type": "string", - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" - }, - "versions": { - "type": "boolean", - "location": "query" - } + } + }, + "defaultObjectAccessControls": { + "methods": { + "delete": { + "id": "storage.defaultObjectAccessControls.delete", + "path": "b/{bucket}/defaultObjectAcl/{entity}", + "httpMethod": "DELETE", + "description": "Permanently deletes the default object ACL entry for the specified entity on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "get": { + "id": "storage.defaultObjectAccessControls.get", + "path": "b/{bucket}/defaultObjectAcl/{entity}", + "httpMethod": "GET", + "description": "Returns the default object ACL entry for the specified entity on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "insert": { + "id": "storage.defaultObjectAccessControls.insert", + "path": "b/{bucket}/defaultObjectAcl", + "httpMethod": "POST", + "description": "Creates a new default object ACL entry on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "list": { + "id": "storage.defaultObjectAccessControls.list", + "path": "b/{bucket}/defaultObjectAcl", + "httpMethod": "GET", + "description": "Retrieves default object ACL entries on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "If present, only return default ACL listing if the bucket's current metageneration matches this value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "If present, only return default ACL listing if the bucket's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "response": { + "$ref": "ObjectAccessControls" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "patch": { + "id": "storage.defaultObjectAccessControls.patch", + "path": "b/{bucket}/defaultObjectAcl/{entity}", + "httpMethod": "PATCH", + "description": "Updates a default object ACL entry on the specified bucket. This method supports patch semantics.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "update": { + "id": "storage.defaultObjectAccessControls.update", + "path": "b/{bucket}/defaultObjectAcl/{entity}", + "httpMethod": "PUT", + "description": "Updates a default object ACL entry on the specified bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "entity" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] } - }, - "patch": { - "path": "b/{bucket}/o/{object}", - "httpMethod": "PATCH", - "parameters": { - "bucket": { - "type": "string", - "required": true, - "location": "path" - }, - "generation": { - "type": "string", - "location": "query" - }, - "ifGenerationMatch": { - "type": "string", - "location": "query" - }, - "ifGenerationNotMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationMatch": { - "type": "string", - "location": "query" - }, - "ifMetagenerationNotMatch": { - "type": "string", - "location": "query" - }, - "object": { - "type": "string", - "required": true, - "location": "path" - }, - "predefinedAcl": { - "type": "string", - "location": "query" - }, - "projection": { - "type": "string", - "location": "query" - }, - "acl": { - "type": "array", - "location": "body" - }, - "cacheControl": { - "type": "string", - "location": "body" - }, - "contentDisposition": { - "type": "string", - "location": "body" - }, - "contentEncoding": { - "type": "string", - "location": "body" - }, - "contentLanguage": { - "type": "string", - "location": "body" - }, - "contentType": { - "type": "string", - "location": "body" - }, - "crc32c": { - "type": "string", - "location": "body" - }, - "md5Hash": { - "type": "string", - "location": "body" - }, - "metadata": { - "type": "object", - "location": "body" + } + }, + "objectAccessControls": { + "methods": { + "delete": { + "id": "storage.objectAccessControls.delete", + "path": "b/{bucket}/o/{object}/acl/{entity}", + "httpMethod": "DELETE", + "description": "Permanently deletes the ACL entry for the specified entity on the specified object.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object", + "entity" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "get": { + "id": "storage.objectAccessControls.get", + "path": "b/{bucket}/o/{object}/acl/{entity}", + "httpMethod": "GET", + "description": "Returns the ACL entry for the specified entity on the specified object.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object", + "entity" + ], + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "insert": { + "id": "storage.objectAccessControls.insert", + "path": "b/{bucket}/o/{object}/acl", + "httpMethod": "POST", + "description": "Creates a new ACL entry on the specified object.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "list": { + "id": "storage.objectAccessControls.list", + "path": "b/{bucket}/o/{object}/acl", + "httpMethod": "GET", + "description": "Retrieves ACL entries on the specified object.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "response": { + "$ref": "ObjectAccessControls" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "patch": { + "id": "storage.objectAccessControls.patch", + "path": "b/{bucket}/o/{object}/acl/{entity}", + "httpMethod": "PATCH", + "description": "Updates an ACL entry on the specified object. This method supports patch semantics.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object", + "entity" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + }, + "update": { + "id": "storage.objectAccessControls.update", + "path": "b/{bucket}/o/{object}/acl/{entity}", + "httpMethod": "PUT", + "description": "Updates an ACL entry on the specified object.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a bucket.", + "required": true, + "location": "path" + }, + "entity": { + "type": "string", + "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object", + "entity" + ], + "request": { + "$ref": "ObjectAccessControl" + }, + "response": { + "$ref": "ObjectAccessControl" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control" + ] + } + } + }, + "objects": { + "methods": { + "compose": { + "id": "storage.objects.compose", + "path": "b/{destinationBucket}/o/{destinationObject}/compose", + "httpMethod": "POST", + "description": "Concatenates a list of existing objects into a new object in the same bucket.", + "parameters": { + "destinationBucket": { + "type": "string", + "description": "Name of the bucket in which to store the new object.", + "required": true, + "location": "path" + }, + "destinationObject": { + "type": "string", + "description": "Name of the new object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "destinationPredefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to the destination object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + } + }, + "parameterOrder": [ + "destinationBucket", + "destinationObject" + ], + "request": { + "$ref": "ComposeRequest" + }, + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsMediaDownload": true, + "useMediaDownloadService": true + }, + "copy": { + "id": "storage.objects.copy", + "path": "b/{sourceBucket}/o/{sourceObject}/copyTo/b/{destinationBucket}/o/{destinationObject}", + "httpMethod": "POST", + "description": "Copies a source object to a destination object. Optionally overrides metadata.", + "parameters": { + "destinationBucket": { + "type": "string", + "description": "Name of the bucket in which to store the new object. Overrides the provided object metadata's bucket value, if any.For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "destinationObject": { + "type": "string", + "description": "Name of the new object. Required when the object metadata is not otherwise provided. Overrides the object metadata's name value, if any.", + "required": true, + "location": "path" + }, + "destinationPredefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to the destination object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + }, + "sourceBucket": { + "type": "string", + "description": "Name of the bucket in which to find the source object.", + "required": true, + "location": "path" + }, + "sourceGeneration": { + "type": "string", + "description": "If present, selects a specific revision of the source object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "sourceObject": { + "type": "string", + "description": "Name of the source object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "sourceBucket", + "sourceObject", + "destinationBucket", + "destinationObject" + ], + "request": { + "$ref": "Object" + }, + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsMediaDownload": true, + "useMediaDownloadService": true + }, + "delete": { + "id": "storage.objects.delete", + "path": "b/{bucket}/o/{object}", + "httpMethod": "DELETE", + "description": "Deletes an object and its metadata. Deletions are permanent if versioning is not enabled for the bucket, or if the generation parameter is used.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which the object resides.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, permanently deletes a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "get": { + "id": "storage.objects.get", + "path": "b/{bucket}/o/{object}", + "httpMethod": "GET", + "description": "Retrieves an object or its metadata.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which the object resides.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsMediaDownload": true, + "useMediaDownloadService": true + }, + "insert": { + "id": "storage.objects.insert", + "path": "b/{bucket}/o", + "httpMethod": "POST", + "description": "Stores a new object and metadata.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which to store the new object. Overrides the provided object metadata's bucket value, if any.", + "required": true, + "location": "path" + }, + "contentEncoding": { + "type": "string", + "description": "If set, sets the contentEncoding property of the final object to this value. Setting this parameter is equivalent to setting the contentEncoding metadata property. This can be useful when uploading an object with uploadType=media to indicate the encoding of the content being uploaded.", + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "name": { + "type": "string", + "description": "Name of the object. Required when the object metadata is not otherwise provided. Overrides the object metadata's name value, if any. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "location": "query" + }, + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "Object" + }, + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsMediaDownload": true, + "useMediaDownloadService": true, + "supportsMediaUpload": true, + "mediaUpload": { + "accept": [ + "*/*" + ], + "protocols": { + "simple": { + "multipart": true, + "path": "/upload/storage/v1/b/{bucket}/o" + }, + "resumable": { + "multipart": true, + "path": "/resumable/upload/storage/v1/b/{bucket}/o" + } + } } + }, + "list": { + "id": "storage.objects.list", + "path": "b/{bucket}/o", + "httpMethod": "GET", + "description": "Retrieves a list of objects matching the criteria.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which to look for objects.", + "required": true, + "location": "path" + }, + "delimiter": { + "type": "string", + "description": "Returns results in a directory-like mode. items will contain only objects whose names, aside from the prefix, do not contain delimiter. Objects whose names, aside from the prefix, contain delimiter will have their name, truncated after the delimiter, returned in prefixes. Duplicate prefixes are omitted.", + "location": "query" + }, + "maxResults": { + "type": "integer", + "description": "Maximum number of items plus prefixes to return. As duplicate prefixes are omitted, fewer total results may be returned than requested. The default value of this parameter is 1,000 items.", + "format": "uint32", + "minimum": "0", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "A previously-returned page token representing part of the larger set of results to view.", + "location": "query" + }, + "prefix": { + "type": "string", + "description": "Filter results to objects whose names begin with this prefix.", + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + }, + "versions": { + "type": "boolean", + "description": "If true, lists all versions of an object as distinct results. The default is false. For more information, see Object Versioning.", + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "response": { + "$ref": "Objects" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsSubscription": true + }, + "patch": { + "id": "storage.objects.patch", + "path": "b/{bucket}/o/{object}", + "httpMethod": "PATCH", + "description": "Updates an object's metadata. This method supports patch semantics.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which the object resides.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "request": { + "$ref": "Object" + }, + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "rewrite": { + "id": "storage.objects.rewrite", + "path": "b/{sourceBucket}/o/{sourceObject}/rewriteTo/b/{destinationBucket}/o/{destinationObject}", + "httpMethod": "POST", + "description": "Rewrites a source object to a destination object. Optionally overrides metadata.", + "parameters": { + "destinationBucket": { + "type": "string", + "description": "Name of the bucket in which to store the new object. Overrides the provided object metadata's bucket value, if any.", + "required": true, + "location": "path" + }, + "destinationObject": { + "type": "string", + "description": "Name of the new object. Required when the object metadata is not otherwise provided. Overrides the object metadata's name value, if any. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "destinationPredefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to the destination object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the destination object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifSourceMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the source object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "maxBytesRewrittenPerCall": { + "type": "string", + "description": "The maximum number of bytes that will be rewritten per rewrite request. Most callers shouldn't need to specify this parameter - it is primarily in place to support testing. If specified the value must be an integral multiple of 1 MiB (1048576). Also, this only applies to requests where the source and destination span locations and/or storage classes. Finally, this value must not change across rewrite calls else you'll get an error that the rewriteToken is invalid.", + "format": "int64", + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + }, + "rewriteToken": { + "type": "string", + "description": "Include this field (from the previous rewrite response) on each rewrite request after the first one, until the rewrite response 'done' flag is true. Calls that provide a rewriteToken can omit all other request fields, but if included those fields must match the values provided in the first rewrite request.", + "location": "query" + }, + "sourceBucket": { + "type": "string", + "description": "Name of the bucket in which to find the source object.", + "required": true, + "location": "path" + }, + "sourceGeneration": { + "type": "string", + "description": "If present, selects a specific revision of the source object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "sourceObject": { + "type": "string", + "description": "Name of the source object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "sourceBucket", + "sourceObject", + "destinationBucket", + "destinationObject" + ], + "request": { + "$ref": "Object" + }, + "response": { + "$ref": "RewriteResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ] + }, + "update": { + "id": "storage.objects.update", + "path": "b/{bucket}/o/{object}", + "httpMethod": "PUT", + "description": "Updates an object's metadata.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which the object resides.", + "required": true, + "location": "path" + }, + "generation": { + "type": "string", + "description": "If present, selects a specific revision of this object (as opposed to the latest version, the default).", + "format": "int64", + "location": "query" + }, + "ifGenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation matches the given value.", + "format": "int64", + "location": "query" + }, + "ifGenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current generation does not match the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", + "format": "int64", + "location": "query" + }, + "ifMetagenerationNotMatch": { + "type": "string", + "description": "Makes the operation conditional on whether the object's current metageneration does not match the given value.", + "format": "int64", + "location": "query" + }, + "object": { + "type": "string", + "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", + "required": true, + "location": "path" + }, + "predefinedAcl": { + "type": "string", + "description": "Apply a predefined set of access controls to this object.", + "enum": [ + "authenticatedRead", + "bucketOwnerFullControl", + "bucketOwnerRead", + "private", + "projectPrivate", + "publicRead" + ], + "enumDescriptions": [ + "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", + "Object owner gets OWNER access, and project team owners get OWNER access.", + "Object owner gets OWNER access, and project team owners get READER access.", + "Object owner gets OWNER access.", + "Object owner gets OWNER access, and project team members get access according to their roles.", + "Object owner gets OWNER access, and allUsers get READER access." + ], + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to full.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + } + }, + "parameterOrder": [ + "bucket", + "object" + ], + "request": { + "$ref": "Object" + }, + "response": { + "$ref": "Object" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsMediaDownload": true, + "useMediaDownloadService": true + }, + "watchAll": { + "id": "storage.objects.watchAll", + "path": "b/{bucket}/o/watch", + "httpMethod": "POST", + "description": "Watch for changes on all objects in a bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of the bucket in which to look for objects.", + "required": true, + "location": "path" + }, + "delimiter": { + "type": "string", + "description": "Returns results in a directory-like mode. items will contain only objects whose names, aside from the prefix, do not contain delimiter. Objects whose names, aside from the prefix, contain delimiter will have their name, truncated after the delimiter, returned in prefixes. Duplicate prefixes are omitted.", + "location": "query" + }, + "maxResults": { + "type": "integer", + "description": "Maximum number of items plus prefixes to return. As duplicate prefixes are omitted, fewer total results may be returned than requested. The default value of this parameter is 1,000 items.", + "format": "uint32", + "minimum": "0", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "A previously-returned page token representing part of the larger set of results to view.", + "location": "query" + }, + "prefix": { + "type": "string", + "description": "Filter results to objects whose names begin with this prefix.", + "location": "query" + }, + "projection": { + "type": "string", + "description": "Set of properties to return. Defaults to noAcl.", + "enum": [ + "full", + "noAcl" + ], + "enumDescriptions": [ + "Include all properties.", + "Omit the acl property." + ], + "location": "query" + }, + "versions": { + "type": "boolean", + "description": "If true, lists all versions of an object as distinct results. The default is false. For more information, see Object Versioning.", + "location": "query" + } + }, + "parameterOrder": [ + "bucket" + ], + "request": { + "$ref": "Channel", + "parameterName": "resource" + }, + "response": { + "$ref": "Channel" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/devstorage.read_write" + ], + "supportsSubscription": true } } } diff --git a/src/Storage/Object.php b/src/Storage/Object.php index d89f0754494c..442bd24fad65 100644 --- a/src/Storage/Object.php +++ b/src/Storage/Object.php @@ -15,10 +15,11 @@ * limitations under the License. */ -namespace Google\Gcloud\Storage; +namespace Google\Cloud\Storage; -use Google\Gcloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\Storage\Connection\ConnectionInterface; use GuzzleHttp\Psr7; +use Psr\Http\Message\StreamInterface; /** * Objects are the individual pieces of data that you store in Google Cloud @@ -63,6 +64,7 @@ public function __construct(ConnectionInterface $connection, $name, $bucket, $ge 'object' => $name, 'generation' => $generation ]; + $this->acl = new Acl($this->connection, 'objectAccessControls', $this->identity); } @@ -71,13 +73,14 @@ public function __construct(ConnectionInterface $connection, $name, $bucket, $ge * * Example: * ``` - * use Google\Gcloud\Storage\Acl; + * use Google\Cloud\Storage\Acl; * * $acl = $object->acl(); * $acl->add('allAuthenticatedUsers', Acl::ROLE_READER); * ``` * * @see https://cloud.google.com/storage/docs/access-control More about Access Control Lists + * * @return Acl */ public function acl() @@ -114,6 +117,8 @@ public function exists() * $object->delete(); * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/delete Objects delete API documentation. + * * @param array $options { * Configuration options. * @@ -151,8 +156,10 @@ public function delete(array $options = []) * ]); * ``` * - * @see https://goo.gl/UBFXDs Learn more about configuring request options - * at the object patch API documentation. + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/patch Objects patch API documentation. + * + * @param array $metadata The available options for metadata are outlined + * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects#resource) * @param array $options { * Configuration options. * @@ -174,20 +181,12 @@ public function delete(array $options = []) * be either 'full' or 'noAcl'. * @type string $fields Selector which will cause the response to only * return the specified fields. - * @type array $acl Access controls on the object. - * @type string $cacheControl Cache-Control directive for the object - * data. - * @type string $contentDisposition Content-Disposition of the object - * data. - * @type string $contentEncoding Content-Encoding of the object data. - * @type string $contentLanguage Content-Language of the object data. - * @type string $contentType Content-Type of the object data. - * @type string $metadata User-provided metadata, in key/value pairs. * } * @return array */ - public function update(array $options = []) + public function update(array $metadata, array $options = []) { + $options += $metadata; $this->data = $this->connection->patchObject($options + $this->identity); return $this->data; @@ -220,14 +219,20 @@ public function downloadAsString(array $options = []) * * @param string $path Path to download file to. * @param array $options Configuration options. - * @return void + * @return StreamInterface */ public function downloadToFile($path, array $options = []) { + $destination = Psr7\stream_for(fopen($path, 'w')); + Psr7\copy_to_stream( $this->connection->downloadObject($options + $this->identity), - Psr7\stream_for(fopen($path, 'w')) + $destination ); + + $destination->seek(0); + + return $destination; } /** @@ -239,6 +244,8 @@ public function downloadToFile($path, array $options = []) * echo $info['metadata']; * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/get Objects get API documentation. + * * @param array $options { * Configuration options. * diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php index b7e37c4b642e..85e13ed9a16c 100644 --- a/src/Storage/StorageClient.php +++ b/src/Storage/StorageClient.php @@ -15,23 +15,24 @@ * limitations under the License. */ -namespace Google\Gcloud\Storage; +namespace Google\Cloud\Storage; -use Google\Gcloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\ClientInterface; +use Google\Cloud\ClientTrait; +use Google\Cloud\Storage\Connection\Rest; /** * Google Cloud Storage client. Allows you to store and retrieve data on * Google's infrastructure. Find more information at * [Google Cloud Storage API docs](https://developers.google.com/storage). */ -class StorageClient +class StorageClient implements ClientInterface { - const DEFAULT_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control'; + use ClientTrait; - /** - * @var ConnectionInterface Represents a connection to Cloud Storage. - */ - private $connection; + 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'; /** * @var string The project ID created in the Google Developers Console. @@ -39,35 +40,55 @@ class StorageClient private $projectId; /** - * Create a storage client. The preferred way to access this client is to - * use {@see Google\Gcloud\Gcloud::storage()}. + * Create a storage client. * * Example: * ``` - * use Google\Gcloud\Storage\Connection\REST; - * use Google\Gcloud\Storage\StorageClient; + * use Google\Cloud\Storage\StorageClient; * * $storage = new StorageClient( - * new REST(), + * new Rest(), * 'myAwesomeProject' * ); * ``` * - * @param ConnectionInterface $connection Represents a connection to Cloud - * Storage. - * @param string $projectId The project ID created in the Google Developers - * Console. + * @param array $config { + * Configuration options. + * + * @type string $projectId The project ID from the Google Developer's + * Console. + * @type callable $authHttpHandler A handler used to deliver Psr7 + * requests specifically for authentication. + * @type callable $httpHandler A handler used to deliver Psr7 requests. + * @type string $keyFile The contents of the service account + * credentials .json file retrieved from the Google Developers + * Console. + * @type string $keyFilePath The full path to your service account + * credentials .json file retrieved from the Google Developers + * Console. + * @type int $retries Number of retries for a failed request. Defaults + * to 3. + * @type array $scopes Scopes to be used for the request. + * } */ - public function __construct(ConnectionInterface $connection, $projectId) + public function __construct(array $config = []) { - $this->connection = $connection; - $this->projectId = $projectId; + if (!isset($config['projectId'])) { + throw new \InvalidArgumentException('A projectId is required.'); + } + + if (!isset($config['scopes'])) { + $config['scopes'] = [self::FULL_CONTROL_SCOPE]; + } + + $this->setConnection(new Rest($config)); + $this->projectId = $config['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 Google\Gcloud\Storage\Bucket}. + * see {@see Google\Cloud\Storage\Bucket}. * * Example: * ``` @@ -101,6 +122,8 @@ public function bucket($name) * } * ``` * + * @see https://cloud.google.com/storage/docs/json_api/v1/buckets/list Buckets list API documentation. + * * @param array $options { * Configuration options. * @@ -149,8 +172,8 @@ public function buckets(array $options = []) * ]); * ``` * - * @see https://goo.gl/PNTqTh Learn more about configuring request options - * at the bucket insert API documentation. + * @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. * @param array $options { * Configuration options. @@ -184,7 +207,7 @@ public function buckets(array $options = []) */ public function createBucket($name, array $options = []) { - $response = $this->connection->createBucket($options + ['name' => $name, 'project' => $this->projectId]); + $response = $this->connection->insertBucket($options + ['name' => $name, 'project' => $this->projectId]); return new Bucket($this->connection, $name, $response); } } diff --git a/src/Upload/AbstractUploader.php b/src/Upload/AbstractUploader.php new file mode 100644 index 000000000000..3edf9d980cc9 --- /dev/null +++ b/src/Upload/AbstractUploader.php @@ -0,0 +1,117 @@ +requestWrapper = $requestWrapper; + $this->data = Psr7\stream_for($data); + $this->uri = $uri; + $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->chunkSize = isset($options['chunkSize']) ? $options['chunkSize'] : null; + $this->requestOptions = array_intersect_key($options, [ + 'httpOptions' => null, + 'retries' => null + ]); + $this->contentType = isset($options['contentType']) + ? $options['contentType'] + : $this->getContentTypeFromFilename($this->data->getMetadata('uri')); + } + + /** + * @return array + */ + abstract public function upload(); + + /** + * Determines the content type. + * + * @param string $filename + */ + private function getContentTypeFromFilename($filename) + { + return Psr7\mimetype_from_filename($filename) ?: 'application/octet-stream'; + } +} diff --git a/src/Upload/MultipartUploader.php b/src/Upload/MultipartUploader.php new file mode 100644 index 000000000000..90b01471bcc0 --- /dev/null +++ b/src/Upload/MultipartUploader.php @@ -0,0 +1,66 @@ + 'metadata', + 'headers' => ['Content-Type' => 'application/json; charset=UTF-8'], + 'contents' => json_encode($this->metadata) + ], + [ + 'name' => 'data', + 'headers' => ['Content-Type' => $this->contentType], + 'contents' => $this->data + ] + ], 'boundary'); + + $headers = [ + 'Content-Type' => 'multipart/related; boundary=boundary', + 'Content-Length' => $multipartStream->getSize() + ]; + + return json_decode( + $this->requestWrapper->send( + new Request( + 'POST', + $this->uri, + $headers, + $multipartStream + ), + $this->requestOptions + )->getBody(), + true + ); + } +} diff --git a/src/Upload/ResumableUploader.php b/src/Upload/ResumableUploader.php new file mode 100644 index 000000000000..22221e86e020 --- /dev/null +++ b/src/Upload/ResumableUploader.php @@ -0,0 +1,185 @@ +resumeUri) { + return $this->createResumeUri(); + } + + return $this->resumeUri; + } + + /** + * Resumes a download using the provided URI. + * + * @param string $resumeUri + * @return array + * @throws GoogleException + */ + public function resume($resumeUri) + { + if (!$this->data->isSeekable()) { + throw new GoogleException('Cannot resume upload on a stream which cannot be seeked.'); + } + + $this->resumeUri = $resumeUri; + $response = $this->getStatusResponse(); + + if ($response->getBody()->getSize() > 0) { + return json_decode($response->getBody(), true); + } + + $this->rangeStart = $this->getRangeStart($response->getHeaderLine('Range')); + + return $this->upload(); + } + + /** + * Triggers the upload process. + * + * @return array + * @throws GoogleException + */ + public function upload() + { + $rangeStart = $this->rangeStart; + $response = null; + $resumeUri = $this->getResumeUri(); + $size = $this->data->getSize() ?: '*'; + + do { + $data = new LimitStream( + $this->data, + $this->chunkSize ?: - 1, + $rangeStart + ); + $rangeEnd = $rangeStart + ($data->getSize() - 1); + $headers = [ + 'Content-Length' => $data->getSize(), + 'Content-Type' => $this->contentType, + 'Content-Range' => "bytes $rangeStart-$rangeEnd/$size", + ]; + + $request = new Request( + 'PUT', + $resumeUri, + $headers, + $data + ); + + try { + $response = $this->requestWrapper->send($request, $this->requestOptions); + } catch (GoogleException $ex) { + throw new GoogleException( + "Upload failed. Please use this URI to resume your upload: $this->resumeUri", + $ex->getCode(), + $ex + ); + } + + $rangeStart = $this->getRangeStart($response->getHeaderLine('Range')); + } while ($response->getStatusCode() === 308); + + return json_decode($response->getBody(), true); + } + + /** + * Creates the resume URI. + * + * @return string + */ + private function createResumeUri() + { + $headers = [ + 'X-Upload-Content-Type' => $this->contentType, + 'X-Upload-Content-Length' => $this->data->getSize(), + 'Content-Type' => 'application/json' + ]; + + $request = new Request( + 'POST', + $this->uri, + $headers, + json_encode($this->metadata) + ); + + $response = $this->requestWrapper->send($request, $this->requestOptions); + $this->resumeUri = $response->getHeaderLine('Location'); + + return $this->resumeUri; + } + + /** + * Gets the status of the upload. + * + * @return ResponseInterface + */ + private function getStatusResponse() + { + $request = new Request( + 'PUT', + $this->resumeUri, + ['Content-Range' => 'bytes */*'] + ); + + return $this->requestWrapper->send($request, $this->requestOptions); + } + + /** + * Gets the starting range for the upload. + * + * @param string $rangeHeader + * @return int + */ + private function getRangeStart($rangeHeader) + { + if (!$rangeHeader) { + return null; + } + + return (int) explode('-', $rangeHeader)[1] + 1; + } +} diff --git a/src/UriTrait.php b/src/UriTrait.php new file mode 100644 index 000000000000..c3a716359b80 --- /dev/null +++ b/src/UriTrait.php @@ -0,0 +1,61 @@ +expand($uri, $variables); + } + + /** + * @param string $uri + * @param array $query + * @return UriInterface + */ + public function buildUriWithQuery($uri, array $query) + { + // @todo fix this hack. when using build_query booleans are converted to + // 1 or 0 which the API does not accept. this casts bools to their + // string representation + $query = array_filter($query); + foreach ($query as $k => &$v) { + if (is_bool($v)) { + $v = $v ? 'true' : 'false'; + } + } + + return Psr7\uri_for($uri)->withQuery(Psr7\build_query($query)); + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php deleted file mode 100644 index cbedf59d248e..000000000000 --- a/tests/ClientTest.php +++ /dev/null @@ -1,11 +0,0 @@ -assertEquals(true, true); - } -} diff --git a/tests/Compute/MetadataTest.php b/tests/Compute/MetadataTest.php index 5d27ead8da5d..fc9dd53341e0 100644 --- a/tests/Compute/MetadataTest.php +++ b/tests/Compute/MetadataTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Gcloud\Tests\Compute; +namespace Google\Cloud\Tests\Compute; -use Google\Gcloud\Compute\Metadata; +use Google\Cloud\Compute\Metadata; class MetadataTest extends \PHPUnit_Framework_TestCase { @@ -28,7 +28,7 @@ public function setUp() { $this->metadata = new Metadata(); $this->mock = $this->getMockBuilder( - '\Google\Gcloud\Compute\Metadata\Readers\StreamReader') + '\Google\Cloud\Compute\Metadata\Readers\StreamReader') ->setMethods(array('read')) ->getmock(); $this->metadata->setReader($this->mock); diff --git a/tests/ExponentialBackoffTest.php b/tests/ExponentialBackoffTest.php new file mode 100644 index 000000000000..9fe17bb2076a --- /dev/null +++ b/tests/ExponentialBackoffTest.php @@ -0,0 +1,108 @@ +delayFunction = function() { + return; + }; + } + + /** + * @dataProvider retriesProvider + */ + public function testThrowsExceptionAfterFullAttempts($retries, $exception) + { + // Expected attempts is the number of retries plus the initial attempt. + $expectedAttempts = $retries ? $retries + 1 : 4; + $actualAttempts = 0; + $hasTriggeredException = false; + $backoff = new ExponentialBackoff($retries); + $backoff->setDelayFunction($this->delayFunction); + + try { + $backoff->execute(function () use (&$actualAttempts, $expectedAttempts, $exception) { + $actualAttempts++; + throw $exception; + }); + } catch (\Exception $ex) { + $hasTriggeredException = true; + } + + $this->assertTrue($hasTriggeredException); + $this->assertEquals($expectedAttempts, $actualAttempts); + } + + public function retriesProvider() + { + $rateLimitExceededMessage = '{"error": {"errors": [{"reason": "rateLimitExceeded"}]}}'; + $userRateLimitExceededMessage = '{"error": {"errors": [{"reason": "userRateLimitExceeded"}]}}'; + + return [ + [null, new \Exception('', 500)], + [2, new \Exception('', 502)], + [3, new \Exception('', 503)], + [4, new \Exception('', 504)], + [5, new \Exception($rateLimitExceededMessage)], + [6, new \Exception($userRateLimitExceededMessage)] + ]; + } + + public function testThrowsExceptionWithNonRetryableError() + { + $nonRetryableErrorMessage = '{"error": {"errors": [{"reason": "notAGoodEnoughReason"}]}}'; + $actualAttempts = 0; + $hasTriggeredException = false; + $backoff = new ExponentialBackoff(); + $backoff->setDelayFunction($this->delayFunction); + + try { + $backoff->execute(function () use (&$actualAttempts, $nonRetryableErrorMessage) { + $actualAttempts++; + throw new \Exception($nonRetryableErrorMessage, 429); + }); + } catch (\Exception $ex) { + $hasTriggeredException = true; + } + + $this->assertTrue($hasTriggeredException); + $this->assertEquals(1, $actualAttempts); + } + + public function testSuccessWithNoRetries() + { + $actualAttempts = 0; + $backoff = new ExponentialBackoff(); + $backoff->setDelayFunction($this->delayFunction); + + $backoff->execute(function () use (&$actualAttempts) { + $actualAttempts++; + return; + }); + + $this->assertEquals(1, $actualAttempts); + } +} diff --git a/tests/HttpRequestWrapperTest.php b/tests/HttpRequestWrapperTest.php deleted file mode 100644 index 5df07867c323..000000000000 --- a/tests/HttpRequestWrapperTest.php +++ /dev/null @@ -1,50 +0,0 @@ -getMock(RequestInterface::class); - $request->method('getUri')->willReturn('/'); - $request->method('getHeaders')->willReturn([]); - - $wrapper = new HttpRequestWrapper(); - $refl = new \ReflectionClass($wrapper); - - $token = 'some_generated_token'; - $credentials = $refl->getProperty('credentials'); - $credentials->setAccessible(true); - $credentials->setValue($wrapper, [ - 'expiry' => strtotime('+300 seconds'), - 'access_token' => $token - ]); - - $signedRequest = $wrapper->signRequest($request); - - $header = $signedRequest->getHeader('Authorization')[0]; - - $this->assertEquals("Bearer $token", $header); - } -} diff --git a/tests/RequestBuilderTest.php b/tests/RequestBuilderTest.php new file mode 100644 index 000000000000..efd9567061d6 --- /dev/null +++ b/tests/RequestBuilderTest.php @@ -0,0 +1,56 @@ +builder = new RequestBuilder( + __DIR__ . '/fixtures/service-fixture.json', + 'http://www.example.com/' + ); + } + + public function testBuildsRequest() + { + $parameters = [ + 'queryParam' => 'query', + 'pathParam' => 'path', + 'referenceProp' => 'reference' + ]; + + $request = $this->builder->build('myResource', 'myMethod', $parameters); + $uri = $request->getUri(); + + $this->assertEquals('/path', $uri->getPath()); + $this->assertEquals('queryParam=query', $uri->getQuery()); + $this->assertEquals('{"referenceProp":"reference"}', (string) $request->getBody()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsExceptionWithNonExistantMethod() + { + $this->builder->build('myResource', 'doesntExist'); + } +} diff --git a/tests/RequestWrapperTest.php b/tests/RequestWrapperTest.php new file mode 100644 index 000000000000..54b7d8bde106 --- /dev/null +++ b/tests/RequestWrapperTest.php @@ -0,0 +1,184 @@ + 'abc', + 'httpHandler' => function ($request, $options = []) use ($response) { + return $response; + } + ]); + + $actualResponse = $requestWrapper->send( + new Request('GET', 'http://www.test.com') + ); + + $this->assertEquals($expectedBody, (string) $actualResponse->getBody()); + } + + /** + * @expectedException Google\Cloud\Exception\GoogleException + */ + public function testThrowsExceptionWhenRequestFails() + { + $requestWrapper = new RequestWrapper([ + 'accessToken' => 'abc', + 'httpHandler' => function ($request, $options = []) { + throw new \Exception(); + } + ]); + + $requestWrapper->send(new Request('GET', 'http://wwww.example.com')); + } + + public function testMultipleRequestsUseCachedCredentials() + { + $request = new Request('GET', 'http://www.example.com'); + $credentialsFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); + $credentialsFetcher->fetchAuthToken(Argument::any()) + ->willReturn(['access_token' => 'abc']) + ->shouldBeCalledTimes(1); + + $requestWrapper = new RequestWrapper([ + 'httpHandler' => function ($request, $options = []) { + return new Response(200, []); + }, + 'credentialsFetcher' => $credentialsFetcher->reveal() + ]); + + $requestWrapper->send($request); + $requestWrapper->send($request); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testThrowsExceptionWithInvalidCredentialsFetcher() + { + $credentialsFetcher = new \stdClass(); + + $requestWrapper = new RequestWrapper([ + 'credentialsFetcher' => $credentialsFetcher + ]); + } + + /** + * @dataProvider credentialsProvider + */ + public function testCredentialsFetcher($wrapperConfig) + { + $requestWrapper = new RequestWrapper($wrapperConfig); + + $this->assertInstanceOf( + 'Google\Auth\FetchAuthTokenInterface', + $requestWrapper->getCredentialsFetcher() + ); + } + + public function credentialsProvider() + { + $config = [ + 'authHttpHandler' => function ($request, $options = []) { + return new Response(200, [], json_encode(['access_token' => 'abc'])); + }, + 'httpHandler' => function ($request, $options = []) { + return new Response(200, []); + } + ]; + + $keyFilePath = __DIR__ . '/fixtures/json-key-fixture.json'; + putenv("GOOGLE_APPLICATION_CREDENTIALS=$keyFilePath"); // for application default credentials + + $credentialsFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface'); + $credentialsFetcher->fetchAuthToken(Argument::any()) + ->willReturn(['access_token' => 'abc']); + + return [ + [$config + ['keyFile' => file_get_contents($keyFilePath)]], // keyFile + [$config + ['keyFilePath' => $keyFilePath]], //keyFilePath + [$config + ['credentialsFetcher' => $credentialsFetcher->reveal()]], // user supplied fetcher + [$config] // application default + ]; + } + + public function testAddsUserAgentToRequest() + { + $requestWrapper = new RequestWrapper([ + 'httpHandler' => function ($request, $options = []) { + $userAgent = $request->getHeaderLine('User-Agent'); + $this->assertEquals('gcloud-php ' . ClientInterface::VERSION, $userAgent); + return new Response(200); + }, + 'accessToken' => 'abc' + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com') + ); + } + + public function testAddsTokenToRequest() + { + $accessToken = 'abc'; + $requestWrapper = new RequestWrapper([ + 'httpHandler' => function ($request, $options = []) use ($accessToken) { + $authHeader = $request->getHeaderLine('Authorization'); + $this->assertEquals('Bearer ' . $accessToken, $authHeader); + return new Response(200); + }, + 'accessToken' => $accessToken + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com') + ); + } + + /** + * @expectedException Google\Cloud\Exception\GoogleException + */ + public function testThrowsExceptionWhenFetchingCredentialsFails() + { + $requestWrapper = new RequestWrapper([ + 'authHttpHandler' => function ($request, $options = []) { + throw new \Exception(); + }, + 'httpHandler' => function ($request, $options = []) { + return new Response(200); + } + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com') + ); + } +} diff --git a/tests/RestTraitTest.php b/tests/RestTraitTest.php new file mode 100644 index 000000000000..d7c78f7651bb --- /dev/null +++ b/tests/RestTraitTest.php @@ -0,0 +1,74 @@ +implementation = new RestTraitImplementation; + $this->requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper'); + $this->requestBuilder = $this->prophesize('Google\Cloud\RequestBuilder'); + $this->requestBuilder->build(Argument::cetera()) + ->willReturn(new Request('GET', '/someplace')); + } + + public function testSendsRequest() + { + $responseBody = '{"whatAWonderful": "response"}'; + $this->requestWrapper->send(Argument::cetera()) + ->willReturn(new Response(200, [], $responseBody)); + + $this->implementation->setRequestBuilder($this->requestBuilder->reveal()); + $this->implementation->setRequestWrapper($this->requestWrapper->reveal()); + $actualResponse = $this->implementation->send('resource', 'method'); + + $this->assertEquals(json_decode($responseBody, true), $actualResponse); + } + + public function testSendsRequestWithOptions() + { + $httpOptions = [ + 'httpOptions' => ['debug' => true], + 'retries' => 5 + ]; + $responseBody = '{"whatAWonderful": "response"}'; + $this->requestWrapper->send(Argument::any(), $httpOptions) + ->willReturn(new Response(200, [], $responseBody)); + + $this->implementation->setRequestBuilder($this->requestBuilder->reveal()); + $this->implementation->setRequestWrapper($this->requestWrapper->reveal()); + $actualResponse = $this->implementation->send('resource', 'method', $httpOptions); + + $this->assertEquals(json_decode($responseBody, true), $actualResponse); + } +} + +class RestTraitImplementation +{ + use RestTrait; +} diff --git a/tests/ServiceBuilderTest.php b/tests/ServiceBuilderTest.php new file mode 100644 index 000000000000..0403a082fdae --- /dev/null +++ b/tests/ServiceBuilderTest.php @@ -0,0 +1,53 @@ + 'myProject']); + + $this->assertInstanceOf('Google\Cloud\Storage\StorageClient', $gcloud->storage()); + } + + public function testBuildsStorageClientWithOverriddenConfig() + { + $gcloud = new ServiceBuilder(); + $storage = $gcloud->storage([ + 'projectId' => 'myProject', + 'scopes' => ['somescope'], + 'httpHandler' => function() { + return; + } + ]); + + $this->assertInstanceOf('Google\Cloud\Storage\StorageClient', $storage); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testStorageThrowsExceptionWithoutProjectId() + { + $gcloud = new ServiceBuilder(); + $gcloud->storage(); + } +} diff --git a/tests/Storage/AclTest.php b/tests/Storage/AclTest.php index 261b868d3b76..5d4b3d191b6f 100644 --- a/tests/Storage/AclTest.php +++ b/tests/Storage/AclTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Gcloud\Tests\Storage; +namespace Google\Cloud\Tests\Storage; -use Google\Gcloud\Storage\Acl; +use Google\Cloud\Storage\Acl; use Prophecy\Argument; class AclTest extends \PHPUnit_Framework_TestCase @@ -26,7 +26,7 @@ class AclTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize('Google\Gcloud\Storage\Connection\ConnectionInterface'); + $this->connection = $this->prophesize('Google\Cloud\Storage\Connection\ConnectionInterface'); } /** diff --git a/tests/Storage/BucketTest.php b/tests/Storage/BucketTest.php index 9374913e22bb..3b31c1fa4f7a 100644 --- a/tests/Storage/BucketTest.php +++ b/tests/Storage/BucketTest.php @@ -15,32 +15,34 @@ * limitations under the License. */ -namespace Google\Gcloud\Tests\Storage; +namespace Google\Cloud\Tests\Storage; -use Google\Gcloud\Storage\Bucket; +use Google\Cloud\Storage\Bucket; use Prophecy\Argument; class BucketTest extends \PHPUnit_Framework_TestCase { - public $connection; + private $connection; + private $resumableUploader; public function setUp() { - $this->connection = $this->prophesize('Google\Gcloud\Storage\Connection\ConnectionInterface'); + $this->connection = $this->prophesize('Google\Cloud\Storage\Connection\ConnectionInterface'); + $this->resumableUploader = $this->prophesize('Google\Cloud\Upload\ResumableUploader'); } public function testGetsAcl() { $bucket = new Bucket($this->connection->reveal(), 'bucket'); - $this->assertInstanceOf('Google\Gcloud\Storage\Acl', $bucket->acl()); + $this->assertInstanceOf('Google\Cloud\Storage\Acl', $bucket->acl()); } public function testGetsDefaultAcl() { $bucket = new Bucket($this->connection->reveal(), 'bucket'); - $this->assertInstanceOf('Google\Gcloud\Storage\Acl', $bucket->defaultAcl()); + $this->assertInstanceOf('Google\Cloud\Storage\Acl', $bucket->defaultAcl()); } public function testDoesExistTrue() @@ -59,11 +61,57 @@ public function testDoesExistFalse() $this->assertFalse($bucket->exists()); } + public function testUploadData() + { + $this->resumableUploader->upload()->willReturn([ + 'name' => 'file.txt', + 'generation' => 123 + ]); + $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader); + $bucket = new Bucket($this->connection->reveal(), 'bucket'); + + $this->assertInstanceOf( + 'Google\Cloud\Storage\Object', + $bucket->upload('some data to upload', ['name' => 'data.txt']) + ); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testUploadDataAsStringWithNoName() + { + $bucket = new Bucket($this->connection->reveal(), 'bucket'); + + $bucket->upload('some more data'); + } + + public function testGetResumableUploader() + { + $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader->reveal()); + $bucket = new Bucket($this->connection->reveal(), 'bucket'); + + $this->assertInstanceOf( + 'Google\Cloud\Upload\ResumableUploader', + $bucket->getResumableUploader('some data to upload', ['name' => 'data.txt']) + ); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetResumableUploaderWithStringWithNoName() + { + $bucket = new Bucket($this->connection->reveal(), 'bucket'); + + $bucket->getResumableUploader('some more data'); + } + public function testGetObject() { $bucket = new Bucket($this->connection->reveal(), 'bucket'); - $this->assertInstanceOf('Google\Gcloud\Storage\Object', $bucket->object('peter-venkman.jpg')); + $this->assertInstanceOf('Google\Cloud\Storage\Object', $bucket->object('peter-venkman.jpg')); } public function testInstantiateObjectWithGeneration() @@ -74,7 +122,7 @@ public function testInstantiateObjectWithGeneration() 'generation' => '5' ]); - $this->assertInstanceOf('Google\Gcloud\Storage\Object', $object); + $this->assertInstanceOf('Google\Cloud\Storage\Object', $object); } public function testGetsObjectsWithoutToken() diff --git a/tests/Storage/Connection/RESTTest.php b/tests/Storage/Connection/RESTTest.php index 018d5f0786f5..ea39220802d6 100644 --- a/tests/Storage/Connection/RESTTest.php +++ b/tests/Storage/Connection/RESTTest.php @@ -1,7 +1,6 @@ requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper'); + $this->successBody = '{"canI":"kickIt"}'; + } + + /** + * @dataProvider methodProvider + * @todo revisit this approach + */ + public function testCallBasicMethods($method) { - $response = $this->getMock(ResponseInterface::class); - $response->method('getBody')->willReturn('{"name": "foo.txt"}'); + $options = []; + $request = new Request('GET', '/somewhere'); + $response = new Response(200, [], $this->successBody); + + $requestBuilder = $this->prophesize('Google\Cloud\RequestBuilder'); + $requestBuilder->build( + Argument::type('string'), + Argument::type('string'), + Argument::type('array') + )->willReturn($request); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $rest = new Rest(); + $rest->setRequestBuilder($requestBuilder->reveal()); + $rest->setRequestWrapper($this->requestWrapper->reveal()); + + if (substr($method, -3) == 'Acl') { + $options = ['type' => 'bucketAccessControls']; + } + + $this->assertEquals(json_decode($this->successBody, true), $rest->$method($options)); + } + + public function methodProvider() + { + return [ + ['deleteAcl'], + ['getAcl'], + ['listAcl'], + ['insertAcl'], + ['patchAcl'], + ['deleteBucket'], + ['getBucket'], + ['listBuckets'], + ['insertBucket'], + ['patchBucket'], + ['deleteObject'], + ['getObject'], + ['listObjects'], + ['patchObject'] + ]; + } + + public function testDownloadObject() + { + $actualRequest = null; + $response = new Response(200, [], $this->successBody); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->will( + function ($args) use (&$actualRequest, $response) { + $actualRequest = $args[0]; + return $response; + } + ); - $wrapper = $this->getMock(HttpRequestWrapper::class); - $wrapper->method('send')->willReturn($response); + $rest = new Rest(); + $rest->setRequestWrapper($this->requestWrapper->reveal()); - $object = (new REST($wrapper))->getObject(); + $actualBody = $rest->downloadObject([ + 'bucket' => 'bigbucket', + 'object' => 'myfile.txt', + 'generation' => 100, + 'httpOptions' => ['debug' => true], + 'retries' => 0 + ]); + + $actualUri = (string) $actualRequest->getUri(); + + $this->assertEquals($this->successBody, $actualBody); + $this->assertEquals( + 'https://storage.googleapis.com/bigbucket/myfile.txt?generation=100&alt=media', + $actualUri + ); + } + + /** + * @dataProvider insertObjectProvider + */ + public function testInsertObject( + array $options, + $expectedUploaderType, + $expectedContentType, + array $expectedMetadata + ) { + $actualRequest = null; + $response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->will( + function ($args) use (&$actualRequest, $response) { + $request = $args[0]; + if ($request->getMethod() === 'POST') { + $actualRequest = $request; + } + + return $response; + } + ); + + $rest = new Rest(); + $rest->setRequestWrapper($this->requestWrapper->reveal()); + $uploader = $rest->insertObject($options); + $uploader->upload(); + list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest); + + $this->assertInstanceOf($expectedUploaderType, $uploader); + $this->assertEquals($expectedContentType, $contentType); + + foreach ($expectedMetadata as $key => $value) { + $this->assertEquals($value, $metadata[$key]); + } + } + + public function insertObjectProvider() + { + $tempFile = Psr7\stream_for(fopen('php://temp', 'r+')); + $tempFile->write(str_repeat('0', 5000001)); + $logoFile = Psr7\stream_for(fopen(__DIR__ . '../../../data/logo.svg', 'r')); + + return [ + [ + [ + 'data' => $tempFile, + 'name' => 'file.txt', + 'predefinedAcl' => 'private', + 'metadata' => ['contentType' => 'text/plain'] + ], + 'Google\Cloud\Upload\ResumableUploader', + 'text/plain', + [ + 'md5Hash' => base64_encode(Psr7\hash($tempFile, 'md5', true)), + 'name' => 'file.txt' + ] + ], + [ + [ + 'data' => $logoFile, + 'validate' => false + ], + 'Google\Cloud\Upload\MultipartUploader', + 'image/svg+xml', + [ + 'name' => 'logo.svg' + ] + ], + [ + [ + 'data' => 'abcdefg', + 'name' => 'file.ext', + 'resumable' => true, + 'validate' => false, + 'metadata' => [ + 'contentType' => 'text/plain', + 'metadata' => [ + 'here' => 'wego' + ] + ] + ], + 'Google\Cloud\Upload\ResumableUploader', + 'text/plain', + [ + 'name' => 'file.ext', + 'metadata' => [ + 'here' => 'wego' + ] + ] + ] + ]; + } + + private function getContentTypeAndMetadata(RequestInterface $request) + { + // Resumable upload request + if ($request->getHeaderLine('X-Upload-Content-Type')) { + return [ + $request->getHeaderLine('X-Upload-Content-Type'), + json_decode($request->getBody(), true) + ]; + } - $this->assertEquals(['name' => 'foo.txt'], $object); + // Multipart upload request + $lines = explode(PHP_EOL, (string) $request->getBody()); + return [ + trim(explode(':', $lines[7])[1]), + json_decode($lines[5], true) + ]; } } diff --git a/tests/Storage/ObjectTest.php b/tests/Storage/ObjectTest.php index 8b1cd11d3349..26686f6339a3 100644 --- a/tests/Storage/ObjectTest.php +++ b/tests/Storage/ObjectTest.php @@ -15,9 +15,10 @@ * limitations under the License. */ -namespace Google\Gcloud\Tests\Storage; +namespace Google\Cloud\Tests\Storage; -use Google\Gcloud\Storage\Object; +use Google\Cloud\Storage\Object; +use GuzzleHttp\Psr7; use Prophecy\Argument; class ObjectTest extends \PHPUnit_Framework_TestCase @@ -26,14 +27,14 @@ class ObjectTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize('Google\Gcloud\Storage\Connection\ConnectionInterface'); + $this->connection = $this->prophesize('Google\Cloud\Storage\Connection\ConnectionInterface'); } public function testGetAcl() { $object = new Object($this->connection->reveal(), 'object.txt', 'bucket'); - $this->assertInstanceOf('Google\Gcloud\Storage\Acl', $object->acl()); + $this->assertInstanceOf('Google\Cloud\Storage\Acl', $object->acl()); } public function testDoesExistTrue() @@ -72,16 +73,24 @@ public function testUpdatesData() public function testDownloadsAsString() { - $stream = $this->prophesize('Psr\Http\Message\StreamInterface'); - $stream->__toString()->willReturn($string = 'abcdefg'); - - $this->connection->downloadObject(Argument::any())->willReturn($stream->reveal()); + $stream = Psr7\stream_for($string = 'abcdefg'); + $this->connection->downloadObject(Argument::any())->willReturn($stream); $object = new Object($this->connection->reveal(), 'object.txt', 'bucket'); $this->assertEquals($string, $object->downloadAsString()); } + public function testDownloadsToFile() + { + $stream = Psr7\stream_for($string = 'abcdefg'); + $this->connection->downloadObject(Argument::any())->willReturn($stream); + + $object = new Object($this->connection->reveal(), 'object.txt', 'bucket'); + + $this->assertEquals($string, $object->downloadToFile('php://temp')->getContents()); + } + public function testGetsInfo() { $objectInfo = [ diff --git a/tests/Storage/StorageClientTest.php b/tests/Storage/StorageClientTest.php index ed41f6a8972f..bcf678aaab30 100644 --- a/tests/Storage/StorageClientTest.php +++ b/tests/Storage/StorageClientTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Gcloud\Tests\Storage; +namespace Google\Cloud\Tests\Storage; -use Google\Gcloud\Storage\StorageClient; +use Google\Cloud\Storage\StorageClient; use Prophecy\Argument; class StorageClientTest extends \PHPUnit_Framework_TestCase @@ -26,14 +26,14 @@ class StorageClientTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize('Google\Gcloud\Storage\Connection\ConnectionInterface'); + $this->connection = $this->prophesize('Google\Cloud\Storage\Connection\ConnectionInterface'); + $this->client = new StorageClient(['projectId' => 'project']); } public function testGetBucket() { - $client = new StorageClient($this->connection->reveal(), 'projectId'); - - $this->assertInstanceOf('Google\Gcloud\Storage\Bucket', $client->bucket('myBucket')); + $this->client->setConnection($this->connection->reveal()); + $this->assertInstanceOf('Google\Cloud\Storage\Bucket', $this->client->bucket('myBucket')); } public function testGetsBucketsWithoutToken() @@ -44,8 +44,8 @@ public function testGetsBucketsWithoutToken() ] ]); - $client = new StorageClient($this->connection->reveal(), 'projectId'); - $buckets = iterator_to_array($client->buckets()); + $this->client->setConnection($this->connection->reveal()); + $buckets = iterator_to_array($this->client->buckets()); $this->assertEquals('bucket1', $buckets[0]->getName()); } @@ -66,17 +66,17 @@ public function testGetsBucketsWithToken() ] ); - $client = new StorageClient($this->connection->reveal(), 'projectId'); - $bucket = iterator_to_array($client->buckets()); + $this->client->setConnection($this->connection->reveal()); + $bucket = iterator_to_array($this->client->buckets()); $this->assertEquals('bucket2', $bucket[1]->getName()); } public function testCreatesBucket() { - $this->connection->createBucket(Argument::any())->willReturn(['name' => 'bucket']); - $client = new StorageClient($this->connection->reveal(), 'projectId'); + $this->connection->insertBucket(Argument::any())->willReturn(['name' => 'bucket']); + $this->client->setConnection($this->connection->reveal()); - $this->assertInstanceOf('Google\Gcloud\Storage\Bucket', $client->createBucket('bucket')); + $this->assertInstanceOf('Google\Cloud\Storage\Bucket', $this->client->createBucket('bucket')); } } diff --git a/tests/Upload/MultipartUploaderTest.php b/tests/Upload/MultipartUploaderTest.php new file mode 100644 index 000000000000..238db13f1f29 --- /dev/null +++ b/tests/Upload/MultipartUploaderTest.php @@ -0,0 +1,48 @@ +prophesize('Google\Cloud\RequestWrapper'); + $stream = Psr7\stream_for('abcd'); + $successBody = '{"canI":"kickIt"}'; + $response = new Response(200, [], $successBody); + + $requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $uploader = new MultipartUploader( + $requestWrapper->reveal(), + $stream, + 'http://www.example.com' + ); + + $this->assertEquals(json_decode($successBody, true), $uploader->upload()); + } +} diff --git a/tests/Upload/ResumableUploaderTest.php b/tests/Upload/ResumableUploaderTest.php new file mode 100644 index 000000000000..ac2cd42a6e50 --- /dev/null +++ b/tests/Upload/ResumableUploaderTest.php @@ -0,0 +1,169 @@ +requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper'); + $this->stream = Psr7\stream_for('abcd'); + $this->successBody = '{"canI":"kickIt"}'; + } + + public function testUploadsData() + { + $response = new Response(200, ['Location' => 'theResumeUri'], $this->successBody); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $this->assertEquals(json_decode($this->successBody, true), $uploader->upload()); + } + + public function testGetResumeUri() + { + $resumeUri = 'theResumeUri'; + $response = new Response(200, ['Location' => $resumeUri]); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $this->assertEquals($resumeUri, $uploader->getResumeUri()); + } + + public function testResumesUpload() + { + $response = new Response(200, [], $this->successBody); + $statusResponse = new Response(200, ['Range' => 'bytes 0-2']); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $this->requestWrapper->send( + Argument::that(function ($request) { + return $request->getHeaderLine('Content-Range') === 'bytes */*'; + }), + Argument::type('array') + )->willReturn($statusResponse); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $this->assertEquals( + json_decode($this->successBody, true), + $uploader->resume('http://some-resume-uri.example.com') + ); + } + + public function testResumeFinishedUpload() + { + $statusResponse = new Response(200, [], $this->successBody); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($statusResponse); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $this->assertEquals( + json_decode($this->successBody, true), + $uploader->resume('http://some-resume-uri.example.com') + ); + } + + /** + * @expectedException Google\Cloud\Exception\GoogleException + */ + public function testThrowsExceptionWhenResumingNonSeekableStream() + { + $stream = $this->prophesize('Psr\Http\Message\StreamInterface'); + $stream->isSeekable()->willReturn(false); + $stream->getMetadata('uri')->willReturn('blah'); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $stream->reveal(), + 'http://www.example.com' + ); + + $uploader->resume('http://some-resume-uri.example.com'); + } + + /** + * @expectedException Google\Cloud\Exception\GoogleException + */ + public function testThrowsExceptionWithFailedUpload() + { + $resumeUriResponse = new Response(200, ['Location' => 'theResumeUri']); + + $this->requestWrapper->send( + Argument::which('getMethod', 'POST'), + Argument::type('array') + )->willReturn($resumeUriResponse); + + $this->requestWrapper->send( + Argument::which('getMethod', 'PUT'), + Argument::type('array') + )->willThrow('Google\Cloud\Exception\GoogleException'); + + $uploader = new ResumableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $uploader->upload(); + } +} diff --git a/tests/UriTraitTest.php b/tests/UriTraitTest.php new file mode 100644 index 000000000000..b9a3292149e0 --- /dev/null +++ b/tests/UriTraitTest.php @@ -0,0 +1,65 @@ +implementation = new UriTraitImplementation; + } + + public function testExpandsUri() + { + $path = 'narf'; + $baseUri = 'http://www.example.com'; + $uri = $this->implementation->expandUri($baseUri . '/{path}', [ + 'path' => $path + ]); + + $this->assertEquals($baseUri . '/' . $path, $uri); + } + + /** + * @dataProvider queryProvider + */ + public function testBuildsUriWithQuery($expectedQuery, $query) + { + $baseUri = 'http://www.example.com'; + $uri = $this->implementation->buildUriWithQuery($baseUri, $query); + + $this->assertEquals($baseUri . $expectedQuery, (string) $uri); + } + + public function queryProvider() + { + return [ + ['?narf=yes', ['narf' => 'yes']], + ['?narf=true', ['narf' => true]] + ]; + } +} + +class UriTraitImplementation +{ + use UriTrait; +} diff --git a/tests/data/logo.svg b/tests/data/logo.svg new file mode 100644 index 000000000000..90df34205d93 --- /dev/null +++ b/tests/data/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/json-key-fixture.json b/tests/fixtures/json-key-fixture.json new file mode 100644 index 000000000000..b3340ad67ead --- /dev/null +++ b/tests/fixtures/json-key-fixture.json @@ -0,0 +1 @@ +{"type":"authorized_user","client_id":"example@example.com","client_secret":"example","refresh_token":"abc"} \ No newline at end of file diff --git a/tests/fixtures/service-fixture.json b/tests/fixtures/service-fixture.json new file mode 100644 index 000000000000..98d21fce6027 --- /dev/null +++ b/tests/fixtures/service-fixture.json @@ -0,0 +1,36 @@ +{ + "schemas": { + "MyReference": { + "type": "object", + "properties": { + "referenceProp": { + "type": "string" + } + } + } + }, + "resources": { + "myResource": { + "methods": { + "myMethod": { + "path": "{pathParam}", + "httpMethod": "POST", + "parameters": { + "queryParam": { + "type": "string", + "location": "query" + }, + "pathParam": { + "type": "string", + "required": true, + "location": "path" + } + }, + "request": { + "$ref": "MyReference" + } + } + } + } + } +}