Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8dd35cb
dataclasses for file lock and retention
mpnowacki-reef May 11, 2021
12310ea
file lock related objects serilizers and deserializers
mpnowacki-reef May 11, 2021
f2c40f8
serializers finished
mpnowacki-reef May 11, 2021
9aaf60a
lsing files works
mpnowacki-reef May 11, 2021
0a157f5
Add file retention parameters to raw_api signatures
ppolewicz May 11, 2021
70191e5
Fix double 'if' in raw_api encryption handling
ppolewicz May 11, 2021
f439e80
Refactor permission checking in raw_simulator
ppolewicz May 11, 2021
0395bbd
Fix for empty period in NONE mode retention settings
ppolewicz May 12, 2021
c33b978
Fix bucket creation, ignore the docs for now
ppolewicz May 12, 2021
fb5b68c
Add all new capabilities (key permissions)
ppolewicz May 12, 2021
61ff868
Add raw_api.update_file_retention
ppolewicz May 12, 2021
9205666
Add basic retention scenario to test_raw_api
ppolewicz May 12, 2021
7d68336
is_file_lock_enabled and default_retention are not stored in buckets …
mpnowacki-reef May 12, 2021
f6b6691
missing colon causing a SyntaxError fixed
mpnowacki-reef May 12, 2021
18bd0f7
missing file lock metadata (legalHold and fileRetention) for hide mar…
mpnowacki-reef May 12, 2021
ac05c27
Add type hint for raw_api.update_file_retention
ppolewicz May 13, 2021
af265dd
Fix retention deserialization so that it works when unauthorized too
ppolewicz May 13, 2021
03c40ef
Add snippets for raw_api integration test debugging
ppolewicz May 13, 2021
77169c2
Change FileSimulator bucket_id field to BucketSimulator type
ppolewicz May 13, 2021
a7c2247
Remove debug logs related to sse from raw_simulator
ppolewicz May 13, 2021
a9574fa
Code formatting
ppolewicz May 13, 2021
52a5bc6
Change capability checkers in raw_simulator to take auth token into a…
ppolewicz May 13, 2021
e2e2675
Add account_auth_token to context of responses for download and
ppolewicz May 13, 2021
6645c64
Add account_auth_token to context of responses for upload and ls oper…
ppolewicz May 13, 2021
f2f8dc1
Add update_file_retention to raw_simulator
ppolewicz May 13, 2021
22ee991
Add file retention to FileSimulator
ppolewicz May 13, 2021
08eefde
Add legal hold to FileSimulator
ppolewicz May 13, 2021
1dea0ed
Add file lock enabling to BucketSimulator
ppolewicz May 13, 2021
f4ee785
Add default retention setting to BucketSimulator
ppolewicz May 13, 2021
bae7c3f
Only output refention headers in file lock enabled buckets
ppolewicz May 13, 2021
ea0e763
Bucket layer for File-Lock
mzukowski-reef May 13, 2021
749dd0b
Update spacing
ppolewicz May 13, 2021
982a390
Consistently place legal_hold after file_retention in all methods
ppolewicz May 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions b2sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .bucket import Bucket, BucketFactory
from .encryption.setting import EncryptionSetting
from .exception import NonExistentBucket, RestrictedBucket
from .file_lock import BucketRetentionSetting
from .file_version import FileIdAndName
from .large_file.services import LargeFileServices
from .raw_api import API_VERSION
Expand Down Expand Up @@ -177,6 +178,8 @@ def create_bucket(
cors_rules=None,
lifecycle_rules=None,
default_server_side_encryption: Optional[EncryptionSetting] = None,
default_retention: Optional[BucketRetentionSetting] = None,
is_file_lock_enabled: Optional[bool] = None,
):
"""
Create a bucket.
Expand All @@ -187,6 +190,8 @@ def create_bucket(
:param dict cors_rules: bucket CORS rules to store with the bucket
:param dict lifecycle_rules: bucket lifecycle rules to store with the bucket
:param b2sdk.v1.EncryptionSetting default_server_side_encryption: default server side encryption settings (``None`` if unknown)
:param b2sdk.v1.BucketRetentionSetting default_retention: default retention setting
:param bool is_file_lock_enabled: boolean value specifies whether bucket is File Lock-enabled
:return: a Bucket object
:rtype: b2sdk.v1.Bucket
"""
Expand All @@ -200,6 +205,7 @@ def create_bucket(
cors_rules=cors_rules,
lifecycle_rules=lifecycle_rules,
default_server_side_encryption=default_server_side_encryption,
is_file_lock_enabled=is_file_lock_enabled,
)
bucket = self.BUCKET_FACTORY_CLASS.from_api_bucket_dict(self, response)
assert name == bucket.name, 'API created a bucket with different name\
Expand All @@ -208,6 +214,12 @@ def create_bucket(
than requested: %s != %s' % (
bucket_type, bucket.type_
)
if default_retention is not None:
# server does not support setting default retention on create
# so we provide convinient helper for it
bucket = self.BUCKET_FACTORY_CLASS.from_api_bucket_dict(
self, bucket.update(default_retention=default_retention)
)
self.cache.save_bucket(bucket)
return bucket

