Skip to content

Commit 6fe915a

Browse files
Add is_file_lock_enabled parameter to Bucket.update()
1 parent ca98eb6 commit 6fe915a

File tree

10 files changed

+132
-13
lines changed

10 files changed

+132
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
* Add `is_file_lock_enabled` parameter to `Bucket.update()` and related methods
11+
912
### Fixed
1013
* Replace `ReplicationScanResult.source_has_sse_c_enabled` with `source_encryption_mode`
1114
* Fix `B2Api.get_key()` and `RawSimulator.delete_key()`

b2sdk/_v3/exception.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@
2626
from b2sdk.exception import BadJson
2727
from b2sdk.exception import BadRequest
2828
from b2sdk.exception import BadUploadUrl
29-
from b2sdk.exception import BucketIdNotFound
3029
from b2sdk.exception import BrokenPipe
30+
from b2sdk.exception import BucketIdNotFound
3131
from b2sdk.exception import BucketNotAllowed
32-
from b2sdk.exception import CapabilityNotAllowed
3332
from b2sdk.exception import CapExceeded
33+
from b2sdk.exception import CapabilityNotAllowed
3434
from b2sdk.exception import ChecksumMismatch
3535
from b2sdk.exception import ClockSkew
3636
from b2sdk.exception import Conflict
3737
from b2sdk.exception import ConnectionReset
3838
from b2sdk.exception import CopyArgumentsMismatch
3939
from b2sdk.exception import DestFileNewer
40+
from b2sdk.exception import DisablingFileLockNotSupported
4041
from b2sdk.exception import DuplicateBucketName
4142
from b2sdk.exception import FileAlreadyHidden
4243
from b2sdk.exception import FileNameNotAllowed
@@ -54,11 +55,14 @@
5455
from b2sdk.exception import PartSha1Mismatch
5556
from b2sdk.exception import RestrictedBucket
5657
from b2sdk.exception import RetentionWriteError
58+
from b2sdk.exception import SSECKeyError
59+
from b2sdk.exception import SSECKeyIdMismatchInCopy
5760
from b2sdk.exception import ServiceError
61+
from b2sdk.exception import SourceReplicationConflict
5862
from b2sdk.exception import StorageCapExceeded
5963
from b2sdk.exception import TooManyRequests
60-
from b2sdk.exception import TransientErrorMixin
6164
from b2sdk.exception import TransactionCapExceeded
65+
from b2sdk.exception import TransientErrorMixin
6266
from b2sdk.exception import TruncatedOutput
6367
from b2sdk.exception import Unauthorized
6468
from b2sdk.exception import UnexpectedCloudBehaviour
@@ -67,8 +71,6 @@
6771
from b2sdk.exception import UnrecognizedBucketType
6872
from b2sdk.exception import UnsatisfiableRange
6973
from b2sdk.exception import UnusableFileName
70-
from b2sdk.exception import SSECKeyIdMismatchInCopy
71-
from b2sdk.exception import SSECKeyError
7274
from b2sdk.exception import WrongEncryptionModeForBucketDefault
7375
from b2sdk.exception import interpret_b2_error
7476
from b2sdk.sync.exception import IncompleteSync
@@ -100,15 +102,16 @@
100102
'BrokenPipe',
101103
'BucketIdNotFound',
102104
'BucketNotAllowed',
103-
'CapabilityNotAllowed',
104105
'CapExceeded',
106+
'CapabilityNotAllowed',
105107
'ChecksumMismatch',
106108
'ClockSkew',
107109
'Conflict',
108110
'ConnectionReset',
109111
'CopyArgumentsMismatch',
110112
'CorruptAccountInfo',
111113
'DestFileNewer',
114+
'DisablingFileLockNotSupported',
112115
'DuplicateBucketName',
113116
'EmptyDirectory',
114117
'EnvironmentEncodingError',
@@ -133,19 +136,20 @@
133136
'RestrictedBucket',
134137
'RetentionWriteError',
135138
'ServiceError',
139+
'SourceReplicationConflict',
136140
'StorageCapExceeded',
137141
'TooManyRequests',
138142
'TransactionCapExceeded',
139143
'TransientErrorMixin',
140144
'TruncatedOutput',
145+
'UnableToCreateDirectory',
141146
'Unauthorized',
142147
'UnexpectedCloudBehaviour',
143148
'UnknownError',
144149
'UnknownHost',
145150
'UnrecognizedBucketType',
146-
'UnableToCreateDirectory',
147-
'UnsupportedFilename',
148151
'UnsatisfiableRange',
152+
'UnsupportedFilename',
149153
'UnusableFileName',
150154
'interpret_b2_error',
151155
'check_invalid_argument',

