Skip to content

Commit 8949bb8

Browse files
Add Paginator to Recursively Delete All Objects Before Delete Bucket (#631)
1 parent 80c54a7 commit 8949bb8

File tree

4 files changed

+79
-18
lines changed

4 files changed

+79
-18
lines changed

linodecli/plugins/obj/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,8 @@ def get_client():
426426
COMMAND_MAP[parsed.command](
427427
get_client, args, suppress_warnings=parsed.suppress_warnings
428428
)
429-
except ClientError:
429+
except ClientError as e:
430+
print(e)
430431
sys.exit(ExitCodes.REQUEST_FAILED)
431432
elif parsed.command == "regenerate-keys":
432433
regenerate_s3_credentials(

linodecli/plugins/obj/buckets.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from linodecli.exit_codes import ExitCodes
99
from linodecli.plugins import inherit_plugin_args
1010
from linodecli.plugins.obj.config import PLUGIN_BASE
11+
from linodecli.plugins.obj.helpers import _delete_all_objects
1112

1213

1314
def create_bucket(
@@ -61,23 +62,9 @@ def delete_bucket(
6162
bucket_name = parsed.name
6263

6364
if parsed.recursive:
64-
objects = [
65-
{"Key": obj.get("Key")}
66-
for obj in client.list_objects_v2(Bucket=bucket_name).get(
67-
"Contents", []
68-
)
69-
if obj.get("Key")
70-
]
71-
client.delete_objects(
72-
Bucket=bucket_name,
73-
Delete={
74-
"Objects": objects,
75-
"Quiet": False,
76-
},
77-
)
65+
_delete_all_objects(client, bucket_name)
7866

7967
client.delete_bucket(Bucket=bucket_name)
80-
8168
print(f"Bucket {parsed.name} removed")
8269

8370
sys.exit(ExitCodes.SUCCESS)

linodecli/plugins/obj/helpers.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,50 @@ def flip_to_page(iterable: Iterable, page: int = 1):
142142
sys.exit(ExitCodes.REQUEST_FAILED)
143143

144144
return next(iterable)
145+
146+
147+
def _get_objects_for_deletion_from_page(object_type, page, versioned=False):
148+
return [
149+
(
150+
{"Key": obj["Key"], "VersionId": obj["VersionId"]}
151+
if versioned
152+
else {"Key": obj["Key"]}
153+
)
154+
for obj in page.get(object_type, [])
155+
]
156+
157+
158+
def _delete_all_objects(client, bucket_name):
159+
pages = client.get_paginator("list_objects_v2").paginate(
160+
Bucket=bucket_name, PaginationConfig={"PageSize": 1000}
161+
)
162+
for page in pages:
163+
client.delete_objects(
164+
Bucket=bucket_name,
165+
Delete={
166+
"Objects": _get_objects_for_deletion_from_page(
167+
"Contents", page
168+
),
169+
"Quiet": False,
170+
},
171+
)
172+
173+
for page in client.get_paginator("list_object_versions").paginate(
174+
Bucket=bucket_name, PaginationConfig={"PageSize": 1000}
175+
):
176+
client.delete_objects(
177+
Bucket=bucket_name,
178+
Delete={
179+
"Objects": _get_objects_for_deletion_from_page(
180+
"Versions", page, True
181+
)
182+
},
183+
)
184+
client.delete_objects(
185+
Bucket=bucket_name,
186+
Delete={
187+
"Objects": _get_objects_for_deletion_from_page(
188+
"DeleteMarkers", page, True
189+
)
190+
},
191+
)

tests/integration/obj/test_obj_plugin.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
from concurrent.futures import ThreadPoolExecutor, wait
34
from dataclasses import dataclass
45
from typing import Callable, Optional
56

@@ -94,8 +95,8 @@ def _create_bucket(bucket_name: Optional[str] = None):
9495
for bk in created_buckets:
9596
try:
9697
delete_bucket(bk)
97-
except:
98-
logging.exception(f"Failed to cleanup bucket: {bk}")
98+
except Exception as e:
99+
logging.exception(f"Failed to cleanup bucket: {bk}, {e}")
99100

100101

101102
def delete_bucket(bucket_name: str, force: bool = True):
@@ -210,6 +211,31 @@ def test_multi_files_multi_bucket(
210211
assert "Done" in output
211212

212213

214+
@pytest.mark.parametrize("num_files", [1005])
215+
def test_large_number_of_files_single_bucket_parallel(
216+
create_bucket: Callable[[Optional[str]], str],
217+
generate_test_files: GetTestFilesType,
218+
keys: Keys,
219+
monkeypatch: MonkeyPatch,
220+
num_files: int,
221+
):
222+
patch_keys(keys, monkeypatch)
223+
224+
bucket_name = create_bucket()
225+
file_paths = generate_test_files(num_files)
226+
227+
with ThreadPoolExecutor(50) as executor:
228+
futures = [
229+
executor.submit(
230+
exec_test_command,
231+
BASE_CMD + ["put", str(file.resolve()), bucket_name],
232+
)
233+
for file in file_paths
234+
]
235+
236+
wait(futures)
237+
238+
213239
def test_all_rows(
214240
create_bucket: Callable[[Optional[str]], str],
215241
generate_test_files: GetTestFilesType,

0 commit comments

Comments
 (0)