Expand Down
87 changes: 86 additions & 1 deletion b2sdk/bucket.py

Large diffs are not rendered by default.

270 changes: 270 additions & 0 deletions b2sdk/file_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
######################################################################
#
# File: b2sdk/file_lock.py
#
# Copyright 2021 Backblaze Inc. All Rights Reserved.
#
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################

from typing import Optional
import enum

from .exception import UnexpectedCloudBehaviour
# TODO: write __repr__ and __eq__ methods for the classes below

ACTIONS_WITHOUT_LOCK_SETTINGS = frozenset(['hide', 'folder'])


@enum.unique
class RetentionMode(enum.Enum):
COMPLIANCE = "compliance" # TODO: docs
GOVERNANCE = "governance" # TODO: docs
NONE = None
UNKNOWN = "unknown"


RETENTION_MODES_REQUIRING_PERIODS = frozenset({RetentionMode.COMPLIANCE, RetentionMode.GOVERNANCE})


class RetentionPeriod:
def __init__(self, years: Optional[int] = None, days: Optional[int] = None):
assert (years is None) != (days is None)
if years is not None:
self.duration = years
self.unit = 'years'
else:
self.duration = days
self.unit = 'days'

@classmethod
def from_period_dict(cls, period_dict):
"""
Build a RetentionPeriod from an object returned by the server, such as:

.. code-block ::

{
"duration": 2,
"unit": "years"
}
"""
return cls(**{period_dict['unit']: period_dict['duration']})

def as_dict(self):
return {
"duration": self.duration,
"unit": self.unit,
}


class FileRetentionSetting:
def __init__(self, mode: RetentionMode, retain_until: Optional[int] = None):
if mode in RETENTION_MODES_REQUIRING_PERIODS and retain_until is None:
raise ValueError('must specify retain_until for retention mode %s' % (mode,))
self.mode = mode
self.retain_until = retain_until

@classmethod
def from_file_version_dict(cls, file_version_dict: dict):
"""
Returns FileRetentionSetting for the given file_version_dict retrieved from the api. E.g.

.. code-block ::

{
"action": "upload",
"fileRetention": {
"isClientAuthorizedToRead": false,
"value": null
},
...
}

{
"action": "upload",
"fileRetention": {
"isClientAuthorizedToRead": true,
"value": {
"mode": "governance",
"retainUntilTimestamp": 1628942493000
}
},
...
}
"""
if 'fileRetention' not in file_version_dict:
if file_version_dict['action'] not in ACTIONS_WITHOUT_LOCK_SETTINGS:
raise UnexpectedCloudBehaviour(
'No fileRetention provided for file version with action=%s' %
(file_version_dict['action'])
)
return NO_RETENTION_FILE_SETTING

retention_dict = file_version_dict['fileRetention']

if not retention_dict['isClientAuthorizedToRead']:
return cls(RetentionMode.UNKNOWN, None)

mode = retention_dict['value']['mode']
if mode is None:
return NO_RETENTION_FILE_SETTING

return cls(
RetentionMode(mode),
retention_dict['value']['retainUntilTimestamp'],
)

def serialize_to_json_for_request(self):
if self.mode is RetentionMode.UNKNOWN:
raise ValueError('cannot use an unknown file retention setting in requests')
return self.as_dict()

def as_dict(self):
return {
"mode": self.mode.value,
"retainUntilTimestamp": self.retain_until,
}