b2sdk/bucket.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def update(
150150
default_server_side_encryption: Optional[EncryptionSetting] = None,
151151
default_retention: Optional[BucketRetentionSetting] = None,
152152
replication: Optional[ReplicationConfiguration] = None,
153+
is_file_lock_enabled: Optional[bool] = None,
153154
) -> 'Bucket':
154155
"""
155156
Update various bucket parameters.
@@ -161,7 +162,8 @@ def update(
161162
:param if_revision_is: revision number, update the info **only if** *revision* equals to *if_revision_is*
162163
:param default_server_side_encryption: default server side encryption settings (``None`` if unknown)
163164
:param default_retention: bucket default retention setting
164-
:param replication: replication rules for the bucket;
165+
:param replication: replication rules for the bucket
166+
:param bool is_file_lock_enabled: specifies whether bucket should get File Lock-enabled
165167
"""
166168
account_id = self.api.account_info.get_account_id()
167169
return self.api.BUCKET_FACTORY_CLASS.from_api_bucket_dict(
@@ -177,6 +179,7 @@ def update(
177179
default_server_side_encryption=default_server_side_encryption,
178180
default_retention=default_retention,
179181
replication=replication,
182+
is_file_lock_enabled=is_file_lock_enabled,
180183
)
181184
)
182185

b2sdk/exception.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,16 @@ class CopyArgumentsMismatch(InvalidUserInput):
517517
pass
518518

519519

