Skip to content

Commit 9e61b49

Browse files
authored
Merge pull request #353 from Backblaze/object_lock
Object lock bucket upgrade
2 parents c65573f + dfbd080 commit 9e61b49

File tree

10 files changed

+162
-23
lines changed

10 files changed

+162
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
* Logging performance summary of parallel download threads
1111
* Add `max_download_streams_per_file` parameter to B2Api class and underlying structures
12+
* Add `is_file_lock_enabled` parameter to `Bucket.update()` and related methods
1213

1314
### Fixed
1415
* Replace `ReplicationScanResult.source_has_sse_c_enabled` with `source_encryption_mode`

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: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import logging
1414
import re
15+
import warnings
1516
from typing import Any, Dict, Optional
1617

1718
from .utils import camelcase_to_underscore, trace_call
@@ -517,6 +518,21 @@ class CopyArgumentsMismatch(InvalidUserInput):
517518
pass
518519

519520

521+
class DisablingFileLockNotSupported(B2Error):
522+
def __str__(self):
523+
return "Disabling file lock is not supported"
524+
525+
526+
class SourceReplicationConflict(B2Error):
527+
def __str__(self):
528+
return "Operation not supported for buckets with source replication"
529+
530+
531+
class EnablingFileLockOnRestrictedBucket(B2Error):
532+
def __str__(self):
533+
return "Turning on file lock for a restricted bucket is not allowed"
534+
535+
520536
@trace_call(logger)
521537
def interpret_b2_error(
522538
status: int,
@@ -555,21 +571,41 @@ def interpret_b2_error(
555571
return PartSha1Mismatch(post_params.get('fileId'))
556572
elif status == 400 and code == "bad_bucket_id":
557573
return BucketIdNotFound(post_params.get('bucketId'))
558-
elif status == 400 and code in ('bad_request', 'auth_token_limit', 'source_too_large'):
559-
# it's "bad_request" on 2022-03-29, but will become 'auth_token_limit' in 2022-04 # TODO: cleanup after 2022-05-01
574+
elif status == 400 and code == "auth_token_limit":
560575
matcher = UPLOAD_TOKEN_USED_CONCURRENTLY_ERROR_MESSAGE_RE.match(message)
561-
if matcher is not None:
562-
token = matcher.group('token')
563-
return UploadTokenUsedConcurrently(token)
564-
565-
# it's "bad_request" on 2022-03-29, but will become 'source_too_large' in 2022-04 # TODO: cleanup after 2022-05-01
576+
assert matcher is not None, f"unexpected error message: {message}"
577+
token = matcher.group('token')
578+
return UploadTokenUsedConcurrently(token)
579+
elif status == 400 and code == "source_too_large":
566580
matcher = COPY_SOURCE_TOO_BIG_ERROR_MESSAGE_RE.match(message)
567-
if matcher is not None:
568-
size = int(matcher.group('size'))
569-
return CopySourceTooBig(message, code, size)
581+
assert matcher is not None, f"unexpected error message: {message}"
582+
size = int(matcher.group('size'))
583+
return CopySourceTooBig(message, code, size)
584+
elif status == 400 and code == 'file_lock_conflict':
585+
return DisablingFileLockNotSupported()
586+
elif status == 400 and code == 'source_replication_conflict':
587+
return SourceReplicationConflict()
588+
elif status == 400 and code == 'restricted_bucket_conflict':
589+
return EnablingFileLockOnRestrictedBucket()
590+
elif status == 400 and code == 'bad_request':
591+
592+
# it's "bad_request" on 2022-09-14, but will become 'disabling_file_lock_not_allowed' # TODO: cleanup after 2022-09-22
593+
if message == 'fileLockEnabled value of false is not allowed when bucket is already file lock enabled.':
594+
return DisablingFileLockNotSupported()
595+
596+
# it's "bad_request" on 2022-09-14, but will become 'source_replication_conflict' # TODO: cleanup after 2022-09-22
597+
if message == 'Turning on file lock for an existing bucket having source replication configuration is not allowed.':
598+
return SourceReplicationConflict()
599+
600+
# it's "bad_request" on 2022-09-14, but will become 'restricted_bucket_conflict' # TODO: cleanup after 2022-09-22
601+
if message == 'Turning on file lock for a restricted bucket is not allowed.':
602+
return EnablingFileLockOnRestrictedBucket()
570603

571604
return BadRequest(message, code)
572605
elif status == 400:
606+
warnings.warn(
607+
f"bad request exception with an unknown `code`. message={message}, code={code}"
608+
)
573609
return BadRequest(message, code)
574610
elif status == 401 and code in ("bad_auth_token", "expired_auth_token"):
575611
return InvalidAuthToken(message, code)

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

0 commit comments

Comments
 (0)