diff --git a/google/cloud/storage/acl.py b/google/cloud/storage/acl.py index a17e4f09e..b3b77766f 100644 --- a/google/cloud/storage/acl.py +++ b/google/cloud/storage/acl.py @@ -84,8 +84,10 @@ when sending metadata for ACLs to the API. """ +from google.cloud.storage._helpers import _add_generation_match_parameters from google.cloud.storage.constants import _DEFAULT_TIMEOUT from google.cloud.storage.retry import DEFAULT_RETRY +from google.cloud.storage.retry import DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED class _ACLEntity(object): @@ -465,7 +467,18 @@ def reload(self, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY): for entry in found.get("items", ()): self.add_entity(self.entity_from_dict(entry)) - def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT): + def _save( + self, + acl, + predefined, + client, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + timeout=_DEFAULT_TIMEOUT, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Helper for :meth:`save` and :meth:`save_predefined`. :type acl: :class:`google.cloud.storage.acl.ACL`, or a compatible list. @@ -481,12 +494,28 @@ def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT): :param client: (Optional) The client to use. If not passed, falls back to the ``client`` stored on the ACL's parent. + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + :type timeout: float or tuple :param timeout: (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` - :type retry: :class:`~google.api_core.retry.Retry` + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy :param retry: (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ @@ -500,6 +529,14 @@ def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT): if self.user_project is not None: query_params["userProject"] = self.user_project + _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, + ) + path = self.save_path result = client._patch_resource( @@ -507,7 +544,7 @@ def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT): {self._URL_PATH_ELEM: list(acl)}, query_params=query_params, timeout=timeout, - retry=None, + retry=retry, ) self.entities.clear() @@ -517,7 +554,17 @@ def _save(self, acl, predefined, client, timeout=_DEFAULT_TIMEOUT): self.loaded = True - def save(self, acl=None, client=None, timeout=_DEFAULT_TIMEOUT): + def save( + self, + acl=None, + client=None, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + timeout=_DEFAULT_TIMEOUT, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Save this ACL for the current bucket. If :attr:`user_project` is set, bills the API request to that project. @@ -531,10 +578,30 @@ def save(self, acl=None, client=None, timeout=_DEFAULT_TIMEOUT): :param client: (Optional) The client to use. If not passed, falls back to the ``client`` stored on the ACL's parent. + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + :type timeout: float or tuple :param timeout: (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ if acl is None: acl = self @@ -543,9 +610,29 @@ def save(self, acl=None, client=None, timeout=_DEFAULT_TIMEOUT): 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): + self._save( + acl, + None, + client, + 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, + timeout=timeout, + retry=retry, + ) + + def save_predefined( + self, + predefined, + client=None, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + timeout=_DEFAULT_TIMEOUT, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Save this ACL for the current bucket using a predefined ACL. If :attr:`user_project` is set, bills the API request to that project. @@ -562,15 +649,54 @@ def save_predefined(self, predefined, client=None, timeout=_DEFAULT_TIMEOUT): :param client: (Optional) The client to use. If not passed, falls back to the ``client`` stored on the ACL's parent. + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + :type timeout: float or tuple :param timeout: (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ predefined = self.validate_predefined(predefined) - self._save(None, predefined, client, timeout=timeout) + self._save( + None, + predefined, + client, + 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, + timeout=timeout, + retry=retry, + ) - def clear(self, client=None, timeout=_DEFAULT_TIMEOUT): + def clear( + self, + client=None, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + timeout=_DEFAULT_TIMEOUT, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Remove all ACL entries. If :attr:`user_project` is set, bills the API request to that project. @@ -585,12 +711,41 @@ def clear(self, client=None, timeout=_DEFAULT_TIMEOUT): :param client: (Optional) The client to use. If not passed, falls back to the ``client`` stored on the ACL's parent. + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + :type timeout: float or tuple :param timeout: (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ - self.save([], client=client, timeout=timeout) + self.save( + [], + client=client, + 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, + timeout=timeout, + retry=retry, + ) class BucketACL(ACL): diff --git a/google/cloud/storage/blob.py b/google/cloud/storage/blob.py index 781d6e0a0..737afbe31 100644 --- a/google/cloud/storage/blob.py +++ b/google/cloud/storage/blob.py @@ -83,6 +83,7 @@ from google.cloud.storage.retry import DEFAULT_RETRY from google.cloud.storage.retry import DEFAULT_RETRY_IF_ETAG_IN_JSON from google.cloud.storage.retry import DEFAULT_RETRY_IF_GENERATION_SPECIFIED +from google.cloud.storage.retry import DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED from google.cloud.storage.fileio import BlobReader from google.cloud.storage.fileio import BlobWriter @@ -3226,7 +3227,16 @@ def test_iam_permissions( return resp.get("permissions", []) - def make_public(self, client=None, timeout=_DEFAULT_TIMEOUT): + def make_public( + self, + client=None, + timeout=_DEFAULT_TIMEOUT, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Update blob's ACL, granting read access to anonymous users. :type client: :class:`~google.cloud.storage.client.Client` or @@ -3239,11 +3249,47 @@ def make_public(self, client=None, timeout=_DEFAULT_TIMEOUT): (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ self.acl.all().grant_read() - self.acl.save(client=client, timeout=timeout) + self.acl.save( + client=client, + timeout=timeout, + 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, + retry=retry, + ) - def make_private(self, client=None, timeout=_DEFAULT_TIMEOUT): + def make_private( + self, + client=None, + timeout=_DEFAULT_TIMEOUT, + if_generation_match=None, + if_generation_not_match=None, + if_metageneration_match=None, + if_metageneration_not_match=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ): """Update blob's ACL, revoking read access for anonymous users. :type client: :class:`~google.cloud.storage.client.Client` or @@ -3255,9 +3301,37 @@ def make_private(self, client=None, timeout=_DEFAULT_TIMEOUT): :param timeout: (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + + :type if_generation_match: long + :param if_generation_match: + (Optional) See :ref:`using-if-generation-match` + + :type if_generation_not_match: long + :param if_generation_not_match: + (Optional) See :ref:`using-if-generation-not-match` + + :type if_metageneration_match: long + :param if_metageneration_match: + (Optional) See :ref:`using-if-metageneration-match` + + :type if_metageneration_not_match: long + :param if_metageneration_not_match: + (Optional) See :ref:`using-if-metageneration-not-match` + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` """ self.acl.all().revoke_read() - self.acl.save(client=client, timeout=timeout) + self.acl.save( + client=client, + timeout=timeout, + 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, + retry=retry, + ) def compose( self, diff --git a/google/cloud/storage/bucket.py b/google/cloud/storage/bucket.py index d2af37ac7..8da6e09a8 100644 --- a/google/cloud/storage/bucket.py +++ b/google/cloud/storage/bucket.py @@ -2839,7 +2839,14 @@ def test_iam_permissions( return resp.get("permissions", []) def make_public( - self, recursive=False, future=False, client=None, timeout=_DEFAULT_TIMEOUT, + self, + recursive=False, + future=False, + client=None, + timeout=_DEFAULT_TIMEOUT, + if_metageneration_match=None, + if_metageneration_not_match=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ): """Update bucket's ACL, granting read access to anonymous users. @@ -2860,6 +2867,18 @@ def make_public( (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + :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. + + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` + :raises ValueError: If ``recursive`` is True, and the bucket contains more than 256 blobs. This is to prevent extremely long runtime of this @@ -2869,14 +2888,26 @@ def make_public( for each blob. """ self.acl.all().grant_read() - self.acl.save(client=client, timeout=timeout) + self.acl.save( + client=client, + timeout=timeout, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + retry=retry, + ) if future: doa = self.default_object_acl if not doa.loaded: doa.reload(client=client, timeout=timeout) doa.all().grant_read() - doa.save(client=client, timeout=timeout) + doa.save( + client=client, + timeout=timeout, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + retry=retry, + ) if recursive: blobs = list( @@ -2899,10 +2930,19 @@ def make_public( for blob in blobs: blob.acl.all().grant_read() - blob.acl.save(client=client, timeout=timeout) + blob.acl.save( + client=client, timeout=timeout, + ) def make_private( - self, recursive=False, future=False, client=None, timeout=_DEFAULT_TIMEOUT, + self, + recursive=False, + future=False, + client=None, + timeout=_DEFAULT_TIMEOUT, + if_metageneration_match=None, + if_metageneration_not_match=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ): """Update bucket's ACL, revoking read access for anonymous users. @@ -2924,6 +2964,16 @@ def make_private( (Optional) The amount of time, in seconds, to wait for the server response. See: :ref:`configuring_timeouts` + :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. + :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy + :param retry: + (Optional) How to retry the RPC. See: :ref:`configuring_retries` + :raises ValueError: If ``recursive`` is True, and the bucket contains more than 256 blobs. This is to prevent extremely long runtime of this @@ -2933,14 +2983,26 @@ def make_private( for each blob. """ self.acl.all().revoke_read() - self.acl.save(client=client, timeout=timeout) + self.acl.save( + client=client, + timeout=timeout, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + retry=retry, + ) if future: doa = self.default_object_acl if not doa.loaded: doa.reload(client=client, timeout=timeout) doa.all().revoke_read() - doa.save(client=client, timeout=timeout) + doa.save( + client=client, + timeout=timeout, + if_metageneration_match=if_metageneration_match, + if_metageneration_not_match=if_metageneration_not_match, + retry=retry, + ) if recursive: blobs = list( diff --git a/tests/system/test_blob.py b/tests/system/test_blob.py index 5d654f648..c530b58ee 100644 --- a/tests/system/test_blob.py +++ b/tests/system/test_blob.py @@ -373,6 +373,41 @@ def test_blob_acl_w_user_project( assert not acl.has_entity("allUsers") +def test_blob_acl_w_metageneration_match( + shared_bucket, blobs_to_delete, file_data, service_account, +): + wrong_metageneration_number = 9 + wrong_generation_number = 6 + + blob = shared_bucket.blob("FilePatchACL") + info = file_data["simple"] + blob.upload_from_filename(info["path"]) + blobs_to_delete.append(blob) + + # Exercise blob ACL with metageneration/generation match + acl = blob.acl + blob.reload() + + with pytest.raises(exceptions.PreconditionFailed): + acl.save_predefined( + "publicRead", if_metageneration_match=wrong_metageneration_number + ) + assert "READER" not in acl.all().get_roles() + + acl.save_predefined("publicRead", if_metageneration_match=blob.metageneration) + assert "READER" in acl.all().get_roles() + + blob.reload() + del acl.entities["allUsers"] + + with pytest.raises(exceptions.PreconditionFailed): + acl.save(if_generation_match=wrong_generation_number) + assert acl.has_entity("allUsers") + + acl.save(if_generation_match=blob.generation) + assert not acl.has_entity("allUsers") + + def test_blob_acl_upload_predefined( shared_bucket, blobs_to_delete, file_data, service_account, ): diff --git a/tests/system/test_bucket.py b/tests/system/test_bucket.py index 55ea09057..9a5c76931 100644 --- a/tests/system/test_bucket.py +++ b/tests/system/test_bucket.py @@ -246,6 +246,42 @@ def test_bucket_acls_iam_w_user_project( with_user_project.set_iam_policy(policy) +def test_bucket_acls_w_metageneration_match(storage_client, buckets_to_delete): + wrong_metageneration_number = 9 + bucket_name = _helpers.unique_name("acl-w-metageneration-match") + bucket = _helpers.retry_429_503(storage_client.create_bucket)(bucket_name) + buckets_to_delete.append(bucket) + + # Exercise bucket ACL with metageneration match + acl = bucket.acl + acl.group("cloud-developer-relations@google.com").grant_read() + bucket.reload() + + with pytest.raises(exceptions.PreconditionFailed): + acl.save(if_metageneration_match=wrong_metageneration_number) + assert ( + "READER" + not in acl.group("cloud-developer-relations@google.com").get_roles() + ) + + acl.save(if_metageneration_match=bucket.metageneration) + assert "READER" in acl.group("cloud-developer-relations@google.com").get_roles() + + # Exercise default object ACL w/ metageneration match + doa = bucket.default_object_acl + doa.group("cloud-developer-relations@google.com").grant_owner() + bucket.reload() + + with pytest.raises(exceptions.PreconditionFailed): + doa.save(if_metageneration_match=wrong_metageneration_number) + assert ( + "OWNER" not in doa.group("cloud-developer-relations@google.com").get_roles() + ) + + doa.save(if_metageneration_match=bucket.metageneration) + assert "OWNER" in doa.group("cloud-developer-relations@google.com").get_roles() + + def test_bucket_copy_blob( storage_client, buckets_to_delete, blobs_to_delete, user_project, ): diff --git a/tests/unit/test_acl.py b/tests/unit/test_acl.py index aad44809e..6083ef1e1 100644 --- a/tests/unit/test_acl.py +++ b/tests/unit/test_acl.py @@ -16,7 +16,10 @@ import mock -from google.cloud.storage.retry import DEFAULT_RETRY +from google.cloud.storage.retry import ( + DEFAULT_RETRY, + DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, +) class Test_ACLEntity(unittest.TestCase): @@ -646,7 +649,7 @@ class Derived(self._get_target_class()): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_save_no_acl_w_timeout(self): @@ -673,7 +676,7 @@ def test_save_no_acl_w_timeout(self): expected_data, query_params=expected_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_save_w_acl_w_user_project(self): @@ -705,7 +708,46 @@ def test_save_w_acl_w_user_project(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_save_w_acl_w_preconditions(self): + save_path = "/testing" + role1 = "role1" + role2 = "role2" + sticky = {"entity": "allUsers", "role": role2} + new_acl = [{"entity": "allUsers", "role": role1}] + api_response = {"acl": [sticky] + new_acl} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + acl = self._make_one() + acl.save_path = save_path + acl.loaded = True + + acl.save( + new_acl, + client=client, + if_metageneration_match=2, + if_metageneration_not_match=1, + ) + + entries = list(acl) + self.assertEqual(len(entries), 2) + self.assertTrue(sticky in entries) + self.assertTrue(new_acl[0] in entries) + + expected_data = {"acl": new_acl} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + save_path, + expected_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_save_prefefined_invalid(self): @@ -749,7 +791,7 @@ class Derived(self._get_target_class()): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_save_predefined_w_XML_alias_w_timeout(self): @@ -779,7 +821,7 @@ def test_save_predefined_w_XML_alias_w_timeout(self): expected_data, query_params=expected_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_save_predefined_w_alternate_query_param(self): @@ -809,7 +851,42 @@ def test_save_predefined_w_alternate_query_param(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_save_predefined_w_preconditions(self): + save_path = "/testing" + predefined = "private" + api_response = {"acl": []} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + acl = self._make_one() + acl.save_path = save_path + acl.loaded = True + + acl.save_predefined( + predefined, + client=client, + if_metageneration_match=2, + if_metageneration_not_match=1, + ) + + entries = list(acl) + self.assertEqual(len(entries), 0) + + expected_data = {"acl": []} + expected_query_params = { + "projection": "full", + "predefinedAcl": predefined, + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + save_path, + expected_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_clear_w_defaults(self): @@ -842,7 +919,7 @@ class Derived(self._get_target_class()): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_clear_w_explicit_client_w_timeout(self): @@ -872,7 +949,40 @@ def test_clear_w_explicit_client_w_timeout(self): expected_data, query_params=expected_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_clear_w_explicit_client_w_preconditions(self): + save_path = "/testing" + role1 = "role1" + role2 = "role2" + sticky = {"entity": "allUsers", "role": role2} + api_response = {"acl": [sticky]} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + acl = self._make_one() + acl.save_path = save_path + acl.loaded = True + acl.entity("allUsers", role1) + + acl.clear( + client=client, if_metageneration_match=2, if_metageneration_not_match=1 + ) + + self.assertEqual(list(acl), [sticky]) + + expected_data = {"acl": []} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + save_path, + expected_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) diff --git a/tests/unit/test_blob.py b/tests/unit/test_blob.py index f74e6e111..db5212466 100644 --- a/tests/unit/test_blob.py +++ b/tests/unit/test_blob.py @@ -27,7 +27,10 @@ from six.moves import http_client from six.moves.urllib.parse import urlencode -from google.cloud.storage.retry import DEFAULT_RETRY +from google.cloud.storage.retry import ( + DEFAULT_RETRY, + DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, +) from google.cloud.storage.retry import DEFAULT_RETRY_IF_ETAG_IN_JSON from google.cloud.storage.retry import DEFAULT_RETRY_IF_GENERATION_SPECIFIED @@ -3936,7 +3939,7 @@ def test_make_public_w_defaults(self): expected_patch_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_make_public_w_timeout(self): @@ -3963,7 +3966,37 @@ def test_make_public_w_timeout(self): expected_patch_data, query_params=expected_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_make_public_w_preconditions(self): + from google.cloud.storage.acl import _ACLEntity + + blob_name = "blob-name" + permissive = [{"entity": "allUsers", "role": _ACLEntity.READER_ROLE}] + api_response = {"acl": permissive} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + bucket = _Bucket(client=client) + blob = self._make_one(blob_name, bucket=bucket) + blob.acl.loaded = True + + blob.make_public(if_metageneration_match=2, if_metageneration_not_match=1) + + self.assertEqual(list(blob.acl), permissive) + + expected_patch_data = {"acl": permissive} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + blob.path, + expected_patch_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_make_private_w_defaults(self): @@ -3987,7 +4020,7 @@ def test_make_private_w_defaults(self): expected_patch_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_make_private_w_timeout(self): @@ -4012,7 +4045,35 @@ def test_make_private_w_timeout(self): expected_patch_data, query_params=expected_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_make_private_w_preconditions(self): + blob_name = "blob-name" + no_permissions = [] + api_response = {"acl": no_permissions} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + bucket = _Bucket(client=client) + blob = self._make_one(blob_name, bucket=bucket) + blob.acl.loaded = True + + blob.make_private(if_metageneration_match=2, if_metageneration_not_match=1) + + self.assertEqual(list(blob.acl), no_permissions) + + expected_patch_data = {"acl": no_permissions} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + blob.path, + expected_patch_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_compose_wo_content_type_set(self): diff --git a/tests/unit/test_bucket.py b/tests/unit/test_bucket.py index cc2466ac8..a63b7fca3 100644 --- a/tests/unit/test_bucket.py +++ b/tests/unit/test_bucket.py @@ -1995,7 +1995,7 @@ def test_copy_blob_w_preserve_acl_false_w_explicit_client(self): expected_patch_data, query_params=expected_patch_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def test_copy_blob_w_name_and_user_project(self): @@ -3199,7 +3199,39 @@ def test_make_public_defaults(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_make_public_w_preconditions(self): + from google.cloud.storage.acl import _ACLEntity + + name = "name" + permissive = [{"entity": "allUsers", "role": _ACLEntity.READER_ROLE}] + api_response = {"acl": permissive, "defaultObjectAcl": []} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + bucket = self._make_one(client=client, name=name) + bucket.acl.loaded = True + bucket.default_object_acl.loaded = True + + bucket.make_public(if_metageneration_match=2, if_metageneration_not_match=1) + + self.assertEqual(list(bucket.acl), permissive) + self.assertEqual(list(bucket.default_object_acl), []) + + expected_path = bucket.path + expected_data = {"acl": permissive} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + expected_path, + expected_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def _make_public_w_future_helper(self, default_object_acl_loaded=True): @@ -3232,7 +3264,7 @@ def _make_public_w_future_helper(self, default_object_acl_loaded=True): expected_kw = { "query_params": {"projection": "full"}, "timeout": self._get_default_timeout(), - "retry": None, + "retry": DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, } client._patch_resource.assert_has_calls( [ @@ -3317,7 +3349,7 @@ def save(self, client=None, timeout=None): expected_patch_data, query_params=expected_patch_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) client.list_blobs.assert_called_once() @@ -3352,7 +3384,7 @@ def test_make_public_recursive_too_many(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) client.list_blobs.assert_called_once() @@ -3380,7 +3412,37 @@ def test_make_private_defaults(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, + ) + + def test_make_private_w_preconditions(self): + name = "name" + no_permissions = [] + api_response = {"acl": no_permissions, "defaultObjectAcl": []} + client = mock.Mock(spec=["_patch_resource"]) + client._patch_resource.return_value = api_response + bucket = self._make_one(client=client, name=name) + bucket.acl.loaded = True + bucket.default_object_acl.loaded = True + + bucket.make_private(if_metageneration_match=2, if_metageneration_not_match=1) + + self.assertEqual(list(bucket.acl), no_permissions) + self.assertEqual(list(bucket.default_object_acl), []) + + expected_path = bucket.path + expected_data = {"acl": no_permissions} + expected_query_params = { + "projection": "full", + "ifMetagenerationMatch": 2, + "ifMetagenerationNotMatch": 1, + } + client._patch_resource.assert_called_once_with( + expected_path, + expected_data, + query_params=expected_query_params, + timeout=self._get_default_timeout(), + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) def _make_private_w_future_helper(self, default_object_acl_loaded=True): @@ -3414,7 +3476,7 @@ def _make_private_w_future_helper(self, default_object_acl_loaded=True): expected_kw = { "query_params": {"projection": "full"}, "timeout": self._get_default_timeout(), - "retry": None, + "retry": DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, } client._patch_resource.assert_has_calls( [ @@ -3497,7 +3559,7 @@ def save(self, client=None, timeout=None): expected_patch_data, query_params=expected_patch_query_params, timeout=timeout, - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) client.list_blobs.assert_called_once() @@ -3531,7 +3593,7 @@ def test_make_private_recursive_too_many(self): expected_data, query_params=expected_query_params, timeout=self._get_default_timeout(), - retry=None, + retry=DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED, ) client.list_blobs.assert_called_once()