|
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