def add_to_to_upload_headers(self, headers):
if self.mode is RetentionMode.UNKNOWN:
raise ValueError('cannot use an unknown file retention setting in requests')

headers['X-Bz-File-Retention-Mode'] = str(
self.mode.value
) # mode = NONE is not supported by the server at the
# moment, but it should be
headers['X-Bz-File-Retention-Retain-Until-Timestamp'] = self.retain_until


class LegalHoldSerializer:
@classmethod
def from_server(cls, file_version_dict) -> Optional[bool]:
if 'legalHold' not in file_version_dict:
if file_version_dict['action'] not in ACTIONS_WITHOUT_LOCK_SETTINGS:
raise UnexpectedCloudBehaviour(
'legalHold not provided for file version with action=%s' %
(file_version_dict['action'])
)
return None
legal_hold_dict = file_version_dict['legalHold']
if legal_hold_dict['value'] is None:
return None
if legal_hold_dict['value'] == 'on':
return True
elif legal_hold_dict['value'] == 'off':
return False
raise ValueError('Unknown legal hold value: %s' % (legal_hold_dict['value'],))

@classmethod
def to_server(cls, bool_value: Optional[bool]) -> str:
if bool_value is None:
raise ValueError('Cannot use unknown legal hold in requests')
if bool_value:
return 'on'
return 'off'

@classmethod
def add_to_upload_headers(cls, bool_value: Optional[bool], headers):
headers['X-Bz-File-Legal-Hold'] = cls.to_server(bool_value)


class BucketRetentionSetting:
def __init__(self, mode: RetentionMode, period: Optional[RetentionPeriod] = None):
if mode in RETENTION_MODES_REQUIRING_PERIODS and period is None:
raise ValueError('must specify period for retention mode %s' % (mode,))
self.mode = mode
self.period = period

@classmethod
def from_bucket_retention_dict(cls, retention_dict: dict):
"""
Build a BucketRetentionSetting from an object returned by the server, such as:

.. code-block::

{
"mode": "compliance",
"period": {
"duration": 7,
"unit": "days"
}
}

"""
period = retention_dict['period']
if period is not None:
period = RetentionPeriod.from_period_dict(period)
return cls(RetentionMode(retention_dict['mode']), period)

def as_dict(self):
result = {
'mode': self.mode.value,
}
if self.period is not None:
result['period'] = self.period.as_dict()
return result

def serialize_to_json_for_request(self):
if self.mode == RetentionMode.UNKNOWN:
raise ValueError('cannot use an unknown file lock configuration in requests')
return self.as_dict()


class FileLockConfiguration:
def __init__(
self,
default_retention: BucketRetentionSetting,
is_file_lock_enabled: Optional[bool],
):
self.default_retention = default_retention
self.is_file_lock_enabled = is_file_lock_enabled

@classmethod
def from_bucket_dict(cls, bucket_dict):
"""
Build a FileLockConfiguration from an object returned by server, such as:

.. code-block::
{
"isClientAuthorizedToRead": true,
"value": {
"defaultRetention": {
"mode": "governance",
"period": {
"duration": 2,
"unit": "years"
}
},
"isFileLockEnabled": true
}
}

or

{
"isClientAuthorizedToRead": false,
"value": null
}
"""

if not bucket_dict['fileLockConfiguration']['isClientAuthorizedToRead']:
return cls(UNKNOWN_BUCKET_RETENTION, None)
retention = BucketRetentionSetting.from_bucket_retention_dict(
bucket_dict['fileLockConfiguration']['value']['defaultRetention']
)
is_file_lock_enabled = bucket_dict['fileLockConfiguration']['value']['isFileLockEnabled']
return cls(retention, is_file_lock_enabled)

def as_dict(self):
return {
"defaultRetention": self.default_retention.as_dict(),
"isFileLockEnabled": self.is_file_lock_enabled,
}


UNKNOWN_BUCKET_RETENTION = BucketRetentionSetting(RetentionMode.UNKNOWN)
UNKNOWN_FILE_LOCK_CONFIGURATION = FileLockConfiguration(UNKNOWN_BUCKET_RETENTION, None)
NO_RETENTION_BUCKET_SETTING = BucketRetentionSetting(RetentionMode.NONE)
NO_RETENTION_FILE_SETTING = FileRetentionSetting(RetentionMode.NONE)
Loading