Skip to content

Commit bbbd465

Browse files
authored
Merge pull request #318 from Backblaze/prepare-replication-prerelease
Replication prerelease
2 parents 280c54b + d195fcd commit bbbd465

File tree

24 files changed

+1331
-120
lines changed

24 files changed

+1331
-120
lines changed

CHANGELOG.md

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

77
## [Unreleased]
88

9+
## [1.16.0] - 2022-04-27
10+
11+
This release contains a preview of replication support. It allows for basic
12+
usage of B2 replication feature (currently in closed beta).
13+
14+
As the interface of the sdk (and the server api) may change, the replication
15+
support shall be considered PRIVATE interface and should be used with caution.
16+
Please consult the documentation on how to safely use the private api interface.
17+
18+
Expect substantial amount of work on sdk interface:
19+
* The interface of `ReplicationConfiguration` WILL change
20+
* The interface of `FileVersion.replication_status` MIGHT change
21+
* The interface of `FileVersionDownload` MIGHT change
22+
23+
### Added
24+
* Add basic replication support to `Bucket` and `FileVersion`
25+
* Add `is_master_key()` method to `AbstractAccountInfo`
26+
* Add `readBucketReplications` and `writeBucketReplications` to `ALL_CAPABILITIES`
27+
* Add log tracing of `interpret_b2_error`
28+
* Add `ReplicationSetupHelper`
29+
30+
### Fixed
31+
* Fix license test on Windows
32+
* Fix cryptic errors when running integration tests with a non-full key
33+
934
## [1.15.0] - 2022-04-12
1035

1136
### Changed
@@ -22,9 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2247
* Add a parameter to customize `sync_policy_manager`
2348
* Add parameters to set the min/max part size for large file upload/copy methods
2449
* Add CopySourceTooBig exception
25-
* Add an option to set a custom file version class to FileVersionFactory
50+
* Add an option to set a custom file version class to `FileVersionFactory`
2651
* Add an option for B2Api to turn off hash checking for downloaded files
27-
* Add an option for B2Api to set write buffer size for DownloadedFile.save_to method
52+
* Add an option for B2Api to set write buffer size for `DownloadedFile.save_to` method
2853
* Add support for multiple profile files for SqliteAccountInfo
2954

3055
### Fixed

b2sdk/_v3/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@
202202
from b2sdk.sync.encryption_provider import ServerDefaultSyncEncryptionSettingsProvider
203203
from b2sdk.sync.encryption_provider import SERVER_DEFAULT_SYNC_ENCRYPTION_SETTINGS_PROVIDER
204204

205+
# replication
206+
207+
from b2sdk.replication.setting import ReplicationConfigurationFactory
208+
from b2sdk.replication.setting import ReplicationConfiguration
209+
from b2sdk.replication.setting import ReplicationSourceConfiguration
210+
from b2sdk.replication.setting import ReplicationRule
211+
from b2sdk.replication.setting import ReplicationDestinationConfiguration
212+
from b2sdk.replication.setup import ReplicationSetupHelper
213+
205214
# other
206215

207216
from b2sdk.b2http import B2Http

b2sdk/account_info/abstract.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ def is_same_account(self, account_id: str, realm: str) -> bool:
126126
except exception.MissingAccountData:
127127
return False
128128

