Skip to content

Commit 4e9b714

Browse files
samples(storage): add samples for soft delete objects (#1486)
1 parent 7e0412a commit 4e9b714

7 files changed

+393
-10
lines changed

samples/snippets/snippets_test.py

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
import requests
2626

2727
import storage_add_bucket_label
28-
import storage_async_upload
2928
import storage_async_download
29+
import storage_async_upload
3030
import storage_batch_request
3131
import storage_bucket_delete_default_kms_key
3232
import storage_change_default_storage_class
@@ -44,6 +44,7 @@
4444
import storage_delete_file
4545
import storage_delete_file_archived_generation
4646
import storage_disable_bucket_lifecycle_management
47+
import storage_disable_soft_delete
4748
import storage_disable_versioning
4849
import storage_download_byte_range
4950
import storage_download_file
@@ -59,26 +60,31 @@
5960
import storage_get_autoclass
6061
import storage_get_bucket_labels
6162
import storage_get_bucket_metadata
62-
import storage_get_soft_deleted_bucket
6363
import storage_get_metadata
6464
import storage_get_service_account
65+
import storage_get_soft_delete_policy
66+
import storage_get_soft_deleted_bucket
6567
import storage_list_buckets
66-
import storage_list_soft_deleted_buckets
67-
import storage_restore_soft_deleted_bucket
6868
import storage_list_file_archived_generations
6969
import storage_list_files
7070
import storage_list_files_with_prefix
71+
import storage_list_soft_deleted_buckets
72+
import storage_list_soft_deleted_object_versions
73+
import storage_list_soft_deleted_objects
7174
import storage_make_public
7275
import storage_move_file
7376
import storage_object_get_kms_key
7477
import storage_remove_bucket_label
7578
import storage_remove_cors_configuration
7679
import storage_rename_file
80+
import storage_restore_object
81+
import storage_restore_soft_deleted_bucket
7782
import storage_set_autoclass
7883
import storage_set_bucket_default_kms_key
7984
import storage_set_client_endpoint
80-
import storage_set_object_retention_policy
8185
import storage_set_metadata
86+
import storage_set_object_retention_policy
87+
import storage_set_soft_delete_policy
8288
import storage_trace_quickstart
8389
import storage_transfer_manager_download_bucket
8490
import storage_transfer_manager_download_chunks_concurrently
@@ -147,6 +153,21 @@ def test_soft_deleted_bucket():
147153
yield bucket
148154

149155

156+
@pytest.fixture(scope="function")
157+
def test_soft_delete_enabled_bucket():
158+
"""Yields a bucket with soft-delete enabled that is deleted after the test completes."""
159+
bucket = None
160+
while bucket is None or bucket.exists():
161+
bucket_name = f"storage-snippets-test-{uuid.uuid4()}"
162+
bucket = storage.Client().bucket(bucket_name)
163+
# Soft-delete retention for 7 days (minimum allowed by API)
164+
bucket.soft_delete_policy.retention_duration_seconds = 7 * 24 * 60 * 60
165+
# Soft-delete requires a region
166+
bucket.create(location="US-CENTRAL1")
167+
yield bucket
168+
bucket.delete(force=True)
169+
170+
150171
@pytest.fixture(scope="function")
151172
def test_public_bucket():
152173
# The new projects don't allow to make a bucket available to public, so
@@ -230,13 +251,17 @@ def test_bucket_metadata(test_bucket, capsys):
230251

231252

232253
def test_get_soft_deleted_bucket(test_soft_deleted_bucket, capsys):
233-
storage_get_soft_deleted_bucket.get_soft_deleted_bucket(test_soft_deleted_bucket.name, test_soft_deleted_bucket.generation)
254+
storage_get_soft_deleted_bucket.get_soft_deleted_bucket(
255+
test_soft_deleted_bucket.name, test_soft_deleted_bucket.generation
256+
)
234257
out, _ = capsys.readouterr()
235258
assert test_soft_deleted_bucket.name in out
236259

237260

238261
def test_restore_soft_deleted_bucket(test_soft_deleted_bucket, capsys):
239-
storage_restore_soft_deleted_bucket.restore_bucket(test_soft_deleted_bucket.name, test_soft_deleted_bucket.generation)
262+
storage_restore_soft_deleted_bucket.restore_bucket(
263+
test_soft_deleted_bucket.name, test_soft_deleted_bucket.generation
264+
)
240265
out, _ = capsys.readouterr()
241266
assert test_soft_deleted_bucket.name in out
242267

@@ -309,7 +334,9 @@ def test_async_download(test_bucket, capsys):
309334
blob = test_bucket.blob(source)
310335
blob.upload_from_string(source)
311336

312-
asyncio.run(storage_async_download.async_download_blobs(test_bucket.name, *source_files))
337+
asyncio.run(
338+
storage_async_download.async_download_blobs(test_bucket.name, *source_files)
339+
)
313340
out, _ = capsys.readouterr()
314341
for x in range(object_count):
315342
assert f"Downloaded storage object async_sample_blob_{x}" in out
@@ -877,7 +904,10 @@ def test_object_retention_policy(test_bucket_create, capsys):
877904
test_bucket_create.name
878905
)
879906
out, _ = capsys.readouterr()
880-
assert f"Created bucket {test_bucket_create.name} with object retention enabled setting" in out
907+
assert (
908+
f"Created bucket {test_bucket_create.name} with object retention enabled setting"
909+
in out
910+
)
881911

