|
30 | 30 | from os import environ, linesep
|
31 | 31 | from pathlib import Path
|
32 | 32 | from tempfile import mkdtemp, mktemp
|
| 33 | +from typing import Any, Iterable, TypeVar |
33 | 34 |
|
34 | 35 | import backoff
|
35 | 36 | from b2sdk.v2 import (
|
@@ -131,6 +132,10 @@ def bucket_name_part(length: int) -> str:
|
131 | 132 | logger.info('name_part: %s', name_part)
|
132 | 133 | return name_part
|
133 | 134 |
|
| 135 | +T = TypeVar('T') |
| 136 | +def wrap_iterables(generators: list[Iterable[T]]): |
| 137 | + for g in generators: |
| 138 | + yield from g |
134 | 139 |
|
135 | 140 | @dataclass
|
136 | 141 | class Api:
|
@@ -219,23 +224,41 @@ def clean_buckets(self, quick=False):
|
219 | 224 | TooManyRequests,
|
220 | 225 | max_tries=8,
|
221 | 226 | )
|
222 |
| - def clean_bucket(self, bucket: Bucket | str): |
223 |
| - if isinstance(bucket, str): |
224 |
| - bucket = self.api.get_bucket_by_name(bucket) |
| 227 | + def clean_bucket(self, bucket_object: Bucket | str, only_files: bool = False, only_folders: list[str] | None = None, ignore_retentions: bool = False): |
| 228 | + """ |
| 229 | + Clean contents of bucket, by default also deleting the bucket. |
225 | 230 |
|
226 |
| - # try optimistic bucket removal first, since it is completely free (as opposed to `ls` call) |
227 |
| - try: |
228 |
| - return self.api.delete_bucket(bucket) |
229 |
| - except (BucketIdNotFound, v3BucketIdNotFound): |
230 |
| - return # bucket was already removed |
231 |
| - except BadRequest as exc: |
232 |
| - assert exc.code == 'cannot_delete_non_empty_bucket' |
| 231 | + Args: |
| 232 | + bucket (Bucket | str): Bucket object or name |
| 233 | + only_files (bool): If to only delete files and not the bucket |
| 234 | + only_folders (list[str] | None): If not None, filter to only files in given folders. |
| 235 | + ignore_retentions (bool): If deletion should happen regardless of files' retention mode. |
| 236 | + """ |
| 237 | + bucket: Bucket |
| 238 | + if isinstance(bucket_object, str): |
| 239 | + bucket = self.api.get_bucket_by_name(bucket_object) |
| 240 | + else: |
| 241 | + bucket = bucket_object |
| 242 | + |
| 243 | + if not only_files: |
| 244 | + # try optimistic bucket removal first, since it is completely free (as opposed to `ls` call) |
| 245 | + try: |
| 246 | + return self.api.delete_bucket(bucket) |
| 247 | + except (BucketIdNotFound, v3BucketIdNotFound): |
| 248 | + return # bucket was already removed |
| 249 | + except BadRequest as exc: |
| 250 | + assert exc.code == 'cannot_delete_non_empty_bucket' |
233 | 251 |
|
234 | 252 | files_leftover = False
|
235 |
| - file_versions = bucket.ls(latest_only=False, recursive=True) |
| 253 | + |
| 254 | + file_versions: Iterable[Any] |
| 255 | + if only_folders: |
| 256 | + file_versions = wrap_iterables([bucket.ls(latest_only=False, recursive=True, folder_to_list=folder,) for folder in only_folders]) |
| 257 | + else: |
| 258 | + file_versions = bucket.ls(latest_only=False, recursive=True) |
236 | 259 |
|
237 | 260 | for file_version_info, _ in file_versions:
|
238 |
| - if file_version_info.file_retention: |
| 261 | + if file_version_info.file_retention and not ignore_retentions: |
239 | 262 | if file_version_info.file_retention.mode == RetentionMode.GOVERNANCE:
|
240 | 263 | print('Removing retention from file version:', file_version_info.id_)
|
241 | 264 | self.api.update_file_retention(
|
@@ -272,7 +295,7 @@ def clean_bucket(self, bucket: Bucket | str):
|
272 | 295 |
|
273 | 296 | if files_leftover:
|
274 | 297 | print('Unable to remove bucket because some retained files remain')
|
275 |
| - else: |
| 298 | + elif not only_files: |
276 | 299 | print('Removing bucket:', bucket.name)
|
277 | 300 | try:
|
278 | 301 | self.api.delete_bucket(bucket)
|
|
0 commit comments