129+
def is_master_key(self) -> bool:
130+
return self.get_account_id() == self.get_application_key_id()
131+
129132
@abstractmethod
130133
def get_account_id(self):
131134
"""

b2sdk/api.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .cache import AbstractCache
1717
from .bucket import Bucket, BucketFactory
1818
from .encryption.setting import EncryptionSetting
19+
from .replication.setting import ReplicationConfiguration
1920
from .exception import BucketIdNotFound, NonExistentBucket, RestrictedBucket
2021
from .file_lock import FileRetentionSetting, LegalHold
2122
from .file_version import DownloadVersionFactory, FileIdAndName, FileVersion, FileVersionFactory
@@ -211,6 +212,7 @@ def create_bucket(
211212
lifecycle_rules=None,
212213
default_server_side_encryption: Optional[EncryptionSetting] = None,
213214
is_file_lock_enabled: Optional[bool] = None,
215+
replication: Optional[ReplicationConfiguration] = None,
214216
):
215217
"""
216218
Create a bucket.
@@ -222,6 +224,7 @@ def create_bucket(
222224
:param dict lifecycle_rules: bucket lifecycle rules to store with the bucket
223225
:param b2sdk.v2.EncryptionSetting default_server_side_encryption: default server side encryption settings (``None`` if unknown)
224226
:param bool is_file_lock_enabled: boolean value specifies whether bucket is File Lock-enabled
227+
:param b2sdk.v2.ReplicationConfiguration replication: bucket replication rules or ``None``
225228
:return: a Bucket object
226229
:rtype: b2sdk.v2.Bucket
227230
"""
@@ -236,6 +239,7 @@ def create_bucket(
236239
lifecycle_rules=lifecycle_rules,
237240
default_server_side_encryption=default_server_side_encryption,
238241
is_file_lock_enabled=is_file_lock_enabled,
242+
replication=replication,
239243
)
240244
bucket = self.BUCKET_FACTORY_CLASS.from_api_bucket_dict(self, response)
241245
assert name == bucket.name, 'API created a bucket with different name\
@@ -323,7 +327,7 @@ def get_bucket_by_id(self, bucket_id: str) -> Bucket:
323327
# There is no such bucket.
324328
raise BucketIdNotFound(bucket_id)
325329

326-
def get_bucket_by_name(self, bucket_name):
330+
def get_bucket_by_name(self, bucket_name: str):
327331
"""
328332
Return the Bucket matching the given bucket_name.
329333
@@ -526,12 +530,25 @@ def list_keys(self, start_application_key_id: Optional[str] = None
526530
return
527531
start_application_key_id = next_application_key_id
528532

533+
def get_key(self, key_id: str) -> Optional[ApplicationKey]:
534+
"""
535+
Gets information about a single key: it's capabilities, prefix, name etc
536+
537+
Returns `None` if the key does not exist.
538+
539+
Raises an exception if profile is not permitted to list keys.
540+
"""
541+
return next(
542+
self.list_keys(start_application_key_id=key_id),
543+
None,
544+
)
545+
529546
# other
530547
def get_file_info(self, file_id: str) -> FileVersion:
531548
"""
532549
Gets info about file version.
533550
534-
:param str file_id: the id of the file who's info will be retrieved.
551+
:param str file_id: the id of the file whose info will be retrieved.
535552
"""
536553
return self.file_version_factory.from_api_response(
537554
self.session.get_file_info_by_id(file_id)

b2sdk/application_key.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ def parse_response_dict(cls, response: dict):
6565
for key, value in optional_args.items() if value is not None},
6666
}
6767

68+
def has_capabilities(self, capabilities) -> bool:
69+
""" checks whether the key has ALL of the given capabilities """
70+
return len(set(capabilities) - set(self.capabilities)) == 0
71+
6872
def as_dict(self):
6973
"""Represent the key as a dict, like the one returned by B2 cloud"""
7074
mandatory_keys = {

b2sdk/bucket.py

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,41 @@
99
######################################################################
1010

1111
import logging
12+
1213
from typing import Optional, Tuple
1314

1415
from .encryption.setting import EncryptionSetting, EncryptionSettingFactory
1516
from .encryption.types import EncryptionMode
16-
from .exception import BucketIdNotFound, CopySourceTooBig, FileNotPresent, FileOrBucketNotFound, UnexpectedCloudBehaviour, UnrecognizedBucketType
17+
from .exception import (
18+
BucketIdNotFound,
19+
CopySourceTooBig,
20+
FileNotPresent,
21+
FileOrBucketNotFound,
22+
UnexpectedCloudBehaviour,
23+
UnrecognizedBucketType,
24+
)
1725
from .file_lock import (
26+
UNKNOWN_BUCKET_RETENTION,
1827
BucketRetentionSetting,
1928
FileLockConfiguration,
2029
FileRetentionSetting,
21-
UNKNOWN_BUCKET_RETENTION,
2230
LegalHold,
2331
)
2432
from .file_version import DownloadVersion, FileVersion
2533
from .progress import AbstractProgressListener, DoNothingProgressListener
34+
from .replication.setting import ReplicationConfiguration, ReplicationConfigurationFactory
2635
from .transfer.emerge.executor import AUTO_CONTENT_TYPE
2736
from .transfer.emerge.write_intent import WriteIntent
2837
from .transfer.inbound.downloaded_file import DownloadedFile
2938
from .transfer.outbound.copy_source import CopySource
3039
from .transfer.outbound.upload_source import UploadSourceBytes, UploadSourceLocalFile
31-
from .utils import B2TraceMeta, disable_trace, limit_trace_arguments
32-
from .utils import b2_url_encode, validate_b2_file_name
40+
from .utils import (
41+
B2TraceMeta,
42+
b2_url_encode,
43+
disable_trace,
44+
limit_trace_arguments,
45+
validate_b2_file_name,
46+
)
3347

3448
logger = logging.getLogger(__name__)
3549

@@ -58,6 +72,7 @@ def __init__(
5872
),
5973
default_retention: BucketRetentionSetting = UNKNOWN_BUCKET_RETENTION,
6074
is_file_lock_enabled: Optional[bool] = None,
75+
replication: Optional[ReplicationConfiguration] = None,
6176
):
6277
"""
6378
:param b2sdk.v2.B2Api api: an API object
@@ -73,6 +88,7 @@ def __init__(
7388
:param b2sdk.v2.EncryptionSetting default_server_side_encryption: default server side encryption settings
7489
:param b2sdk.v2.BucketRetentionSetting default_retention: default retention setting
7590
:param bool is_file_lock_enabled: whether file locking is enabled or not
91+
:param b2sdk.v2.ReplicationConfiguration replication: replication rules for the bucket
7692
"""
7793
self.api = api
7894
self.id_ = id_
@@ -87,6 +103,7 @@ def __init__(
87103
self.default_server_side_encryption = default_server_side_encryption
88104
self.default_retention = default_retention
89105
self.is_file_lock_enabled = is_file_lock_enabled
106+
self.replication = replication
90107

91108
def get_fresh_state(self) -> 'Bucket':
92109
"""
@@ -98,15 +115,15 @@ def get_fresh_state(self) -> 'Bucket':
98115
raise BucketIdNotFound(self.id_)
99116
return buckets_found[0]
100117

101-
def get_id(self):
118+
def get_id(self) -> str:
102119
"""
103120
Return bucket ID.
104121
105122
:rtype: str
106123
"""
107124
return self.id_
108125

109-
def set_info(self, new_bucket_info, if_revision_is=None):
126+
def set_info(self, new_bucket_info, if_revision_is=None) -> 'Bucket':
110127
"""
111128
Update bucket info.
112129
@@ -115,7 +132,7 @@ def set_info(self, new_bucket_info, if_revision_is=None):
115132
"""
116133
return self.update(bucket_info=new_bucket_info, if_revision_is=if_revision_is)
117134

118-
def set_type(self, bucket_type):
135+
def set_type(self, bucket_type) -> 'Bucket':
119136
"""
120137
Update bucket type.
121138
@@ -132,7 +149,8 @@ def update(
132149
if_revision_is: Optional[int] = None,
133150
default_server_side_encryption: Optional[EncryptionSetting] = None,
134151
default_retention: Optional[BucketRetentionSetting] = None,
135-
):
152+
replication: Optional[ReplicationConfiguration] = None,
153+
) -> 'Bucket':
136154
"""
137155
Update various bucket parameters.
138156
@@ -143,6 +161,7 @@ def update(
143161
:param if_revision_is: revision number, update the info **only if** *revision* equals to *if_revision_is*
144162
:param default_server_side_encryption: default server side encryption settings (``None`` if unknown)
145163
:param default_retention: bucket default retention setting
164+
:param replication: replication rules for the bucket;
146165
"""
147166
account_id = self.api.account_info.get_account_id()
148167
return self.api.BUCKET_FACTORY_CLASS.from_api_bucket_dict(
@@ -157,6 +176,7 @@ def update(
157176
if_revision_is=if_revision_is,
158177
default_server_side_encryption=default_server_side_encryption,
159178
default_retention=default_retention,
179+
replication=replication,
160180
)
161181
)
162182

@@ -936,6 +956,7 @@ def as_dict(self):
936956
result['defaultServerSideEncryption'] = self.default_server_side_encryption.as_dict()
937957
result['isFileLockEnabled'] = self.is_file_lock_enabled
938958
result['defaultRetention'] = self.default_retention.as_dict()
959+
result['replication'] = self.replication and self.replication.as_dict()
939960

940961
return result
941962

@@ -967,32 +988,64 @@ def from_api_bucket_dict(cls, api, bucket_dict):
967988
968989
.. code-block:: python
969990
970-
{
971-
"bucketType": "allPrivate",
972-
"bucketId": "a4ba6a39d8b6b5fd561f0010",
973-
"bucketName": "zsdfrtsazsdfafr",
974-
"accountId": "4aa9865d6f00",
975-
"bucketInfo": {},
976-
"options": [],
977-
"revision": 1,
978-
"defaultServerSideEncryption": {
979-
"isClientAuthorizedToRead" : true,
980-
"value": {
981-
"algorithm" : "AES256",
982-
"mode" : "SSE-B2"
983-
}
984-
},
985-
"fileLockConfiguration": {
986-
"isClientAuthorizedToRead": true,
987-
"value": {
988-
"defaultRetention": {
989-
"mode": null,
990-
"period": null
991+
{
992+
"bucketType": "allPrivate",
993+
"bucketId": "a4ba6a39d8b6b5fd561f0010",
994+
"bucketName": "zsdfrtsazsdfafr",
995+
"accountId": "4aa9865d6f00",
996+
"bucketInfo": {},
997+
"options": [],
998+
"revision": 1,
999+
"defaultServerSideEncryption": {
1000+
"isClientAuthorizedToRead" : true,
1001+
"value": {
1002+
"algorithm" : "AES256",
1003+
"mode" : "SSE-B2"
1004+
}
1005+
},
1006+
"fileLockConfiguration": {
1007+
"isClientAuthorizedToRead": true,
1008+
"value": {
1009+
"defaultRetention": {
1010+
"mode": null,
1011+
"period": null
1012+
},
1013+
"isFileLockEnabled": false
1014+
}
1015+
},
1016+
"replicationConfiguration": {
1017+
"clientIsAllowedToRead": true,
1018+
"value": {
1019+
"asReplicationSource": {
1020+
"replicationRules": [
1021+
{
1022+
"destinationBucketId": "c5f35d53a90a7ea284fb0719",
1023+
"fileNamePrefix": "",
1024+
"includeExistingFiles": True,
1025+
"isEnabled": true,
1026+
"priority": 1,
1027+
"replicationRuleName": "replication-us-west"
1028+
},
1029+
{
1030+
"destinationBucketId": "55f34d53a96a7ea284fb0719",
1031+
"fileNamePrefix": "",
1032+
"includeExistingFiles": True,
1033+
"isEnabled": true,
1034+
"priority": 2,
1035+
"replicationRuleName": "replication-us-west-2"
1036+
}
1037+
],
1038+
"sourceApplicationKeyId": "10053d55ae26b790000000006"
9911039
},
992-
"isFileLockEnabled": false
1040+
"asReplicationDestination": {
1041+
"sourceToDestinationKeyMapping": {
1042+
"10053d55ae26b790000000045": "10053d55ae26b790000000004",
1043+
"10053d55ae26b790000000046": "10053d55ae26b790030000004"
1044+
}
1045+
}
9931046
}
994-
}
995-
}
1047+
}
1048+
}
9961049
9971050
into a Bucket object.
9981051
@@ -1016,6 +1069,7 @@ def from_api_bucket_dict(cls, api, bucket_dict):
10161069
raise UnexpectedCloudBehaviour('server did not provide `defaultServerSideEncryption`')
10171070
default_server_side_encryption = EncryptionSettingFactory.from_bucket_dict(bucket_dict)
10181071
file_lock_configuration = FileLockConfiguration.from_bucket_dict(bucket_dict)
1072+
replication = ReplicationConfigurationFactory.from_bucket_dict(bucket_dict).value
10191073
return cls.BUCKET_CLASS(
10201074
api,
10211075
bucket_id,
@@ -1030,4 +1084,5 @@ def from_api_bucket_dict(cls, api, bucket_dict):
10301084
default_server_side_encryption,
10311085
file_lock_configuration.default_retention,
10321086
file_lock_configuration.is_file_lock_enabled,
1087+
replication,
10331088
)

0 commit comments

Comments
 (0)