882912
blob_name = "test_object_retention"
883913
storage_set_object_retention_policy.set_object_retention_policy(
@@ -898,7 +928,10 @@ def test_create_bucket_hierarchical_namespace(test_bucket_create, capsys):
898928
test_bucket_create.name
899929
)
900930
out, _ = capsys.readouterr()
901-
assert f"Created bucket {test_bucket_create.name} with hierarchical namespace enabled" in out
931+
assert (
932+
f"Created bucket {test_bucket_create.name} with hierarchical namespace enabled"
933+
in out
934+
)
902935

903936

904937
def test_storage_trace_quickstart(test_bucket, capsys):
@@ -911,3 +944,96 @@ def test_storage_trace_quickstart(test_bucket, capsys):
911944
assert (
912945
f"Downloaded storage object {blob_name} from bucket {test_bucket.name}" in out
913946
)
947+
948+
949+
def test_storage_disable_soft_delete(test_soft_delete_enabled_bucket, capsys):
950+
bucket_name = test_soft_delete_enabled_bucket.name
951+
storage_disable_soft_delete.disable_soft_delete(bucket_name)
952+
out, _ = capsys.readouterr()
953+
assert f"Soft-delete policy is disabled for bucket {bucket_name}" in out
954+
955+
956+
def test_storage_get_soft_delete_policy(test_soft_delete_enabled_bucket, capsys):
957+
bucket_name = test_soft_delete_enabled_bucket.name
958+
storage_get_soft_delete_policy.get_soft_delete_policy(bucket_name)
959+
out, _ = capsys.readouterr()
960+
assert f"Soft-delete policy for {bucket_name}" in out
961+
assert "Object soft-delete policy is enabled" in out
962+
assert "Object retention duration: " in out
963+
assert "Policy effective time: " in out
964+
965+
# Disable the soft-delete policy
966+
test_soft_delete_enabled_bucket.soft_delete_policy.retention_duration_seconds = 0
967+
test_soft_delete_enabled_bucket.patch()
968+
storage_get_soft_delete_policy.get_soft_delete_policy(bucket_name)
969+
out, _ = capsys.readouterr()
970+
assert f"Soft-delete policy for {bucket_name}" in out
971+
assert "Object soft-delete policy is disabled" in out
972+
973+
974+
def test_storage_set_soft_delete_policy(test_soft_delete_enabled_bucket, capsys):
975+
bucket_name = test_soft_delete_enabled_bucket.name
976+
retention_duration_seconds = 10 * 24 * 60 * 60 # 10 days
977+
storage_set_soft_delete_policy.set_soft_delete_policy(
978+
bucket_name, retention_duration_seconds
979+
)
980+
out, _ = capsys.readouterr()
981+
assert (
982+
f"Soft delete policy for bucket {bucket_name} was set to {retention_duration_seconds} seconds retention period"
983+
in out
984+
)
985+
986+
987+
def test_storage_list_soft_deleted_objects(test_soft_delete_enabled_bucket, capsys):
988+
bucket_name = test_soft_delete_enabled_bucket.name
989+
blob_name = f"test_object_{uuid.uuid4().hex}.txt"
990+
blob_content = "This object will be soft-deleted for listing."
991+
blob = test_soft_delete_enabled_bucket.blob(blob_name)
992+
blob.upload_from_string(blob_content)
993+
blob_generation = blob.generation
994+
995+
blob.delete() # Soft-delete the object
996+
storage_list_soft_deleted_objects.list_soft_deleted_objects(bucket_name)
997+
out, _ = capsys.readouterr()
998+
assert f"Name: {blob_name}, Generation: {blob_generation}" in out
999+
1000+
1001+
def test_storage_list_soft_deleted_object_versions(
1002+
test_soft_delete_enabled_bucket, capsys
1003+
):
1004+
bucket_name = test_soft_delete_enabled_bucket.name
1005+
blob_name = f"test_object_{uuid.uuid4().hex}.txt"
1006+
blob_content = "This object will be soft-deleted for version listing."
1007+
blob = test_soft_delete_enabled_bucket.blob(blob_name)
1008+
blob.upload_from_string(blob_content)
1009+
blob_generation = blob.generation
1010+
1011+
blob.delete() # Soft-delete the object
1012+
storage_list_soft_deleted_object_versions.list_soft_deleted_object_versions(
1013+
bucket_name, blob_name
1014+
)
1015+
out, _ = capsys.readouterr()
1016+
assert f"Version ID: {blob_generation}" in out
1017+
1018+
1019+
def test_storage_restore_soft_deleted_object(test_soft_delete_enabled_bucket, capsys):
1020+
bucket_name = test_soft_delete_enabled_bucket.name
1021+
blob_name = f"test-restore-sd-obj-{uuid.uuid4().hex}.txt"
1022+
blob_content = "This object will be soft-deleted and restored."
1023+
blob = test_soft_delete_enabled_bucket.blob(blob_name)
1024+
blob.upload_from_string(blob_content)
1025+
blob_generation = blob.generation
1026+
1027+
blob.delete() # Soft-delete the object
1028+
storage_restore_object.restore_soft_deleted_object(
1029+
bucket_name, blob_name, blob_generation
1030+
)
1031+
out, _ = capsys.readouterr()
1032+
assert (
1033+
f"Soft-deleted object {blob_name} is restored in the bucket {bucket_name}"
1034+
in out
1035+
)
1036+
1037+
# Verify the restoration
1038+
blob = test_soft_delete_enabled_bucket.get_blob(blob_name)
1039+
assert blob is not None
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import sys
18+
19+
# [START storage_disable_soft_delete]
20+
from google.cloud import storage
21+
22+
23+
def disable_soft_delete(bucket_name):
24+
"""Disable soft-delete policy for the bucket."""
25+
# bucket_name = "your-bucket-name"
26+
27+
storage_client = storage.Client()
28+
bucket = storage_client.get_bucket(bucket_name)
29+
30+
# Setting the retention duration to 0 disables soft-delete.
31+
bucket.soft_delete_policy.retention_duration_seconds = 0
32+
bucket.patch()
33+
34+
print(f"Soft-delete policy is disabled for bucket {bucket_name}")
35+
36+
37+
# [END storage_disable_soft_delete]
38+
39+
if __name__ == "__main__":
40+
disable_soft_delete(bucket_name=sys.argv[1])
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import sys
18+
19+
# [START storage_get_soft_delete_policy]
20+
from google.cloud import storage
21+
22+
23+
def get_soft_delete_policy(bucket_name):
24+
"""Gets the soft-delete policy of the bucket"""
25+
# bucket_name = "your-bucket-name"
26+
27+
storage_client = storage.Client()
28+
bucket = storage_client.get_bucket(bucket_name)
29+
30+
print(f"Soft-delete policy for {bucket_name}")
31+
if (
32+
bucket.soft_delete_policy
33+
and bucket.soft_delete_policy.retention_duration_seconds
34+
):
35+
print("Object soft-delete policy is enabled")
36+
print(
37+
f"Object retention duration: {bucket.soft_delete_policy.retention_duration_seconds} seconds"
38+
)
39+
print(f"Policy effective time: {bucket.soft_delete_policy.effective_time}")
40+
else:
41+
print("Object soft-delete policy is disabled")
42+
43+
44+
# [END storage_get_soft_delete_policy]
45+
46+
if __name__ == "__main__":
47+
get_soft_delete_policy(bucket_name=sys.argv[1])
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import sys
18+
19+
# [START storage_list_soft_deleted_object_versions]
20+
from google.cloud import storage
21+
22+
23+
def list_soft_deleted_object_versions(bucket_name, blob_name):
24+
"""Lists all versions of a soft-deleted object in the bucket."""
25+
# bucket_name = "your-bucket-name"
26+
# blob_name = "your-object-name"
27+
28+
storage_client = storage.Client()
29+
blobs = storage_client.list_blobs(bucket_name, prefix=blob_name, soft_deleted=True)
30+
31+
# Note: The call returns a response only when the iterator is consumed.
32+
for blob in blobs:
33+
print(
34+
f"Version ID: {blob.generation}, Soft Delete Time: {blob.soft_delete_time}"
35+
)
36+
37+
38+
# [END storage_list_soft_deleted_object_versions]
39+
40+
if __name__ == "__main__":
41+
list_soft_deleted_object_versions(bucket_name=sys.argv[1], blob_name=sys.argv[2])
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the 'License');
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import sys
18+
19+
# [START storage_list_soft_deleted_objects]
20+
from google.cloud import storage
21+
22+
23+
def list_soft_deleted_objects(bucket_name):
24+
"""Lists all soft-deleted objects in the bucket."""
25+
# bucket_name = "your-bucket-name"
26+
27+
storage_client = storage.Client()
28+
blobs = storage_client.list_blobs(bucket_name, soft_deleted=True)
29+
30+
# Note: The call returns a response only when the iterator is consumed.
31+
for blob in blobs:
32+
print(
33+
f"Name: {blob.name}, Generation: {blob.generation}, Soft Delete Time: {blob.soft_delete_time}"
34+
)
35+
36+
37+
# [END storage_list_soft_deleted_objects]
38+
39+
if __name__ == "__main__":
40+
list_soft_deleted_objects(bucket_name=sys.argv[1])

0 commit comments

Comments
 (0)