Skip to content

Commit cf9efa2

Browse files
Merge pull request #1066 from Backblaze/persistent-bucket-auto-clean
Persistent bucket auto clean
2 parents cb77345 + 8e4473d commit cf9efa2

File tree

4 files changed

+61
-17
lines changed

4 files changed

+61
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deleting used files by integration tests right away.

test/integration/conftest.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import uuid
2020
from os import environ, path
2121
from tempfile import TemporaryDirectory
22+
from typing import Generator
2223

2324
import pytest
2425
from b2sdk.v2 import B2_ACCOUNT_INFO_ENV_VAR, XDG_CONFIG_HOME_ENV_VAR, Bucket
@@ -35,6 +36,7 @@
3536
from .persistent_bucket import (
3637
PersistentBucketAggregate,
3738
get_or_create_persistent_bucket,
39+
prune_used_files,
3840
)
3941

4042
logger = logging.getLogger(__name__)
@@ -421,18 +423,32 @@ def b2_uri_args(apiver_int):
421423
return b2_uri_args_v3
422424

423425

424-
# -- Persistent bucket fixtures --
426+
# -- Persistent bucket code ---
427+
428+
subfolder_list: list[str] = []
429+
430+
@pytest.fixture(scope="session")
431+
def base_persistent_bucket(b2_api):
432+
bucket = get_or_create_persistent_bucket(b2_api)
433+
yield bucket
434+
prune_used_files(b2_api=b2_api,bucket=bucket, folders=subfolder_list)
435+
436+
425437
@pytest.fixture
426438
def unique_subfolder():
427439
subfolder = f'test-{uuid.uuid4().hex[:8]}'
440+
subfolder_list.append(subfolder)
428441
yield subfolder
429442

430443

431444
@pytest.fixture
432-
def persistent_bucket(unique_subfolder, b2_api) -> PersistentBucketAggregate:
445+
def persistent_bucket(unique_subfolder,
446+
base_persistent_bucket) -> Generator[PersistentBucketAggregate]:
433447
"""
434448
Since all consumers of the `bucket_name` fixture expect a new bucket to be created,
435449
we need to mirror this behavior by appending a unique subfolder to the persistent bucket name.
436450
"""
437-
persistent_bucket = get_or_create_persistent_bucket(b2_api)
438-
yield PersistentBucketAggregate(persistent_bucket.name, unique_subfolder)
451+
yield PersistentBucketAggregate(base_persistent_bucket.name,
452+
unique_subfolder)
453+
454+
logger.info("Persistent bucket aggregate finished completion.")

test/integration/helpers.py

+36-13
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from os import environ, linesep
3131
from pathlib import Path
3232
from tempfile import mkdtemp, mktemp
33+
from typing import Any, Iterable, TypeVar
3334

3435
import backoff
3536
from b2sdk.v2 import (
@@ -131,6 +132,10 @@ def bucket_name_part(length: int) -> str:
131132
logger.info('name_part: %s', name_part)
132133
return name_part
133134

135+
T = TypeVar('T')
136+
def wrap_iterables(generators: list[Iterable[T]]):
137+
for g in generators:
138+
yield from g
134139

135140
@dataclass
136141
class Api:
@@ -219,23 +224,41 @@ def clean_buckets(self, quick=False):
219224
TooManyRequests,
220225
max_tries=8,
221226
)
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.
225230
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'
233251

234252
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)
236259

237260
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:
239262
if file_version_info.file_retention.mode == RetentionMode.GOVERNANCE:
240263
print('Removing retention from file version:', file_version_info.id_)
241264
self.api.update_file_retention(
@@ -272,7 +295,7 @@ def clean_bucket(self, bucket: Bucket | str):
272295

273296
if files_leftover:
274297
print('Unable to remove bucket because some retained files remain')
275-
else:
298+
elif not only_files:
276299
print('Removing bucket:', bucket.name)
277300
try:
278301
self.api.delete_bucket(bucket)

test/integration/persistent_bucket.py

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
from dataclasses import dataclass
1313
from functools import cached_property
14+
from typing import List
1415

1516
import backoff
1617
from b2sdk.v2 import Bucket
@@ -62,3 +63,6 @@ def get_or_create_persistent_bucket(b2_api: Api) -> Bucket:
6263
# add the new bucket name to the list of bucket names
6364
b2_api.bucket_name_log.append(bucket_name)
6465
return bucket
66+
67+
def prune_used_files(b2_api: Api, bucket: Bucket, folders: List[str]):
68+
b2_api.clean_bucket(bucket_object=bucket, only_files=True, only_folders=folders,ignore_retentions=True)

0 commit comments

Comments
 (0)