Added delete option to database storage.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-12 12:10:01 -04:00
parent 308604a33c
commit 963b5bc68b
1868 changed files with 192402 additions and 13278 deletions

View file

@ -0,0 +1,45 @@
# Copyright 2014 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shortcut methods for getting set up with Google Cloud Storage.
You'll typically use these to get started with the API:
.. literalinclude:: snippets.py
:start-after: [START storage_get_started]
:end-before: [END storage_get_started]
:dedent: 4
The main concepts with this API are:
- :class:`~google.cloud.storage.bucket.Bucket` which represents a particular
bucket (akin to a mounted disk on a computer).
- :class:`~google.cloud.storage.blob.Blob` which represents a pointer to a
particular entity in Cloud Storage (akin to a file path on a remote
machine).
"""
from pkg_resources import get_distribution
__version__ = get_distribution("google-cloud-storage").version
from google.cloud.storage.batch import Batch
from google.cloud.storage.blob import Blob
from google.cloud.storage.bucket import Bucket
from google.cloud.storage.client import Client
__all__ = ["__version__", "Batch", "Blob", "Bucket", "Client"]

View file

@ -0,0 +1,514 @@
# Copyright 2014 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper functions for Cloud Storage utility classes.
These are *not* part of the API.
"""
import base64
from hashlib import md5
from datetime import datetime
import os
from six.moves.urllib.parse import urlsplit
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
STORAGE_EMULATOR_ENV_VAR = "STORAGE_EMULATOR_HOST"
"""Environment variable defining host for Storage emulator."""
_DEFAULT_STORAGE_HOST = u"https://storage.googleapis.com"
# generation match parameters in camel and snake cases
_GENERATION_MATCH_PARAMETERS = (
("if_generation_match", "ifGenerationMatch"),
("if_generation_not_match", "ifGenerationNotMatch"),
("if_metageneration_match", "ifMetagenerationMatch"),
("if_metageneration_not_match", "ifMetagenerationNotMatch"),
("if_source_generation_match", "ifSourceGenerationMatch"),
("if_source_generation_not_match", "ifSourceGenerationNotMatch"),
("if_source_metageneration_match", "ifSourceMetagenerationMatch"),
("if_source_metageneration_not_match", "ifSourceMetagenerationNotMatch"),
)
def _get_storage_host():
return os.environ.get(STORAGE_EMULATOR_ENV_VAR, _DEFAULT_STORAGE_HOST)
def _validate_name(name):
"""Pre-flight ``Bucket`` name validation.
:type name: str or :data:`NoneType`
:param name: Proposed bucket name.
:rtype: str or :data:`NoneType`
:returns: ``name`` if valid.
"""
if name is None:
return
# The first and last characters must be alphanumeric.
if not all([name[0].isalnum(), name[-1].isalnum()]):
raise ValueError("Bucket names must start and end with a number or letter.")
return name
class _PropertyMixin(object):
"""Abstract mixin for cloud storage classes with associated properties.
Non-abstract subclasses should implement:
- path
- client
- user_project
:type name: str
:param name: The name of the object. Bucket names must start and end with a
number or letter.
"""
def __init__(self, name=None):
self.name = name
self._properties = {}
self._changes = set()
@property
def path(self):
"""Abstract getter for the object path."""
raise NotImplementedError
@property
def client(self):
"""Abstract getter for the object client."""
raise NotImplementedError
@property
def user_project(self):
"""Abstract getter for the object user_project."""
raise NotImplementedError
def _require_client(self, client):
"""Check client or verify over-ride.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.
:rtype: :class:`google.cloud.storage.client.Client`
:returns: The client passed in or the currently bound client.
"""
if client is None:
client = self.client
return client
def _encryption_headers(self):
"""Return any encryption headers needed to fetch the object.
.. note::
Defined here because :meth:`reload` calls it, but this method is
really only relevant for :class:`~google.cloud.storage.blob.Blob`.
:rtype: dict
:returns: a mapping of encryption-related headers.
"""
return {}
@property
def _query_params(self):
"""Default query parameters."""
params = {}
if self.user_project is not None:
params["userProject"] = self.user_project
return params
def reload(
self,
client=None,
projection="noAcl",
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Reload properties from Cloud Storage.
If :attr:`user_project` is set, bills the API request to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.
:type projection: str
:param projection: (Optional) If used, must be 'full' or 'noAcl'.
Defaults to ``'noAcl'``. Specifies the set of
properties to return.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.
:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.
:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.
:type if_metageneration_not_match: long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.
"""
client = self._require_client(client)
query_params = self._query_params
# Pass only '?projection=noAcl' here because 'acl' and related
# are handled via custom endpoints.
query_params["projection"] = projection
_add_generation_match_parameters(
query_params,
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
api_response = client._connection.api_request(
method="GET",
path=self.path,
query_params=query_params,
headers=self._encryption_headers(),
_target_object=self,
timeout=timeout,
)
self._set_properties(api_response)
def _patch_property(self, name, value):
"""Update field of this object's properties.
This method will only update the field provided and will not
touch the other fields.
It **will not** reload the properties from the server. The behavior is
local only and syncing occurs via :meth:`patch`.
:type name: str
:param name: The field name to update.
:type value: object
:param value: The value being updated.
"""
self._changes.add(name)
self._properties[name] = value
def _set_properties(self, value):
"""Set the properties for the current object.
:type value: dict or :class:`google.cloud.storage.batch._FutureDict`
:param value: The properties to be set.
"""
self._properties = value
# If the values are reset, the changes must as well.
self._changes = set()
def patch(
self,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Sends all changed properties in a PATCH request.
Updates the ``_properties`` with the response from the backend.
If :attr:`user_project` is set, bills the API request to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.
:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.
:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.
:type if_metageneration_not_match: long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.
"""
client = self._require_client(client)
query_params = self._query_params
# Pass '?projection=full' here because 'PATCH' documented not
# to work properly w/ 'noAcl'.
query_params["projection"] = "full"
_add_generation_match_parameters(
query_params,
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
update_properties = {key: self._properties[key] for key in self._changes}
# Make the API call.
api_response = client._connection.api_request(
method="PATCH",
path=self.path,
data=update_properties,
query_params=query_params,
_target_object=self,
timeout=timeout,
)
self._set_properties(api_response)
def update(
self,
client=None,
timeout=_DEFAULT_TIMEOUT,
if_generation_match=None,
if_generation_not_match=None,
if_metageneration_match=None,
if_metageneration_not_match=None,
):
"""Sends all properties in a PUT request.
Updates the ``_properties`` with the response from the backend.
If :attr:`user_project` is set, bills the API request to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current object.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:type if_generation_match: long
:param if_generation_match: (Optional) Make the operation conditional on whether
the blob's current generation matches the given value.
Setting to 0 makes the operation succeed only if there
are no live versions of the blob.
:type if_generation_not_match: long
:param if_generation_not_match: (Optional) Make the operation conditional on whether
the blob's current generation does not match the given
value. If no live blob exists, the precondition fails.
Setting to 0 makes the operation succeed only if there
is a live version of the blob.
:type if_metageneration_match: long
:param if_metageneration_match: (Optional) Make the operation conditional on whether the
blob's current metageneration matches the given value.
:type if_metageneration_not_match: long
:param if_metageneration_not_match: (Optional) Make the operation conditional on whether the
blob's current metageneration does not match the given value.
"""
client = self._require_client(client)
query_params = self._query_params
query_params["projection"] = "full"
_add_generation_match_parameters(
query_params,
if_generation_match=if_generation_match,
if_generation_not_match=if_generation_not_match,
if_metageneration_match=if_metageneration_match,
if_metageneration_not_match=if_metageneration_not_match,
)
api_response = client._connection.api_request(
method="PUT",
path=self.path,
data=self._properties,
query_params=query_params,
_target_object=self,
timeout=timeout,
)
self._set_properties(api_response)
def _scalar_property(fieldname):
"""Create a property descriptor around the :class:`_PropertyMixin` helpers.
"""
def _getter(self):
"""Scalar property getter."""
return self._properties.get(fieldname)
def _setter(self, value):
"""Scalar property setter."""
self._patch_property(fieldname, value)
return property(_getter, _setter)
def _write_buffer_to_hash(buffer_object, hash_obj, digest_block_size=8192):
"""Read blocks from a buffer and update a hash with them.
:type buffer_object: bytes buffer
:param buffer_object: Buffer containing bytes used to update a hash object.
:type hash_obj: object that implements update
:param hash_obj: A hash object (MD5 or CRC32-C).
:type digest_block_size: int
:param digest_block_size: The block size to write to the hash.
Defaults to 8192.
"""
block = buffer_object.read(digest_block_size)
while len(block) > 0:
hash_obj.update(block)
# Update the block for the next iteration.
block = buffer_object.read(digest_block_size)
def _base64_md5hash(buffer_object):
"""Get MD5 hash of bytes (as base64).
:type buffer_object: bytes buffer
:param buffer_object: Buffer containing bytes used to compute an MD5
hash (as base64).
:rtype: str
:returns: A base64 encoded digest of the MD5 hash.
"""
hash_obj = md5()
_write_buffer_to_hash(buffer_object, hash_obj)
digest_bytes = hash_obj.digest()
return base64.b64encode(digest_bytes)
def _convert_to_timestamp(value):
"""Convert non-none datetime to timestamp.
:type value: :class:`datetime.datetime`
:param value: The datetime to convert.
:rtype: int
:returns: The timestamp.
"""
utc_naive = value.replace(tzinfo=None) - value.utcoffset()
mtime = (utc_naive - datetime(1970, 1, 1)).total_seconds()
return mtime
def _add_generation_match_parameters(parameters, **match_parameters):
"""Add generation match parameters into the given parameters list.
:type parameters: list or dict
:param parameters: Parameters list or dict.
:type match_parameters: dict
:param match_parameters: if*generation*match parameters to add.
:raises: :exc:`ValueError` if ``parameters`` is not a ``list()``
or a ``dict()``.
"""
for snakecase_name, camelcase_name in _GENERATION_MATCH_PARAMETERS:
value = match_parameters.get(snakecase_name)
if value is not None:
if isinstance(parameters, list):
parameters.append((camelcase_name, value))
elif isinstance(parameters, dict):
parameters[camelcase_name] = value
else:
raise ValueError(
"`parameters` argument should be a dict() or a list()."
)
def _raise_if_more_than_one_set(**kwargs):
"""Raise ``ValueError`` exception if more than one parameter was set.
:type error: :exc:`ValueError`
:param error: Description of which fields were set
:raises: :class:`~ValueError` containing the fields that were set
"""
if sum(arg is not None for arg in kwargs.values()) > 1:
escaped_keys = ["'%s'" % name for name in kwargs.keys()]
keys_but_last = ", ".join(escaped_keys[:-1])
last_key = escaped_keys[-1]
msg = "Pass at most one of {keys_but_last} and {last_key}".format(
keys_but_last=keys_but_last, last_key=last_key
)
raise ValueError(msg)
def _bucket_bound_hostname_url(host, scheme=None):
"""Helper to build bucket bound hostname URL.
:type host: str
:param host: Host name.
:type scheme: str
:param scheme: (Optional) Web scheme. If passed, use it
as a scheme in the result URL.
:rtype: str
:returns: A bucket bound hostname URL.
"""
url_parts = urlsplit(host)
if url_parts.scheme and url_parts.netloc:
return host
return "{scheme}://{host}/".format(scheme=scheme, host=host)

View file

@ -0,0 +1,48 @@
# Copyright 2014 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Create / interact with Google Cloud Storage connections."""
from google.cloud import _http
from google.cloud.storage import __version__
class Connection(_http.JSONConnection):
"""A connection to Google Cloud Storage via the JSON REST API.
:type client: :class:`~google.cloud.storage.client.Client`
:param client: The client that owns the current connection.
:type client_info: :class:`~google.api_core.client_info.ClientInfo`
:param client_info: (Optional) instance used to generate user agent.
"""
DEFAULT_API_ENDPOINT = "https://storage.googleapis.com"
def __init__(self, client, client_info=None, api_endpoint=DEFAULT_API_ENDPOINT):
super(Connection, self).__init__(client, client_info)
self.API_BASE_URL = api_endpoint
self._client_info.client_library_version = __version__
# TODO: When metrics all use gccl, this should be removed #9552
if self._client_info.user_agent is None: # pragma: no branch
self._client_info.user_agent = ""
self._client_info.user_agent += " gcloud-python/{} ".format(__version__)
API_VERSION = "v1"
"""The version of the API, used in building the API call's URL."""
API_URL_TEMPLATE = "{api_base_url}/storage/{api_version}{path}"
"""A template for the URL of a particular API call."""

View file

@ -0,0 +1,720 @@
# Copyright 2017 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import binascii
import collections
import datetime
import hashlib
import json
import six
import google.auth.credentials
from google.auth import exceptions
from google.auth.transport import requests
from google.cloud import _helpers
NOW = datetime.datetime.utcnow # To be replaced by tests.
SERVICE_ACCOUNT_URL = (
"https://googleapis.dev/python/google-api-core/latest/"
"auth.html#setting-up-a-service-account"
)
def ensure_signed_credentials(credentials):
"""Raise AttributeError if the credentials are unsigned.
:type credentials: :class:`google.auth.credentials.Signing`
:param credentials: The credentials used to create a private key
for signing text.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.
"""
if not isinstance(credentials, google.auth.credentials.Signing):
raise AttributeError(
"you need a private key to sign credentials."
"the credentials you are currently using {} "
"just contains a token. see {} for more "
"details.".format(type(credentials), SERVICE_ACCOUNT_URL)
)
def get_signed_query_params_v2(credentials, expiration, string_to_sign):
"""Gets query parameters for creating a signed URL.
:type credentials: :class:`google.auth.credentials.Signing`
:param credentials: The credentials used to create a private key
for signing text.
:type expiration: int or long
:param expiration: When the signed URL should expire.
:type string_to_sign: str
:param string_to_sign: The string to be signed by the credentials.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.
:rtype: dict
:returns: Query parameters matching the signing credentials with a
signed payload.
"""
ensure_signed_credentials(credentials)
signature_bytes = credentials.sign_bytes(string_to_sign)
signature = base64.b64encode(signature_bytes)
service_account_name = credentials.signer_email
return {
"GoogleAccessId": service_account_name,
"Expires": expiration,
"Signature": signature,
}
def get_expiration_seconds_v2(expiration):
"""Convert 'expiration' to a number of seconds in the future.
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire. If
a ``datetime`` instance is passed without an explicit
``tzinfo`` set, it will be assumed to be ``UTC``.
:raises: :exc:`TypeError` when expiration is not a valid type.
:rtype: int
:returns: a timestamp as an absolute number of seconds since epoch.
"""
# If it's a timedelta, add it to `now` in UTC.
if isinstance(expiration, datetime.timedelta):
now = NOW().replace(tzinfo=_helpers.UTC)
expiration = now + expiration
# If it's a datetime, convert to a timestamp.
if isinstance(expiration, datetime.datetime):
micros = _helpers._microseconds_from_datetime(expiration)
expiration = micros // 10 ** 6
if not isinstance(expiration, six.integer_types):
raise TypeError(
"Expected an integer timestamp, datetime, or "
"timedelta. Got %s" % type(expiration)
)
return expiration
_EXPIRATION_TYPES = six.integer_types + (datetime.datetime, datetime.timedelta)
def get_expiration_seconds_v4(expiration):
"""Convert 'expiration' to a number of seconds offset from the current time.
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire. If
a ``datetime`` instance is passed without an explicit
``tzinfo`` set, it will be assumed to be ``UTC``.
:raises: :exc:`TypeError` when expiration is not a valid type.
:raises: :exc:`ValueError` when expiration is too large.
:rtype: Integer
:returns: seconds in the future when the signed URL will expire
"""
if not isinstance(expiration, _EXPIRATION_TYPES):
raise TypeError(
"Expected an integer timestamp, datetime, or "
"timedelta. Got %s" % type(expiration)
)
now = NOW().replace(tzinfo=_helpers.UTC)
if isinstance(expiration, six.integer_types):
seconds = expiration
if isinstance(expiration, datetime.datetime):
if expiration.tzinfo is None:
expiration = expiration.replace(tzinfo=_helpers.UTC)
expiration = expiration - now
if isinstance(expiration, datetime.timedelta):
seconds = int(expiration.total_seconds())
if seconds > SEVEN_DAYS:
raise ValueError(
"Max allowed expiration interval is seven days {}".format(SEVEN_DAYS)
)
return seconds
def get_canonical_headers(headers):
"""Canonicalize headers for signing.
See:
https://cloud.google.com/storage/docs/access-control/signed-urls#about-canonical-extension-headers
:type headers: Union[dict|List(Tuple(str,str))]
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.
:rtype: str
:returns: List of headers, normalized / sortted per the URL refernced above.
"""
if headers is None:
headers = []
elif isinstance(headers, dict):
headers = list(headers.items())
if not headers:
return [], []
normalized = collections.defaultdict(list)
for key, val in headers:
key = key.lower().strip()
val = " ".join(val.split())
normalized[key].append(val)
ordered_headers = sorted((key, ",".join(val)) for key, val in normalized.items())
canonical_headers = ["{}:{}".format(*item) for item in ordered_headers]
return canonical_headers, ordered_headers
_Canonical = collections.namedtuple(
"_Canonical", ["method", "resource", "query_parameters", "headers"]
)
def canonicalize_v2(method, resource, query_parameters, headers):
"""Canonicalize method, resource per the V2 spec.
:type method: str
:param method: The HTTP verb that will be used when requesting the URL.
Defaults to ``'GET'``. If method is ``'RESUMABLE'`` then the
signature will additionally contain the `x-goog-resumable`
header, and the method changed to POST. See the signed URL
docs regarding this flow:
https://cloud.google.com/storage/docs/access-control/signed-urls
:type resource: str
:param resource: A pointer to a specific resource
(typically, ``/bucket-name/path/to/blob.txt``).
:type query_parameters: dict
:param query_parameters:
(Optional) Additional query parameters to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers#query
:type headers: Union[dict|List(Tuple(str,str))]
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.
:rtype: :class:_Canonical
:returns: Canonical method, resource, query_parameters, and headers.
"""
headers, _ = get_canonical_headers(headers)
if method == "RESUMABLE":
method = "POST"
headers.append("x-goog-resumable:start")
if query_parameters is None:
return _Canonical(method, resource, [], headers)
normalized_qp = sorted(
(key.lower(), value and value.strip() or "")
for key, value in query_parameters.items()
)
encoded_qp = six.moves.urllib.parse.urlencode(normalized_qp)
canonical_resource = "{}?{}".format(resource, encoded_qp)
return _Canonical(method, canonical_resource, normalized_qp, headers)
def generate_signed_url_v2(
credentials,
resource,
expiration,
api_access_endpoint="",
method="GET",
content_md5=None,
content_type=None,
response_type=None,
response_disposition=None,
generation=None,
headers=None,
query_parameters=None,
service_account_email=None,
access_token=None,
):
"""Generate a V2 signed URL to provide query-string auth'n to a resource.
.. note::
Assumes ``credentials`` implements the
:class:`google.auth.credentials.Signing` interface. Also assumes
``credentials`` has a ``service_account_email`` property which
identifies the credentials.
.. note::
If you are on Google Compute Engine, you can't generate a signed URL.
Follow `Issue 922`_ for updates on this. If you'd like to be able to
generate a signed URL from GCE, you can use a standard service account
from a JSON file rather than a GCE service account.
See headers `reference`_ for more details on optional arguments.
.. _Issue 922: https://github.com/GoogleCloudPlatform/\
google-cloud-python/issues/922
.. _reference: https://cloud.google.com/storage/docs/reference-headers
:type credentials: :class:`google.auth.credentials.Signing`
:param credentials: Credentials object with an associated private key to
sign text.
:type resource: str
:param resource: A pointer to a specific resource
(typically, ``/bucket-name/path/to/blob.txt``).
Caller should have already URL-encoded the value.
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire. If
a ``datetime`` instance is passed without an explicit
``tzinfo`` set, it will be assumed to be ``UTC``.
:type api_access_endpoint: str
:param api_access_endpoint: (Optional) URI base. Defaults to empty string.
:type method: str
:param method: The HTTP verb that will be used when requesting the URL.
Defaults to ``'GET'``. If method is ``'RESUMABLE'`` then the
signature will additionally contain the `x-goog-resumable`
header, and the method changed to POST. See the signed URL
docs regarding this flow:
https://cloud.google.com/storage/docs/access-control/signed-urls
:type content_md5: str
:param content_md5: (Optional) The MD5 hash of the object referenced by
``resource``.
:type content_type: str
:param content_type: (Optional) The content type of the object referenced
by ``resource``.
:type response_type: str
:param response_type: (Optional) Content type of responses to requests for
the signed URL. Ignored if content_type is set on
object/blob metadata.
:type response_disposition: str
:param response_disposition: (Optional) Content disposition of responses to
requests for the signed URL.
:type generation: str
:param generation: (Optional) A value that indicates which generation of
the resource to fetch.
:type headers: Union[dict|List(Tuple(str,str))]
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.
:type service_account_email: str
:param service_account_email: (Optional) E-mail address of the service account.
:type access_token: str
:param access_token: (Optional) Access token for a service account.
:type query_parameters: dict
:param query_parameters:
(Optional) Additional query parameters to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers#query
:raises: :exc:`TypeError` when expiration is not a valid type.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.
:rtype: str
:returns: A signed URL you can use to access the resource
until expiration.
"""
expiration_stamp = get_expiration_seconds_v2(expiration)
canonical = canonicalize_v2(method, resource, query_parameters, headers)
# Generate the string to sign.
elements_to_sign = [
canonical.method,
content_md5 or "",
content_type or "",
str(expiration_stamp),
]
elements_to_sign.extend(canonical.headers)
elements_to_sign.append(canonical.resource)
string_to_sign = "\n".join(elements_to_sign)
# Set the right query parameters.
if access_token and service_account_email:
signature = _sign_message(string_to_sign, access_token, service_account_email)
signed_query_params = {
"GoogleAccessId": service_account_email,
"Expires": expiration_stamp,
"Signature": signature,
}
else:
signed_query_params = get_signed_query_params_v2(
credentials, expiration_stamp, string_to_sign
)
if response_type is not None:
signed_query_params["response-content-type"] = response_type
if response_disposition is not None:
signed_query_params["response-content-disposition"] = response_disposition
if generation is not None:
signed_query_params["generation"] = generation
signed_query_params.update(canonical.query_parameters)
sorted_signed_query_params = sorted(signed_query_params.items())
# Return the built URL.
return "{endpoint}{resource}?{querystring}".format(
endpoint=api_access_endpoint,
resource=resource,
querystring=six.moves.urllib.parse.urlencode(sorted_signed_query_params),
)
SEVEN_DAYS = 7 * 24 * 60 * 60 # max age for V4 signed URLs.
DEFAULT_ENDPOINT = "https://storage.googleapis.com"
def generate_signed_url_v4(
credentials,
resource,
expiration,
api_access_endpoint=DEFAULT_ENDPOINT,
method="GET",
content_md5=None,
content_type=None,
response_type=None,
response_disposition=None,
generation=None,
headers=None,
query_parameters=None,
service_account_email=None,
access_token=None,
_request_timestamp=None, # for testing only
):
"""Generate a V4 signed URL to provide query-string auth'n to a resource.
.. note::
Assumes ``credentials`` implements the
:class:`google.auth.credentials.Signing` interface. Also assumes
``credentials`` has a ``service_account_email`` property which
identifies the credentials.
.. note::
If you are on Google Compute Engine, you can't generate a signed URL.
Follow `Issue 922`_ for updates on this. If you'd like to be able to
generate a signed URL from GCE, you can use a standard service account
from a JSON file rather than a GCE service account.
See headers `reference`_ for more details on optional arguments.
.. _Issue 922: https://github.com/GoogleCloudPlatform/\
google-cloud-python/issues/922
.. _reference: https://cloud.google.com/storage/docs/reference-headers
:type credentials: :class:`google.auth.credentials.Signing`
:param credentials: Credentials object with an associated private key to
sign text.
:type resource: str
:param resource: A pointer to a specific resource
(typically, ``/bucket-name/path/to/blob.txt``).
Caller should have already URL-encoded the value.
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire. If
a ``datetime`` instance is passed without an explicit
``tzinfo`` set, it will be assumed to be ``UTC``.
:type api_access_endpoint: str
:param api_access_endpoint: (Optional) URI base. Defaults to
"https://storage.googleapis.com/"
:type method: str
:param method: The HTTP verb that will be used when requesting the URL.
Defaults to ``'GET'``. If method is ``'RESUMABLE'`` then the
signature will additionally contain the `x-goog-resumable`
header, and the method changed to POST. See the signed URL
docs regarding this flow:
https://cloud.google.com/storage/docs/access-control/signed-urls
:type content_md5: str
:param content_md5: (Optional) The MD5 hash of the object referenced by
``resource``.
:type content_type: str
:param content_type: (Optional) The content type of the object referenced
by ``resource``.
:type response_type: str
:param response_type: (Optional) Content type of responses to requests for
the signed URL. Ignored if content_type is set on
object/blob metadata.
:type response_disposition: str
:param response_disposition: (Optional) Content disposition of responses to
requests for the signed URL.
:type generation: str
:param generation: (Optional) A value that indicates which generation of
the resource to fetch.
:type headers: dict
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.
:type query_parameters: dict
:param query_parameters:
(Optional) Additional query parameters to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers#query
:type service_account_email: str
:param service_account_email: (Optional) E-mail address of the service account.
:type access_token: str
:param access_token: (Optional) Access token for a service account.
:raises: :exc:`TypeError` when expiration is not a valid type.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.
:rtype: str
:returns: A signed URL you can use to access the resource
until expiration.
"""
ensure_signed_credentials(credentials)
expiration_seconds = get_expiration_seconds_v4(expiration)
if _request_timestamp is None:
request_timestamp, datestamp = get_v4_now_dtstamps()
else:
request_timestamp = _request_timestamp
datestamp = _request_timestamp[:8]
client_email = credentials.signer_email
credential_scope = "{}/auto/storage/goog4_request".format(datestamp)
credential = "{}/{}".format(client_email, credential_scope)
if headers is None:
headers = {}
if content_type is not None:
headers["Content-Type"] = content_type
if content_md5 is not None:
headers["Content-MD5"] = content_md5
header_names = [key.lower() for key in headers]
if "host" not in header_names:
headers["Host"] = six.moves.urllib.parse.urlparse(api_access_endpoint).netloc
if method.upper() == "RESUMABLE":
method = "POST"
headers["x-goog-resumable"] = "start"
canonical_headers, ordered_headers = get_canonical_headers(headers)
canonical_header_string = (
"\n".join(canonical_headers) + "\n"
) # Yes, Virginia, the extra newline is part of the spec.
signed_headers = ";".join([key for key, _ in ordered_headers])
if query_parameters is None:
query_parameters = {}
else:
query_parameters = {key: value or "" for key, value in query_parameters.items()}
query_parameters["X-Goog-Algorithm"] = "GOOG4-RSA-SHA256"
query_parameters["X-Goog-Credential"] = credential
query_parameters["X-Goog-Date"] = request_timestamp
query_parameters["X-Goog-Expires"] = expiration_seconds
query_parameters["X-Goog-SignedHeaders"] = signed_headers
if response_type is not None:
query_parameters["response-content-type"] = response_type
if response_disposition is not None:
query_parameters["response-content-disposition"] = response_disposition
if generation is not None:
query_parameters["generation"] = generation
canonical_query_string = _url_encode(query_parameters)
lowercased_headers = dict(ordered_headers)
if "x-goog-content-sha256" in lowercased_headers:
payload = lowercased_headers["x-goog-content-sha256"]
else:
payload = "UNSIGNED-PAYLOAD"
canonical_elements = [
method,
resource,
canonical_query_string,
canonical_header_string,
signed_headers,
payload,
]
canonical_request = "\n".join(canonical_elements)
canonical_request_hash = hashlib.sha256(
canonical_request.encode("ascii")
).hexdigest()
string_elements = [
"GOOG4-RSA-SHA256",
request_timestamp,
credential_scope,
canonical_request_hash,
]
string_to_sign = "\n".join(string_elements)
if access_token and service_account_email:
signature = _sign_message(string_to_sign, access_token, service_account_email)
signature_bytes = base64.b64decode(signature)
signature = binascii.hexlify(signature_bytes).decode("ascii")
else:
signature_bytes = credentials.sign_bytes(string_to_sign.encode("ascii"))
signature = binascii.hexlify(signature_bytes).decode("ascii")
return "{}{}?{}&X-Goog-Signature={}".format(
api_access_endpoint, resource, canonical_query_string, signature
)
def get_v4_now_dtstamps():
"""Get current timestamp and datestamp in V4 valid format.
:rtype: str, str
:returns: Current timestamp, datestamp.
"""
now = NOW()
timestamp = now.strftime("%Y%m%dT%H%M%SZ")
datestamp = now.date().strftime("%Y%m%d")
return timestamp, datestamp
def _sign_message(message, access_token, service_account_email):
"""Signs a message.
:type message: str
:param message: The message to be signed.
:type access_token: str
:param access_token: Access token for a service account.
:type service_account_email: str
:param service_account_email: E-mail address of the service account.
:raises: :exc:`TransportError` if an `access_token` is unauthorized.
:rtype: str
:returns: The signature of the message.
"""
message = _helpers._to_bytes(message)
method = "POST"
url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:signBlob?alt=json".format(
service_account_email
)
headers = {
"Authorization": "Bearer " + access_token,
"Content-type": "application/json",
}
body = json.dumps({"payload": base64.b64encode(message).decode("utf-8")})
request = requests.Request()
response = request(url=url, method=method, body=body, headers=headers)
if response.status != six.moves.http_client.OK:
raise exceptions.TransportError(
"Error calling the IAM signBytes API: {}".format(response.data)
)
data = json.loads(response.data.decode("utf-8"))
return data["signedBlob"]
def _url_encode(query_params):
"""Encode query params into URL.
:type query_params: dict
:param query_params: Query params to be encoded.
:rtype: str
:returns: URL encoded query params.
"""
params = [
"{}={}".format(_quote_param(name), _quote_param(value))
for name, value in query_params.items()
]
return "&".join(sorted(params))
def _quote_param(param):
"""Quote query param.
:type param: Any
:param param: Query param to be encoded.
:rtype: str
:returns: URL encoded query param.
"""
if not isinstance(param, bytes):
param = str(param)
return six.moves.urllib.parse.quote(param, safe="~")

View file

@ -0,0 +1,657 @@
# Copyright 2014 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Manipulate access control lists that Cloud Storage provides.
:class:`google.cloud.storage.bucket.Bucket` has a getting method that creates
an ACL object under the hood, and you can interact with that using
:func:`google.cloud.storage.bucket.Bucket.acl`:
.. literalinclude:: snippets.py
:start-after: [START client_bucket_acl]
:end-before: [END client_bucket_acl]
:dedent: 4
Adding and removing permissions can be done with the following methods
(in increasing order of granularity):
- :func:`ACL.all`
corresponds to access for all users.
- :func:`ACL.all_authenticated` corresponds
to access for all users that are signed into a Google account.
- :func:`ACL.domain` corresponds to access on a
per Google Apps domain (ie, ``example.com``).
- :func:`ACL.group` corresponds to access on a
per group basis (either by ID or e-mail address).
- :func:`ACL.user` corresponds to access on a
per user basis (either by ID or e-mail address).
And you are able to ``grant`` and ``revoke`` the following roles:
- **Reading**:
:func:`_ACLEntity.grant_read` and :func:`_ACLEntity.revoke_read`
- **Writing**:
:func:`_ACLEntity.grant_write` and :func:`_ACLEntity.revoke_write`
- **Owning**:
:func:`_ACLEntity.grant_owner` and :func:`_ACLEntity.revoke_owner`
You can use any of these like any other factory method (these happen to
be :class:`_ACLEntity` factories):
.. literalinclude:: snippets.py
:start-after: [START acl_user_settings]
:end-before: [END acl_user_settings]
:dedent: 4
After that, you can save any changes you make with the
:func:`google.cloud.storage.acl.ACL.save` method:
.. literalinclude:: snippets.py
:start-after: [START acl_save]
:end-before: [END acl_save]
:dedent: 4
You can alternatively save any existing :class:`google.cloud.storage.acl.ACL`
object (whether it was created by a factory method or not) from a
:class:`google.cloud.storage.bucket.Bucket`:
.. literalinclude:: snippets.py
:start-after: [START acl_save_bucket]
:end-before: [END acl_save_bucket]
:dedent: 4
To get the list of ``entity`` and ``role`` for each unique pair, the
:class:`ACL` class is iterable:
.. literalinclude:: snippets.py
:start-after: [START acl_print]
:end-before: [END acl_print]
:dedent: 4
This list of tuples can be used as the ``entity`` and ``role`` fields
when sending metadata for ACLs to the API.
"""
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
class _ACLEntity(object):
"""Class representing a set of roles for an entity.
This is a helper class that you likely won't ever construct
outside of using the factor methods on the :class:`ACL` object.
:type entity_type: str
:param entity_type: The type of entity (ie, 'group' or 'user').
:type identifier: str
:param identifier: (Optional) The ID or e-mail of the entity. For the special
entity types (like 'allUsers').
"""
READER_ROLE = "READER"
WRITER_ROLE = "WRITER"
OWNER_ROLE = "OWNER"
def __init__(self, entity_type, identifier=None):
self.identifier = identifier
self.roles = set([])
self.type = entity_type
def __str__(self):
if not self.identifier:
return str(self.type)
else:
return "{acl.type}-{acl.identifier}".format(acl=self)
def __repr__(self):
return "<ACL Entity: {acl} ({roles})>".format(
acl=self, roles=", ".join(self.roles)
)
def get_roles(self):
"""Get the list of roles permitted by this entity.
:rtype: list of strings
:returns: The list of roles associated with this entity.
"""
return self.roles
def grant(self, role):
"""Add a role to the entity.
:type role: str
:param role: The role to add to the entity.
"""
self.roles.add(role)
def revoke(self, role):
"""Remove a role from the entity.
:type role: str
:param role: The role to remove from the entity.
"""
if role in self.roles:
self.roles.remove(role)
def grant_read(self):
"""Grant read access to the current entity."""
self.grant(_ACLEntity.READER_ROLE)
def grant_write(self):
"""Grant write access to the current entity."""
self.grant(_ACLEntity.WRITER_ROLE)
def grant_owner(self):
"""Grant owner access to the current entity."""
self.grant(_ACLEntity.OWNER_ROLE)
def revoke_read(self):
"""Revoke read access from the current entity."""
self.revoke(_ACLEntity.READER_ROLE)
def revoke_write(self):
"""Revoke write access from the current entity."""
self.revoke(_ACLEntity.WRITER_ROLE)
def revoke_owner(self):
"""Revoke owner access from the current entity."""
self.revoke(_ACLEntity.OWNER_ROLE)
class ACL(object):
"""Container class representing a list of access controls."""
_URL_PATH_ELEM = "acl"
_PREDEFINED_QUERY_PARAM = "predefinedAcl"
PREDEFINED_XML_ACLS = {
# XML API name -> JSON API name
"project-private": "projectPrivate",
"public-read": "publicRead",
"public-read-write": "publicReadWrite",
"authenticated-read": "authenticatedRead",
"bucket-owner-read": "bucketOwnerRead",
"bucket-owner-full-control": "bucketOwnerFullControl",
}
PREDEFINED_JSON_ACLS = frozenset(
[
"private",
"projectPrivate",
"publicRead",
"publicReadWrite",
"authenticatedRead",
"bucketOwnerRead",
"bucketOwnerFullControl",
]
)
"""See
https://cloud.google.com/storage/docs/access-control/lists#predefined-acl
"""
loaded = False
# Subclasses must override to provide these attributes (typically,
# as properties).
reload_path = None
save_path = None
user_project = None
def __init__(self):
self.entities = {}
def _ensure_loaded(self, timeout=_DEFAULT_TIMEOUT):
"""Load if not already loaded.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
if not self.loaded:
self.reload(timeout=timeout)
@classmethod
def validate_predefined(cls, predefined):
"""Ensures predefined is in list of predefined json values
:type predefined: str
:param predefined: name of a predefined acl
:type predefined: str
:param predefined: validated JSON name of predefined acl
:raises: :exc: `ValueError`: If predefined is not a valid acl
"""
predefined = cls.PREDEFINED_XML_ACLS.get(predefined, predefined)
if predefined and predefined not in cls.PREDEFINED_JSON_ACLS:
raise ValueError("Invalid predefined ACL: %s" % (predefined,))
return predefined
def reset(self):
"""Remove all entities from the ACL, and clear the ``loaded`` flag."""
self.entities.clear()
self.loaded = False
def __iter__(self):
self._ensure_loaded()
for entity in self.entities.values():
for role in entity.get_roles():
if role:
yield {"entity": str(entity), "role": role}
def entity_from_dict(self, entity_dict):
"""Build an _ACLEntity object from a dictionary of data.
An entity is a mutable object that represents a list of roles
belonging to either a user or group or the special types for all
users and all authenticated users.
:type entity_dict: dict
:param entity_dict: Dictionary full of data from an ACL lookup.
:rtype: :class:`_ACLEntity`
:returns: An Entity constructed from the dictionary.
"""
entity = entity_dict["entity"]
role = entity_dict["role"]
if entity == "allUsers":
entity = self.all()
elif entity == "allAuthenticatedUsers":
entity = self.all_authenticated()
elif "-" in entity:
entity_type, identifier = entity.split("-", 1)
entity = self.entity(entity_type=entity_type, identifier=identifier)
if not isinstance(entity, _ACLEntity):
raise ValueError("Invalid dictionary: %s" % entity_dict)
entity.grant(role)
return entity
def has_entity(self, entity):
"""Returns whether or not this ACL has any entries for an entity.
:type entity: :class:`_ACLEntity`
:param entity: The entity to check for existence in this ACL.
:rtype: bool
:returns: True of the entity exists in the ACL.
"""
self._ensure_loaded()
return str(entity) in self.entities
def get_entity(self, entity, default=None):
"""Gets an entity object from the ACL.
:type entity: :class:`_ACLEntity` or string
:param entity: The entity to get lookup in the ACL.
:type default: anything
:param default: This value will be returned if the entity
doesn't exist.
:rtype: :class:`_ACLEntity`
:returns: The corresponding entity or the value provided
to ``default``.
"""
self._ensure_loaded()
return self.entities.get(str(entity), default)
def add_entity(self, entity):
"""Add an entity to the ACL.
:type entity: :class:`_ACLEntity`
:param entity: The entity to add to this ACL.
"""
self._ensure_loaded()
self.entities[str(entity)] = entity
def entity(self, entity_type, identifier=None):
"""Factory method for creating an Entity.
If an entity with the same type and identifier already exists,
this will return a reference to that entity. If not, it will
create a new one and add it to the list of known entities for
this ACL.
:type entity_type: str
:param entity_type: The type of entity to create
(ie, ``user``, ``group``, etc)
:type identifier: str
:param identifier: The ID of the entity (if applicable).
This can be either an ID or an e-mail address.
:rtype: :class:`_ACLEntity`
:returns: A new Entity or a reference to an existing identical entity.
"""
entity = _ACLEntity(entity_type=entity_type, identifier=identifier)
if self.has_entity(entity):
entity = self.get_entity(entity)
else:
self.add_entity(entity)
return entity
def user(self, identifier):
"""Factory method for a user Entity.
:type identifier: str
:param identifier: An id or e-mail for this particular user.
:rtype: :class:`_ACLEntity`
:returns: An Entity corresponding to this user.
"""
return self.entity("user", identifier=identifier)
def group(self, identifier):
"""Factory method for a group Entity.
:type identifier: str
:param identifier: An id or e-mail for this particular group.
:rtype: :class:`_ACLEntity`
:returns: An Entity corresponding to this group.
"""
return self.entity("group", identifier=identifier)
def domain(self, domain):
"""Factory method for a domain Entity.
:type domain: str
:param domain: The domain for this entity.
:rtype: :class:`_ACLEntity`
:returns: An entity corresponding to this domain.
"""
return self.entity("domain", identifier=domain)
def all(self):
"""Factory method for an Entity representing all users.
:rtype: :class:`_ACLEntity`
:returns: An entity representing all users.
"""
return self.entity("allUsers")
def all_authenticated(self):
"""Factory method for an Entity representing all authenticated users.
:rtype: :class:`_ACLEntity`
:returns: An entity representing all authenticated users.
"""
return self.entity("allAuthenticatedUsers")
def get_entities(self):
"""Get a list of all Entity objects.
:rtype: list of :class:`_ACLEntity` objects
:returns: A list of all Entity objects.
"""
self._ensure_loaded()
return list(self.entities.values())
@property
def client(self):
"""Abstract getter for the object client."""
raise NotImplementedError
def _require_client(self, client):
"""Check client or verify over-ride.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use. If not passed, falls back to the
``client`` stored on the current ACL.
:rtype: :class:`google.cloud.storage.client.Client`
:returns: The client passed in or the currently bound client.
"""
if client is None:
client = self.client
return client
def reload(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""Reload the ACL data from Cloud Storage.
If :attr:`user_project` is set, bills the API request to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the ACL's parent.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
path = self.reload_path
client = self._require_client(client)
query_params = {}
if self.user_project is not None:
query_params["userProject"] = self.user_project
self.entities.clear()
found = client._connection.api_request(
method="GET", path=path, query_params=query_params, timeout=timeout
)
self.loaded = True
for entry in found.get("items", ()):
self.add_entity(self.entity_from_dict(entry))
def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT):
"""Helper for :meth:`save` and :meth:`save_predefined`.
:type acl: :class:`google.cloud.storage.acl.ACL`, or a compatible list.
:param acl: The ACL object to save. If left blank, this will save
current entries.
:type predefined: str
:param predefined: An identifier for a predefined ACL. Must be one of the
keys in :attr:`PREDEFINED_JSON_ACLS` If passed, `acl` must be None.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the ACL's parent.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
query_params = {"projection": "full"}
if predefined is not None:
acl = []
query_params[self._PREDEFINED_QUERY_PARAM] = predefined
if self.user_project is not None:
query_params["userProject"] = self.user_project
path = self.save_path
client = self._require_client(client)
result = client._connection.api_request(
method="PATCH",
path=path,
data={self._URL_PATH_ELEM: list(acl)},
query_params=query_params,
timeout=timeout,
)
self.entities.clear()
for entry in result.get(self._URL_PATH_ELEM, ()):
self.add_entity(self.entity_from_dict(entry))
self.loaded = True
def save(self, acl=None, client=None, timeout=_DEFAULT_TIMEOUT):
"""Save this ACL for the current bucket.
If :attr:`user_project` is set, bills the API request to that project.
:type acl: :class:`google.cloud.storage.acl.ACL`, or a compatible list.
:param acl: The ACL object to save. If left blank, this will save
current entries.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the ACL's parent.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
if acl is None:
acl = self
save_to_backend = acl.loaded
else:
save_to_backend = True
if save_to_backend:
self._save(acl, None, client, timeout=timeout)
def save_predefined(self, predefined, client=None, timeout=_DEFAULT_TIMEOUT):
"""Save this ACL for the current bucket using a predefined ACL.
If :attr:`user_project` is set, bills the API request to that project.
:type predefined: str
:param predefined: An identifier for a predefined ACL. Must be one
of the keys in :attr:`PREDEFINED_JSON_ACLS`
or :attr:`PREDEFINED_XML_ACLS` (which will be
aliased to the corresponding JSON name).
If passed, `acl` must be None.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the ACL's parent.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
predefined = self.validate_predefined(predefined)
self._save(None, predefined, client, timeout=timeout)
def clear(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""Remove all ACL entries.
If :attr:`user_project` is set, bills the API request to that project.
Note that this won't actually remove *ALL* the rules, but it
will remove all the non-default rules. In short, you'll still
have access to a bucket that you created even after you clear
ACL rules with this method.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the ACL's parent.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
self.save([], client=client, timeout=timeout)
class BucketACL(ACL):
"""An ACL specifically for a bucket.
:type bucket: :class:`google.cloud.storage.bucket.Bucket`
:param bucket: The bucket to which this ACL relates.
"""
def __init__(self, bucket):
super(BucketACL, self).__init__()
self.bucket = bucket
@property
def client(self):
"""The client bound to this ACL's bucket."""
return self.bucket.client
@property
def reload_path(self):
"""Compute the path for GET API requests for this ACL."""
return "%s/%s" % (self.bucket.path, self._URL_PATH_ELEM)
@property
def save_path(self):
"""Compute the path for PATCH API requests for this ACL."""
return self.bucket.path
@property
def user_project(self):
"""Compute the user project charged for API requests for this ACL."""
return self.bucket.user_project
class DefaultObjectACL(BucketACL):
"""A class representing the default object ACL for a bucket."""
_URL_PATH_ELEM = "defaultObjectAcl"
_PREDEFINED_QUERY_PARAM = "predefinedDefaultObjectAcl"
class ObjectACL(ACL):
"""An ACL specifically for a Cloud Storage object / blob.
:type blob: :class:`google.cloud.storage.blob.Blob`
:param blob: The blob that this ACL corresponds to.
"""
def __init__(self, blob):
super(ObjectACL, self).__init__()
self.blob = blob
@property
def client(self):
"""The client bound to this ACL's blob."""
return self.blob.client
@property
def reload_path(self):
"""Compute the path for GET API requests for this ACL."""
return "%s/acl" % self.blob.path
@property
def save_path(self):
"""Compute the path for PATCH API requests for this ACL."""
return self.blob.path
@property
def user_project(self):
"""Compute the user project charged for API requests for this ACL."""
return self.blob.user_project

View file

@ -0,0 +1,348 @@
# Copyright 2014 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Batch updates / deletes of storage buckets / blobs.
See https://cloud.google.com/storage/docs/json_api/v1/how-tos/batch
"""
from email.encoders import encode_noop
from email.generator import Generator
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.parser import Parser
import io
import json
import requests
import six
from google.cloud import _helpers
from google.cloud import exceptions
from google.cloud.storage._http import Connection
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
class MIMEApplicationHTTP(MIMEApplication):
"""MIME type for ``application/http``.
Constructs payload from headers and body
:type method: str
:param method: HTTP method
:type uri: str
:param uri: URI for HTTP request
:type headers: dict
:param headers: HTTP headers
:type body: str
:param body: (Optional) HTTP payload
"""
def __init__(self, method, uri, headers, body):
if isinstance(body, dict):
body = json.dumps(body)
headers["Content-Type"] = "application/json"
headers["Content-Length"] = len(body)
if body is None:
body = ""
lines = ["%s %s HTTP/1.1" % (method, uri)]
lines.extend(
["%s: %s" % (key, value) for key, value in sorted(headers.items())]
)
lines.append("")
lines.append(body)
payload = "\r\n".join(lines)
if six.PY2:
# email.message.Message is an old-style class, so we
# cannot use 'super()'.
MIMEApplication.__init__(self, payload, "http", encode_noop)
else: # pragma: NO COVER Python3
super_init = super(MIMEApplicationHTTP, self).__init__
super_init(payload, "http", encode_noop)
class _FutureDict(object):
"""Class to hold a future value for a deferred request.
Used by for requests that get sent in a :class:`Batch`.
"""
@staticmethod
def get(key, default=None):
"""Stand-in for dict.get.
:type key: object
:param key: Hashable dictionary key.
:type default: object
:param default: Fallback value to dict.get.
:raises: :class:`KeyError` always since the future is intended to fail
as a dictionary.
"""
raise KeyError("Cannot get(%r, default=%r) on a future" % (key, default))
def __getitem__(self, key):
"""Stand-in for dict[key].
:type key: object
:param key: Hashable dictionary key.
:raises: :class:`KeyError` always since the future is intended to fail
as a dictionary.
"""
raise KeyError("Cannot get item %r from a future" % (key,))
def __setitem__(self, key, value):
"""Stand-in for dict[key] = value.
:type key: object
:param key: Hashable dictionary key.
:type value: object
:param value: Dictionary value.
:raises: :class:`KeyError` always since the future is intended to fail
as a dictionary.
"""
raise KeyError("Cannot set %r -> %r on a future" % (key, value))
class _FutureResponse(requests.Response):
"""Reponse that returns a placeholder dictionary for a batched requests."""
def __init__(self, future_dict):
super(_FutureResponse, self).__init__()
self._future_dict = future_dict
self.status_code = 204
def json(self):
return self._future_dict
@property
def content(self):
return self._future_dict
class Batch(Connection):
"""Proxy an underlying connection, batching up change operations.
:type client: :class:`google.cloud.storage.client.Client`
:param client: The client to use for making connections.
"""
_MAX_BATCH_SIZE = 1000
def __init__(self, client):
super(Batch, self).__init__(client)
self._requests = []
self._target_objects = []
def _do_request(
self, method, url, headers, data, target_object, timeout=_DEFAULT_TIMEOUT
):
"""Override Connection: defer actual HTTP request.
Only allow up to ``_MAX_BATCH_SIZE`` requests to be deferred.
:type method: str
:param method: The HTTP method to use in the request.
:type url: str
:param url: The URL to send the request to.
:type headers: dict
:param headers: A dictionary of HTTP headers to send with the request.
:type data: str
:param data: The data to send as the body of the request.
:type target_object: object
:param target_object:
(Optional) This allows us to enable custom behavior in our batch
connection. Here we defer an HTTP request and complete
initialization of the object at a later time.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:rtype: tuple of ``response`` (a dictionary of sorts)
and ``content`` (a string).
:returns: The HTTP response object and the content of the response.
"""
if len(self._requests) >= self._MAX_BATCH_SIZE:
raise ValueError(
"Too many deferred requests (max %d)" % self._MAX_BATCH_SIZE
)
self._requests.append((method, url, headers, data, timeout))
result = _FutureDict()
self._target_objects.append(target_object)
if target_object is not None:
target_object._properties = result
return _FutureResponse(result)
def _prepare_batch_request(self):
"""Prepares headers and body for a batch request.
:rtype: tuple (dict, str)
:returns: The pair of headers and body of the batch request to be sent.
:raises: :class:`ValueError` if no requests have been deferred.
"""
if len(self._requests) == 0:
raise ValueError("No deferred requests")
multi = MIMEMultipart()
# Use timeout of last request, default to _DEFAULT_TIMEOUT
timeout = _DEFAULT_TIMEOUT
for method, uri, headers, body, _timeout in self._requests:
subrequest = MIMEApplicationHTTP(method, uri, headers, body)
multi.attach(subrequest)
timeout = _timeout
# The `email` package expects to deal with "native" strings
if six.PY2: # pragma: NO COVER Python3
buf = io.BytesIO()
else:
buf = io.StringIO()
generator = Generator(buf, False, 0)
generator.flatten(multi)
payload = buf.getvalue()
# Strip off redundant header text
_, body = payload.split("\n\n", 1)
return dict(multi._headers), body, timeout
def _finish_futures(self, responses):
"""Apply all the batch responses to the futures created.
:type responses: list of (headers, payload) tuples.
:param responses: List of headers and payloads from each response in
the batch.
:raises: :class:`ValueError` if no requests have been deferred.
"""
# If a bad status occurs, we track it, but don't raise an exception
# until all futures have been populated.
exception_args = None
if len(self._target_objects) != len(responses): # pragma: NO COVER
raise ValueError("Expected a response for every request.")
for target_object, subresponse in zip(self._target_objects, responses):
if not 200 <= subresponse.status_code < 300:
exception_args = exception_args or subresponse
elif target_object is not None:
try:
target_object._properties = subresponse.json()
except ValueError:
target_object._properties = subresponse.content
if exception_args is not None:
raise exceptions.from_http_response(exception_args)
def finish(self):
"""Submit a single `multipart/mixed` request with deferred requests.
:rtype: list of tuples
:returns: one ``(headers, payload)`` tuple per deferred request.
"""
headers, body, timeout = self._prepare_batch_request()
url = "%s/batch/storage/v1" % self.API_BASE_URL
# Use the private ``_base_connection`` rather than the property
# ``_connection``, since the property may be this
# current batch.
response = self._client._base_connection._make_request(
"POST", url, data=body, headers=headers, timeout=timeout
)
responses = list(_unpack_batch_response(response))
self._finish_futures(responses)
return responses
def current(self):
"""Return the topmost batch, or None."""
return self._client.current_batch
def __enter__(self):
self._client._push_batch(self)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
try:
if exc_type is None:
self.finish()
finally:
self._client._pop_batch()
def _generate_faux_mime_message(parser, response):
"""Convert response, content -> (multipart) email.message.
Helper for _unpack_batch_response.
"""
# We coerce to bytes to get consistent concat across
# Py2 and Py3. Percent formatting is insufficient since
# it includes the b in Py3.
content_type = _helpers._to_bytes(response.headers.get("content-type", ""))
faux_message = b"".join(
[b"Content-Type: ", content_type, b"\nMIME-Version: 1.0\n\n", response.content]
)
if six.PY2:
return parser.parsestr(faux_message)
else: # pragma: NO COVER Python3
return parser.parsestr(faux_message.decode("utf-8"))
def _unpack_batch_response(response):
"""Convert requests.Response -> [(headers, payload)].
Creates a generator of tuples of emulating the responses to
:meth:`requests.Session.request`.
:type response: :class:`requests.Response`
:param response: HTTP response / headers from a request.
"""
parser = Parser()
message = _generate_faux_mime_message(parser, response)
if not isinstance(message._payload, list): # pragma: NO COVER
raise ValueError("Bad response: not multi-part")
for subrequest in message._payload:
status_line, rest = subrequest._payload.split("\n", 1)
_, status, _ = status_line.split(" ", 2)
sub_message = parser.parsestr(rest)
payload = sub_message._payload
msg_headers = dict(sub_message._headers)
content_id = msg_headers.get("Content-ID")
subresponse = requests.Response()
subresponse.request = requests.Request(
method="BATCH", url="contentid://{}".format(content_id)
).prepare()
subresponse.status_code = int(status)
subresponse.headers.update(msg_headers)
subresponse._content = payload.encode("utf-8")
yield subresponse

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,98 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants used acros google.cloud.storage modules."""
# Storage classes
STANDARD_STORAGE_CLASS = "STANDARD"
"""Storage class for objects accessed more than once per month.
See: https://cloud.google.com/storage/docs/storage-classes
"""
NEARLINE_STORAGE_CLASS = "NEARLINE"
"""Storage class for objects accessed at most once per month.
See: https://cloud.google.com/storage/docs/storage-classes
"""
COLDLINE_STORAGE_CLASS = "COLDLINE"
"""Storage class for objects accessed at most once per year.
See: https://cloud.google.com/storage/docs/storage-classes
"""
ARCHIVE_STORAGE_CLASS = "ARCHIVE"
"""Storage class for objects accessed less frequently than once per year.
See: https://cloud.google.com/storage/docs/storage-classes
"""
MULTI_REGIONAL_LEGACY_STORAGE_CLASS = "MULTI_REGIONAL"
"""Legacy storage class.
Alias for :attr:`STANDARD_STORAGE_CLASS`.
Can only be used for objects in buckets whose
:attr:`~google.cloud.storage.bucket.Bucket.location_type` is
:attr:`~google.cloud.storage.bucket.Bucket.MULTI_REGION_LOCATION_TYPE`.
See: https://cloud.google.com/storage/docs/storage-classes
"""
REGIONAL_LEGACY_STORAGE_CLASS = "REGIONAL"
"""Legacy storage class.
Alias for :attr:`STANDARD_STORAGE_CLASS`.
Can only be used for objects in buckets whose
:attr:`~google.cloud.storage.bucket.Bucket.location_type` is
:attr:`~google.cloud.storage.bucket.Bucket.REGION_LOCATION_TYPE`.
See: https://cloud.google.com/storage/docs/storage-classes
"""
DURABLE_REDUCED_AVAILABILITY_LEGACY_STORAGE_CLASS = "DURABLE_REDUCED_AVAILABILITY"
"""Legacy storage class.
Similar to :attr:`NEARLINE_STORAGE_CLASS`.
"""
# Location types
MULTI_REGION_LOCATION_TYPE = "multi-region"
"""Location type: data will be replicated across regions in a multi-region.
Provides highest availability across largest area.
"""
REGION_LOCATION_TYPE = "region"
"""Location type: data will be stored within a single region.
Provides lowest latency within a single region.
"""
DUAL_REGION_LOCATION_TYPE = "dual-region"
"""Location type: data will be stored within two primary regions.
Provides high availability and low latency across two regions.
"""
# Internal constants
_DEFAULT_TIMEOUT = 60 # in seconds
"""The default request timeout in seconds if a timeout is not explicitly given.
"""

View file

@ -0,0 +1,287 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.cloud.exceptions import NotFound
from google.cloud._helpers import _rfc3339_to_datetime
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
class HMACKeyMetadata(object):
"""Metadata about an HMAC service account key withn Cloud Storage.
:type client: :class:`~google.cloud.stoage.client.Client`
:param client: client associated with the key metadata.
:type access_id: str
:param access_id: (Optional) Unique ID of an existing key.
:type project_id: str
:param project_id: (Optional) Project ID of an existing key.
Defaults to client's project.
:type user_project: str
:param user_project: (Optional) This parameter is currently ignored.
"""
ACTIVE_STATE = "ACTIVE"
"""Key is active, and may be used to sign requests."""
INACTIVE_STATE = "INACTIVE"
"""Key is inactive, and may not be used to sign requests.
It can be re-activated via :meth:`update`.
"""
DELETED_STATE = "DELETED"
"""Key is deleted. It cannot be re-activated."""
_SETTABLE_STATES = (ACTIVE_STATE, INACTIVE_STATE)
def __init__(self, client, access_id=None, project_id=None, user_project=None):
self._client = client
self._properties = {}
if access_id is not None:
self._properties["accessId"] = access_id
if project_id is not None:
self._properties["projectId"] = project_id
self._user_project = user_project
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._client == other._client and self.access_id == other.access_id
def __hash__(self):
return hash(self._client) + hash(self.access_id)
@property
def access_id(self):
"""Access ID of the key.
:rtype: str or None
:returns: unique identifier of the key within a project.
"""
return self._properties.get("accessId")
@property
def etag(self):
"""ETag identifying the version of the key metadata.
:rtype: str or None
:returns: ETag for the version of the key's metadata.
"""
return self._properties.get("etag")
@property
def id(self):
"""ID of the key, including the Project ID and the Access ID.
:rtype: str or None
:returns: ID of the key.
"""
return self._properties.get("id")
@property
def project(self):
"""Project ID associated with the key.
:rtype: str or None
:returns: project identfier for the key.
"""
return self._properties.get("projectId")
@property
def service_account_email(self):
"""Service account e-mail address associated with the key.
:rtype: str or None
:returns: e-mail address for the service account which created the key.
"""
return self._properties.get("serviceAccountEmail")
@property
def state(self):
"""Get / set key's state.
One of:
- ``ACTIVE``
- ``INACTIVE``
- ``DELETED``
:rtype: str or None
:returns: key's current state.
"""
return self._properties.get("state")
@state.setter
def state(self, value):
if value not in self._SETTABLE_STATES:
raise ValueError(
"State may only be set to one of: {}".format(
", ".join(self._SETTABLE_STATES)
)
)
self._properties["state"] = value
@property
def time_created(self):
"""Retrieve the timestamp at which the HMAC key was created.
:rtype: :class:`datetime.datetime` or ``NoneType``
:returns: Datetime object parsed from RFC3339 valid timestamp, or
``None`` if the bucket's resource has not been loaded
from the server.
"""
value = self._properties.get("timeCreated")
if value is not None:
return _rfc3339_to_datetime(value)
@property
def updated(self):
"""Retrieve the timestamp at which the HMAC key was created.
:rtype: :class:`datetime.datetime` or ``NoneType``
:returns: Datetime object parsed from RFC3339 valid timestamp, or
``None`` if the bucket's resource has not been loaded
from the server.
"""
value = self._properties.get("updated")
if value is not None:
return _rfc3339_to_datetime(value)
@property
def path(self):
"""Resource path for the metadata's key."""
if self.access_id is None:
raise ValueError("No 'access_id' set.")
project = self.project
if project is None:
project = self._client.project
return "/projects/{}/hmacKeys/{}".format(project, self.access_id)
@property
def user_project(self):
"""Project ID to be billed for API requests made via this bucket.
This property is currently ignored by the server.
:rtype: str
"""
return self._user_project
def exists(self, timeout=_DEFAULT_TIMEOUT):
"""Determine whether or not the key for this metadata exists.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:rtype: bool
:returns: True if the key exists in Cloud Storage.
"""
try:
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project
self._client._connection.api_request(
method="GET", path=self.path, query_params=qs_params, timeout=timeout
)
except NotFound:
return False
else:
return True
def reload(self, timeout=_DEFAULT_TIMEOUT):
"""Reload properties from Cloud Storage.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project
self._properties = self._client._connection.api_request(
method="GET", path=self.path, query_params=qs_params, timeout=timeout
)
def update(self, timeout=_DEFAULT_TIMEOUT):
"""Save writable properties to Cloud Storage.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project
payload = {"state": self.state}
self._properties = self._client._connection.api_request(
method="PUT",
path=self.path,
data=payload,
query_params=qs_params,
timeout=timeout,
)
def delete(self, timeout=_DEFAULT_TIMEOUT):
"""Delete the key from Cloud Storage.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:raises :class:`~google.api_core.exceptions.NotFound`:
if the key does not exist on the back-end.
"""
if self.state != self.INACTIVE_STATE:
raise ValueError("Cannot delete key if not in 'INACTIVE' state.")
qs_params = {}
if self.user_project is not None:
qs_params["userProject"] = self.user_project
self._client._connection.api_request(
method="DELETE", path=self.path, query_params=qs_params, timeout=timeout
)

View file

@ -0,0 +1,86 @@
# Copyright 2017 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Storage API IAM policy definitions
For allowed roles / permissions, see:
https://cloud.google.com/storage/docs/access-control/iam
"""
# Storage-specific IAM roles
STORAGE_OBJECT_CREATOR_ROLE = "roles/storage.objectCreator"
"""Role implying rights to create objects, but not delete or overwrite them."""
STORAGE_OBJECT_VIEWER_ROLE = "roles/storage.objectViewer"
"""Role implying rights to view object properties, excluding ACLs."""
STORAGE_OBJECT_ADMIN_ROLE = "roles/storage.objectAdmin"
"""Role implying full control of objects."""
STORAGE_ADMIN_ROLE = "roles/storage.admin"
"""Role implying full control of objects and buckets."""
STORAGE_VIEWER_ROLE = "Viewer"
"""Can list buckets."""
STORAGE_EDITOR_ROLE = "Editor"
"""Can create, list, and delete buckets."""
STORAGE_OWNER_ROLE = "Owners"
"""Can create, list, and delete buckets."""
# Storage-specific permissions
STORAGE_BUCKETS_CREATE = "storage.buckets.create"
"""Permission: create buckets."""
STORAGE_BUCKETS_DELETE = "storage.buckets.delete"
"""Permission: delete buckets."""
STORAGE_BUCKETS_GET = "storage.buckets.get"
"""Permission: read bucket metadata, excluding ACLs."""
STORAGE_BUCKETS_GET_IAM_POLICY = "storage.buckets.getIamPolicy"
"""Permission: read bucket ACLs."""
STORAGE_BUCKETS_LIST = "storage.buckets.list"
"""Permission: list buckets."""
STORAGE_BUCKETS_SET_IAM_POLICY = "storage.buckets.setIamPolicy"
"""Permission: update bucket ACLs."""
STORAGE_BUCKETS_UPDATE = "storage.buckets.list"
"""Permission: update buckets, excluding ACLS."""
STORAGE_OBJECTS_CREATE = "storage.objects.create"
"""Permission: add new objects to a bucket."""
STORAGE_OBJECTS_DELETE = "storage.objects.delete"
"""Permission: delete objects."""
STORAGE_OBJECTS_GET = "storage.objects.get"
"""Permission: read object data / metadata, excluding ACLs."""
STORAGE_OBJECTS_GET_IAM_POLICY = "storage.objects.getIamPolicy"
"""Permission: read object ACLs."""
STORAGE_OBJECTS_LIST = "storage.objects.list"
"""Permission: list objects in a bucket."""
STORAGE_OBJECTS_SET_IAM_POLICY = "storage.objects.setIamPolicy"
"""Permission: update object ACLs."""
STORAGE_OBJECTS_UPDATE = "storage.objects.update"
"""Permission: update object metadat, excluding ACLs."""

View file

@ -0,0 +1,426 @@
# Copyright 2017 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Support for bucket notification resources."""
import re
from google.api_core.exceptions import NotFound
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
OBJECT_FINALIZE_EVENT_TYPE = "OBJECT_FINALIZE"
OBJECT_METADATA_UPDATE_EVENT_TYPE = "OBJECT_METADATA_UPDATE"
OBJECT_DELETE_EVENT_TYPE = "OBJECT_DELETE"
OBJECT_ARCHIVE_EVENT_TYPE = "OBJECT_ARCHIVE"
JSON_API_V1_PAYLOAD_FORMAT = "JSON_API_V1"
NONE_PAYLOAD_FORMAT = "NONE"
_TOPIC_REF_FMT = "//pubsub.googleapis.com/projects/{}/topics/{}"
_PROJECT_PATTERN = r"(?P<project>[a-z][a-z0-9-]{4,28}[a-z0-9])"
_TOPIC_NAME_PATTERN = r"(?P<name>[A-Za-z](\w|[-_.~+%])+)"
_TOPIC_REF_PATTERN = _TOPIC_REF_FMT.format(_PROJECT_PATTERN, _TOPIC_NAME_PATTERN)
_TOPIC_REF_RE = re.compile(_TOPIC_REF_PATTERN)
_BAD_TOPIC = (
"Resource has invalid topic: {}; see "
"https://cloud.google.com/storage/docs/json_api/v1/"
"notifications/insert#topic"
)
class BucketNotification(object):
"""Represent a single notification resource for a bucket.
See: https://cloud.google.com/storage/docs/json_api/v1/notifications
:type bucket: :class:`google.cloud.storage.bucket.Bucket`
:param bucket: Bucket to which the notification is bound.
:type topic_name: str
:param topic_name:
(Optional) Topic name to which notifications are published.
:type topic_project: str
:param topic_project:
(Optional) Project ID of topic to which notifications are published.
If not passed, uses the project ID of the bucket's client.
:type custom_attributes: dict
:param custom_attributes:
(Optional) Additional attributes passed with notification events.
:type event_types: list(str)
:param event_types:
(Optional) Event types for which notification events are published.
:type blob_name_prefix: str
:param blob_name_prefix:
(Optional) Prefix of blob names for which notification events are
published.
:type payload_format: str
:param payload_format:
(Optional) Format of payload for notification events.
:type notification_id: str
:param notification_id:
(Optional) The ID of the notification.
"""
def __init__(
self,
bucket,
topic_name=None,
topic_project=None,
custom_attributes=None,
event_types=None,
blob_name_prefix=None,
payload_format=NONE_PAYLOAD_FORMAT,
notification_id=None,
):
self._bucket = bucket
self._topic_name = topic_name
if topic_project is None:
topic_project = bucket.client.project
if topic_project is None:
raise ValueError("Client project not set: pass an explicit topic_project.")
self._topic_project = topic_project
self._properties = {}
if custom_attributes is not None:
self._properties["custom_attributes"] = custom_attributes
if event_types is not None:
self._properties["event_types"] = event_types
if blob_name_prefix is not None:
self._properties["object_name_prefix"] = blob_name_prefix
if notification_id is not None:
self._properties["id"] = notification_id
self._properties["payload_format"] = payload_format
@classmethod
def from_api_repr(cls, resource, bucket):
"""Construct an instance from the JSON repr returned by the server.
See: https://cloud.google.com/storage/docs/json_api/v1/notifications
:type resource: dict
:param resource: JSON repr of the notification
:type bucket: :class:`google.cloud.storage.bucket.Bucket`
:param bucket: Bucket to which the notification is bound.
:rtype: :class:`BucketNotification`
:returns: the new notification instance
"""
topic_path = resource.get("topic")
if topic_path is None:
raise ValueError("Resource has no topic")
name, project = _parse_topic_path(topic_path)
instance = cls(bucket, name, topic_project=project)
instance._properties = resource
return instance
@property
def bucket(self):
"""Bucket to which the notification is bound."""
return self._bucket
@property
def topic_name(self):
"""Topic name to which notifications are published."""
return self._topic_name
@property
def topic_project(self):
"""Project ID of topic to which notifications are published.
"""
return self._topic_project
@property
def custom_attributes(self):
"""Custom attributes passed with notification events.
"""
return self._properties.get("custom_attributes")
@property
def event_types(self):
"""Event types for which notification events are published.
"""
return self._properties.get("event_types")
@property
def blob_name_prefix(self):
"""Prefix of blob names for which notification events are published.
"""
return self._properties.get("object_name_prefix")
@property
def payload_format(self):
"""Format of payload of notification events."""
return self._properties.get("payload_format")
@property
def notification_id(self):
"""Server-set ID of notification resource."""
return self._properties.get("id")
@property
def etag(self):
"""Server-set ETag of notification resource."""
return self._properties.get("etag")
@property
def self_link(self):
"""Server-set ETag of notification resource."""
return self._properties.get("selfLink")
@property
def client(self):
"""The client bound to this notfication."""
return self.bucket.client
@property
def path(self):
"""The URL path for this notification."""
return "/b/{}/notificationConfigs/{}".format(
self.bucket.name, self.notification_id
)
def _require_client(self, client):
"""Check client or verify over-ride.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: the client to use.
:rtype: :class:`google.cloud.storage.client.Client`
:returns: The client passed in or the bucket's client.
"""
if client is None:
client = self.client
return client
def _set_properties(self, response):
"""Helper for :meth:`reload`.
:type response: dict
:param response: resource mapping from server
"""
self._properties.clear()
self._properties.update(response)
def create(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""API wrapper: create the notification.
See:
https://cloud.google.com/storage/docs/json_api/v1/notifications/insert
If :attr:`user_project` is set on the bucket, bills the API request
to that project.
:type client: :class:`~google.cloud.storage.client.Client`
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the notification's bucket.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
"""
if self.notification_id is not None:
raise ValueError(
"Notification already exists w/ id: {}".format(self.notification_id)
)
client = self._require_client(client)
query_params = {}
if self.bucket.user_project is not None:
query_params["userProject"] = self.bucket.user_project
path = "/b/{}/notificationConfigs".format(self.bucket.name)
properties = self._properties.copy()
properties["topic"] = _TOPIC_REF_FMT.format(self.topic_project, self.topic_name)
self._properties = client._connection.api_request(
method="POST",
path=path,
query_params=query_params,
data=properties,
timeout=timeout,
)
def exists(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""Test whether this notification exists.
See:
https://cloud.google.com/storage/docs/json_api/v1/notifications/get
If :attr:`user_project` is set on the bucket, bills the API request
to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:rtype: bool
:returns: True, if the notification exists, else False.
:raises ValueError: if the notification has no ID.
"""
if self.notification_id is None:
raise ValueError("Notification not intialized by server")
client = self._require_client(client)
query_params = {}
if self.bucket.user_project is not None:
query_params["userProject"] = self.bucket.user_project
try:
client._connection.api_request(
method="GET", path=self.path, query_params=query_params, timeout=timeout
)
except NotFound:
return False
else:
return True
def reload(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""Update this notification from the server configuration.
See:
https://cloud.google.com/storage/docs/json_api/v1/notifications/get
If :attr:`user_project` is set on the bucket, bills the API request
to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:raises ValueError: if the notification has no ID.
"""
if self.notification_id is None:
raise ValueError("Notification not intialized by server")
client = self._require_client(client)
query_params = {}
if self.bucket.user_project is not None:
query_params["userProject"] = self.bucket.user_project
response = client._connection.api_request(
method="GET", path=self.path, query_params=query_params, timeout=timeout
)
self._set_properties(response)
def delete(self, client=None, timeout=_DEFAULT_TIMEOUT):
"""Delete this notification.
See:
https://cloud.google.com/storage/docs/json_api/v1/notifications/delete
If :attr:`user_project` is set on the bucket, bills the API request
to that project.
:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
to the ``client`` stored on the current bucket.
:type timeout: float or tuple
:param timeout: (Optional) The amount of time, in seconds, to wait
for the server response.
Can also be passed as a tuple (connect_timeout, read_timeout).
See :meth:`requests.Session.request` documentation for details.
:raises: :class:`google.api_core.exceptions.NotFound`:
if the notification does not exist.
:raises ValueError: if the notification has no ID.
"""
if self.notification_id is None:
raise ValueError("Notification not intialized by server")
client = self._require_client(client)
query_params = {}
if self.bucket.user_project is not None:
query_params["userProject"] = self.bucket.user_project
client._connection.api_request(
method="DELETE", path=self.path, query_params=query_params, timeout=timeout
)
def _parse_topic_path(topic_path):
"""Verify that a topic path is in the correct format.
.. _resource manager docs: https://cloud.google.com/resource-manager/\
reference/rest/v1beta1/projects#\
Project.FIELDS.project_id
.. _topic spec: https://cloud.google.com/storage/docs/json_api/v1/\
notifications/insert#topic
Expected to be of the form:
//pubsub.googleapis.com/projects/{project}/topics/{topic}
where the ``project`` value must be "6 to 30 lowercase letters, digits,
or hyphens. It must start with a letter. Trailing hyphens are prohibited."
(see `resource manager docs`_) and ``topic`` must have length at least two,
must start with a letter and may only contain alphanumeric characters or
``-``, ``_``, ``.``, ``~``, ``+`` or ``%`` (i.e characters used for URL
encoding, see `topic spec`_).
Args:
topic_path (str): The topic path to be verified.
Returns:
Tuple[str, str]: The ``project`` and ``topic`` parsed from the
``topic_path``.
Raises:
ValueError: If the topic path is invalid.
"""
match = _TOPIC_REF_RE.match(topic_path)
if match is None:
raise ValueError(_BAD_TOPIC.format(topic_path))
return match.group("name"), match.group("project")