520+
class DisablingFileLockNotSupported(B2Error):
521+
def __str__(self):
522+
return "Disabling file lock is not supported"
523+
524+
525+
class SourceReplicationConflict(B2Error):
526+
def __str__(self):
527+
return "Operation not supported for buckets with source replication"
528+
529+
520530
@trace_call(logger)
521531
def interpret_b2_error(
522532
status: int,
@@ -569,6 +579,10 @@ def interpret_b2_error(
569579
return CopySourceTooBig(size)
570580

571581
return BadRequest(message, code)
582+
elif status == 400 and code == 'disabling_file_lock_not_allowed':
583+
raise DisablingFileLockNotSupported()
584+
elif status == 400 and code == 'source_replication_conflict':
585+
raise SourceReplicationConflict()
572586
elif status == 400:
573587
return BadRequest(message, code)
574588
elif status == 401 and code in ("bad_auth_token", "expired_auth_token"):

b2sdk/raw_api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ def update_bucket(
288288
default_server_side_encryption: Optional[EncryptionSetting] = None,
289289
default_retention: Optional[BucketRetentionSetting] = None,
290290
replication: Optional[ReplicationConfiguration] = None,
291+
is_file_lock_enabled: Optional[bool] = None,
291292
):
292293
pass
293294

@@ -740,6 +741,7 @@ def update_bucket(
740741
default_server_side_encryption: Optional[EncryptionSetting] = None,
741742
default_retention: Optional[BucketRetentionSetting] = None,
742743
replication: Optional[ReplicationConfiguration] = None,
744+
is_file_lock_enabled: Optional[bool] = None,
743745
):
744746
kwargs = {}
745747
if if_revision_is is not None:
@@ -761,6 +763,8 @@ def update_bucket(
761763
kwargs['defaultRetention'] = default_retention.serialize_to_json_for_request()
762764
if replication is not None:
763765
kwargs['replicationConfiguration'] = replication.serialize_to_json_for_request()
766+
if is_file_lock_enabled is not None:
767+
kwargs['fileLockEnabled'] = is_file_lock_enabled
764768

765769
assert kwargs
766770

b2sdk/raw_simulator.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
ChecksumMismatch,
3434
Conflict,
3535
CopySourceTooBig,
36+
DisablingFileLockNotSupported,
3637
DuplicateBucketName,
3738
FileNotPresent,
3839
FileSha1Mismatch,
@@ -42,6 +43,7 @@
4243
NonExistentBucket,
4344
PartSha1Mismatch,
4445
SSECKeyError,
46+
SourceReplicationConflict,
4547
Unauthorized,
4648
UnsatisfiableRange,
4749
)
@@ -946,10 +948,23 @@ def _update_bucket(
946948
default_server_side_encryption: Optional[EncryptionSetting] = None,
947949
default_retention: Optional[BucketRetentionSetting] = None,
948950
replication: Optional[ReplicationConfiguration] = None,
951+
is_file_lock_enabled: Optional[bool] = None,
949952
):
950953
if if_revision_is is not None and self.revision != if_revision_is:
951954
raise Conflict()
952955

956+
if is_file_lock_enabled is not None:
957+
if self.is_file_lock_enabled and not is_file_lock_enabled:
958+
raise DisablingFileLockNotSupported()
959+
960+
if (
961+
not self.is_file_lock_enabled and is_file_lock_enabled and self.replication and
962+
self.replication.is_source
963+
):
964+
raise SourceReplicationConflict()
965+
966+
self.is_file_lock_enabled = is_file_lock_enabled
967+
953968
if bucket_type is not None:
954969
self.bucket_type = bucket_type
955970
if bucket_info is not None:
@@ -962,8 +977,10 @@ def _update_bucket(
962977
self.default_server_side_encryption = default_server_side_encryption
963978
if default_retention:
964979
self.default_retention = default_retention
980+
if replication is not None:
981+
self.replication = replication
982+
965983
self.revision += 1
966-
self.replication = replication
967984
return self.bucket_dict(self.api.current_token)
968985

969986
def upload_file(
@@ -1716,8 +1733,9 @@ def update_bucket(
17161733
default_server_side_encryption: Optional[EncryptionSetting] = None,
17171734
default_retention: Optional[BucketRetentionSetting] = None,
17181735
replication: Optional[ReplicationConfiguration] = None,
1736+
is_file_lock_enabled: Optional[bool] = None,
17191737
):
1720-
assert bucket_type or bucket_info or cors_rules or lifecycle_rules or default_server_side_encryption or replication
1738+
assert bucket_type or bucket_info or cors_rules or lifecycle_rules or default_server_side_encryption or replication or is_file_lock_enabled is not None
17211739
bucket = self._get_bucket_by_id(bucket_id)
17221740
self._assert_account_auth(api_url, account_auth_token, bucket.account_id, 'writeBuckets')
17231741
return bucket._update_bucket(
@@ -1729,6 +1747,7 @@ def update_bucket(
17291747
default_server_side_encryption=default_server_side_encryption,
17301748
default_retention=default_retention,
17311749
replication=replication,
1750+
is_file_lock_enabled=is_file_lock_enabled,
17321751
)
17331752

17341753
@classmethod

b2sdk/session.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ def update_bucket(
320320
default_server_side_encryption: Optional[EncryptionSetting] = None,
321321
default_retention: Optional[BucketRetentionSetting] = None,
322322
replication: Optional[ReplicationConfiguration] = None,
323+
is_file_lock_enabled: Optional[bool] = None,
323324
):
324325
return self._wrap_default_token(
325326
self.raw_api.update_bucket,
@@ -333,6 +334,7 @@ def update_bucket(
333334
default_server_side_encryption=default_server_side_encryption,
334335
default_retention=default_retention,
335336
replication=replication,
337+
is_file_lock_enabled=is_file_lock_enabled,
336338
)
337339

338340
def upload_file(

b2sdk/v1/bucket.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def update(
209209
if_revision_is: Optional[int] = None,
210210
default_server_side_encryption: Optional[v2.EncryptionSetting] = None,
211211
default_retention: Optional[v2.BucketRetentionSetting] = None,
212+
is_file_lock_enabled: Optional[bool] = None,
212213
**kwargs
213214
):
214215
"""
@@ -221,6 +222,7 @@ def update(
221222
:param if_revision_is: revision number, update the info **only if** *revision* equals to *if_revision_is*
222223
:param default_server_side_encryption: default server side encryption settings (``None`` if unknown)
223224
:param default_retention: bucket default retention setting
225+
:param bool is_file_lock_enabled: specifies whether bucket should get File Lock-enabled
224226
"""
225227
# allow common tests to execute without hitting attributeerror
226228

@@ -240,6 +242,7 @@ def update(
240242
if_revision_is=if_revision_is,
241243
default_server_side_encryption=default_server_side_encryption,
242244
default_retention=default_retention,
245+
is_file_lock_enabled=is_file_lock_enabled,
243246
)
244247

245248
def ls(

test/integration/test_raw_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from b2sdk.b2http import B2Http
2121
from b2sdk.encryption.setting import EncryptionAlgorithm, EncryptionMode, EncryptionSetting
22+
from b2sdk.exception import DisablingFileLockNotSupported
2223
from b2sdk.replication.setting import ReplicationConfiguration, ReplicationRule
2324
from b2sdk.replication.types import ReplicationStatus
2425
from b2sdk.file_lock import BucketRetentionSetting, NO_RETENTION_FILE_SETTING, RetentionMode, RetentionPeriod
@@ -519,9 +520,24 @@ def raw_api_test_helper(raw_api, should_cleanup_old_buckets):
519520
default_retention=BucketRetentionSetting(
520521
mode=RetentionMode.GOVERNANCE, period=RetentionPeriod(days=1)
521522
),
523+
is_file_lock_enabled=True,
522524
)
523525
assert first_bucket_revision < updated_bucket['revision']
524526

527+
# NOTE: this update_bucket call is only here to be able to find out the error code returned by
528+
# the server if an attempt is made to disable file lock. It has to be done here since the CLI
529+
# by design does not allow disabling file lock at all (i.e. there is no --fileLockEnabled=false
530+
# option or anything equivalent to that).
531+
with pytest.raises(DisablingFileLockNotSupported):
532+
raw_api.update_bucket(
533+
api_url,
534+
account_auth_token,
535+
account_id,
536+
bucket_id,
537+
'allPrivate',
538+
is_file_lock_enabled=False,
539+
)
540+
525541
# Clean up this test.
526542
_clean_and_delete_bucket(raw_api, api_url, account_auth_token, account_id, bucket_id)
527543

test/unit/bucket/test_bucket.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@
2323
AlreadyFailed,
2424
B2Error,
2525
B2RequestTimeoutDuringUpload,
26+
BadRequest,
2627
BucketIdNotFound,
28+
DisablingFileLockNotSupported,
29+
FileSha1Mismatch,
2730
InvalidAuthToken,
2831
InvalidMetadataDirective,
2932
InvalidRange,
3033
InvalidUploadSource,
3134
MaxRetriesExceeded,
32-
UnsatisfiableRange,
33-
FileSha1Mismatch,
3435
SSECKeyError,
36+
SourceReplicationConflict,
37+
UnsatisfiableRange,
3538
)
3639
if apiver_deps.V <= 1:
3740
from apiver_deps import DownloadDestBytes, PreSeekedDownloadDest
@@ -1063,6 +1066,54 @@ def test_update_if_revision_is(self):
10631066
not_updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
10641067
self.assertEqual([{'life': 'is life'}], not_updated_bucket.lifecycle_rules)
10651068

1069+
def test_is_file_lock_enabled(self):
1070+
assert not self.bucket.is_file_lock_enabled
1071+
1072+
# set is_file_lock_enabled to False when it's already false
1073+
self.bucket.update(is_file_lock_enabled=False)
1074+
updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
1075+
assert not updated_bucket.is_file_lock_enabled
1076+
1077+
# sunny day scenario
1078+
self.bucket.update(is_file_lock_enabled=True)
1079+
updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
1080+
assert updated_bucket.is_file_lock_enabled
1081+
assert self.simulator.bucket_name_to_bucket[self.bucket.name].is_file_lock_enabled
1082+
1083+
# attempt to clear is_file_lock_enabled
1084+
with pytest.raises(DisablingFileLockNotSupported):
1085+
self.bucket.update(is_file_lock_enabled=False)
1086+
updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
1087+
assert updated_bucket.is_file_lock_enabled
1088+
1089+
# attempt to set is_file_lock_enabled when it's already set
1090+
self.bucket.update(is_file_lock_enabled=True)
1091+
updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
1092+
assert updated_bucket.is_file_lock_enabled
1093+
1094+
@pytest.mark.apiver(from_ver=2)
1095+
def test_is_file_lock_enabled_source_replication(self):
1096+
assert not self.bucket.is_file_lock_enabled
1097+
1098+
# attempt to set is_file_lock_enabled with source replication enabled
1099+
self.bucket.update(replication=REPLICATION)
1100+
with pytest.raises(SourceReplicationConflict):
1101+
self.bucket.update(is_file_lock_enabled=True)
1102+
updated_bucket = self.bucket.update(replication=REPLICATION)
1103+
assert not updated_bucket.is_file_lock_enabled
1104+
1105+
# sunny day scenario
1106+
self.bucket.update(
1107+
replication=ReplicationConfiguration(
1108+
rules=[],
1109+
source_to_destination_key_mapping={},
1110+
)
1111+
)
1112+
self.bucket.update(is_file_lock_enabled=True)
1113+
updated_bucket = self.api.get_bucket_by_name(self.bucket.name)
1114+
assert updated_bucket.is_file_lock_enabled
1115+
assert self.simulator.bucket_name_to_bucket[self.bucket.name].is_file_lock_enabled
1116+
10661117

10671118
class TestUpload(TestCaseWithBucket):
10681119
def test_upload_bytes(self):

0 commit comments

Comments
 (0)