Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileVersion and B2Folder hold references to B2Api #174

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Refactored `sync.file.*File` and `sync.file.*FileVersion` to `sync.path.*SyncPath`
* Refactored `FileVersionInfo` to `FileVersion`
* `ScanPoliciesManager` exclusion interface changed
* `B2Api` unittests for v0, v1 and v2 are now common
* `B2Api.cancel_large_file` returns a `FileIdAndName` object instead of a `FileVersion` object in v2
* `FileVersion` has a mandatory `api` parameter
* `B2Folder` holds a handle to B2Api
* `Bucket` unit tests for v1 and v2 are now common

## [1.8.0] - 2021-05-21

Expand Down Expand Up @@ -192,8 +197,7 @@ has changed.
### Added
Initial official release of SDK as a separate package (until now it was a part of B2 CLI)

[Unreleased]: https://github.com/Backblaze/b2-sdk-python/compare/v1.8.0...HEAD
[1.8.0]: https://github.com/Backblaze/b2-sdk-python/compare/v1.7.0...v1.8.0
[Unreleased]: https://github.com/Backblaze/b2-sdk-python/compare/v1.7.0...HEAD
[1.7.0]: https://github.com/Backblaze/b2-sdk-python/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/Backblaze/b2-sdk-python/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/Backblaze/b2-sdk-python/compare/v1.4.0...v1.5.0
Expand Down
27 changes: 11 additions & 16 deletions b2sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ def url_for_api(info, api_name):
class Services(object):
""" Gathers objects that provide high level logic over raw api usage. """

def __init__(self, session, max_upload_workers=10, max_copy_workers=10):
def __init__(self, api, max_upload_workers=10, max_copy_workers=10):
"""
Initialize Services object using given session.

:param b2sdk.v1.Session session:
:param b2sdk.v1.B2Api api:
:param int max_upload_workers: a number of upload threads
:param int max_copy_workers: a number of copy threads
"""
self.session = session
self.api = api
self.session = api.session
self.large_file = LargeFileServices(self)
self.download_manager = DownloadManager(self)
self.upload_manager = UploadManager(self, max_upload_workers=max_upload_workers)
Expand Down Expand Up @@ -121,7 +122,7 @@ def __init__(
"""
self.session = self.SESSION_CLASS(account_info=account_info, cache=cache, raw_api=raw_api)
self.services = Services(
self.session,
self,
max_upload_workers=max_upload_workers,
max_copy_workers=max_copy_workers,
)
Expand Down Expand Up @@ -394,28 +395,22 @@ def list_parts(self, file_id, start_part_number=None, batch_size=None):
)

# delete/cancel
def cancel_large_file(self, file_id):
def cancel_large_file(self, file_id: str) -> FileIdAndName:
"""
Cancel a large file upload.

:param str file_id: a file ID
:rtype: None
"""
return self.services.large_file.cancel_large_file(file_id)

def delete_file_version(self, file_id, file_name):
def delete_file_version(self, file_id: str, file_name: str) -> FileIdAndName:
"""
Permanently and irrevocably delete one version of a file.

:param str file_id: a file ID
:param str file_name: a file name
:rtype: FileIdAndName
"""
# filename argument is not first, because one day it may become optional
response = self.session.delete_file_version(file_id, file_name)
assert response['fileId'] == file_id
assert response['fileName'] == file_name
return FileIdAndName(file_id, file_name)
file_id_and_name = FileIdAndName.from_cancel_or_delete_response(response)
assert file_id_and_name.file_id == file_id
assert file_id_and_name.file_name == file_name
return file_id_and_name

# download
def get_download_url_for_fileid(self, file_id):
Expand Down
12 changes: 7 additions & 5 deletions b2sdk/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ def get_file_info_by_id(self, file_id: str) -> FileVersion:
:param str file_id: the id of the file who's info will be retrieved.
:rtype: generator[b2sdk.v1.FileVersionInfo]
"""
return self.FILE_VERSION_FACTORY.from_api_response(self.api.get_file_info(file_id))
return self.FILE_VERSION_FACTORY.from_api_response(
self.api, self.api.get_file_info(file_id)
)

def get_file_info_by_name(self, file_name: str) -> FileVersion:
"""
Expand All @@ -242,7 +244,7 @@ def get_file_info_by_name(self, file_name: str) -> FileVersion:
"""
try:
return self.FILE_VERSION_FACTORY.from_response_headers(
self.api.session.get_file_info_by_name(self.name, file_name)
self.api, self.api.session.get_file_info_by_name(self.name, file_name)
)
except FileOrBucketNotFound:
raise FileNotPresent(bucket_name=self.name, file_id_or_name=file_name)
Expand Down Expand Up @@ -290,7 +292,7 @@ def list_file_versions(self, file_name, fetch_count=None):
)

for entry in response['files']:
file_version = self.FILE_VERSION_FACTORY.from_api_response(entry)
file_version = self.FILE_VERSION_FACTORY.from_api_response(self.api, entry)
if file_version.file_name != file_name:
# All versions for the requested file name have been listed.
return
Expand Down Expand Up @@ -350,7 +352,7 @@ def ls(self, folder_to_list='', show_versions=False, recursive=False, fetch_coun
else:
response = session.list_file_names(self.id_, start_file_name, fetch_count, prefix)
for entry in response['files']:
file_version = self.FILE_VERSION_FACTORY.from_api_response(entry)
file_version = self.FILE_VERSION_FACTORY.from_api_response(self.api, entry)
if not file_version.file_name.startswith(prefix):
# We're past the files we care about
return
Expand Down Expand Up @@ -788,7 +790,7 @@ def hide_file(self, file_name):
:rtype: b2sdk.v1.FileVersionInfo
"""
response = self.api.session.hide_file(self.id_, file_name)
return self.FILE_VERSION_FACTORY.from_api_response(response)
return self.FILE_VERSION_FACTORY.from_api_response(self.api, response)

def copy(
self,
Expand Down
67 changes: 33 additions & 34 deletions b2sdk/file_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
#
######################################################################

from typing import Optional

from .encryption.setting import EncryptionSetting, EncryptionSettingFactory
from .file_lock import FileRetentionSetting, LegalHold
from .file_lock import FileRetentionSetting, LegalHold, NO_RETENTION_FILE_SETTING
from .raw_api import SRC_LAST_MODIFIED_MILLIS


Expand All @@ -36,6 +34,7 @@ class FileVersion:

__slots__ = [
'id_',
'api',
'file_name',
'size',
'content_type',
Expand All @@ -52,6 +51,7 @@ class FileVersion:

def __init__(
self,
api,
id_,
file_name,
size,
Expand All @@ -60,14 +60,12 @@ def __init__(
file_info,
upload_timestamp,
action,
content_md5=None,
server_side_encryption: Optional[EncryptionSetting] = None, # TODO: make it mandatory in v2
file_retention: Optional[
FileRetentionSetting
] = None, # TODO: in v2 change the default value to NO_RETENTION_FILE_SETTING
legal_hold: Optional[LegalHold
] = None, # TODO: in v2 change the default value to LegalHold.UNSET
content_md5,
server_side_encryption: EncryptionSetting,
file_retention: FileRetentionSetting = NO_RETENTION_FILE_SETTING,
legal_hold: LegalHold = LegalHold.UNSET,
):
self.api = api
self.id_ = id_
self.file_name = file_name
self.size = size
Expand All @@ -93,6 +91,8 @@ def as_dict(self):
'fileName': self.file_name,
'fileInfo': self.file_info,
'legalHold': self.legal_hold.to_dict_repr() if self.legal_hold is not None else None,
'serverSideEncryption': self.server_side_encryption.as_dict(),
'fileRetention': self.file_retention.as_dict(),
}

if self.size is not None:
Expand All @@ -107,10 +107,6 @@ def as_dict(self):
result['contentSha1'] = self.content_sha1
if self.content_md5 is not None:
result['contentMd5'] = self.content_md5
if self.server_side_encryption is not None: # this is for backward compatibility of interface only, b2sdk always sets it
result['serverSideEncryption'] = self.server_side_encryption.as_dict()
if self.file_retention is not None: # this is for backward compatibility of interface only, b2sdk always sets it
result['fileRetention'] = self.file_retention.as_dict()
return result

def __eq__(self, other):
Expand All @@ -120,14 +116,20 @@ def __eq__(self, other):
return False
return True

def __repr__(self):
return '%s(%s)' % (
self.__class__.__name__,
', '.join(repr(getattr(self, attr)) for attr in self.__slots__)
)


class FileVersionFactory(object):
"""
Construct :py:class:`b2sdk.v1.FileVersionInfo` objects from various structures.
"""

@classmethod
def from_api_response(cls, file_version_dict, force_action=None):
def from_api_response(cls, api, file_version_dict, force_action=None):
"""
Turn this:

Expand Down Expand Up @@ -182,6 +184,7 @@ def from_api_response(cls, file_version_dict, force_action=None):
legal_hold = LegalHold.from_file_version_dict(file_version_dict)

return FileVersion(
api,
id_,
file_name,
size,
Expand All @@ -197,21 +200,9 @@ def from_api_response(cls, file_version_dict, force_action=None):
)

@classmethod
def from_cancel_large_file_response(cls, response):
return FileVersion(
response['fileId'],
response['fileName'],
0, # size
'unknown',
'none',
{},
0, # upload timestamp
'cancel'
)

@classmethod
def from_response_headers(cls, headers):
def from_response_headers(cls, api, headers):
return FileVersion(
api=api,
id_=headers.get('x-bz-file-id'),
file_name=headers.get('x-bz-file-name'),
size=headers.get('content-length'),
Expand All @@ -220,6 +211,7 @@ def from_response_headers(cls, headers):
file_info=None,
upload_timestamp=headers.get('x-bz-upload-timestamp'),
action=None,
content_md5=None,
server_side_encryption=EncryptionSettingFactory.from_response_headers(headers),
file_retention=FileRetentionSetting.from_response_headers(headers),
legal_hold=LegalHold.from_response_headers(headers),
Expand All @@ -230,16 +222,23 @@ class FileIdAndName(object):
"""
A structure which represents a B2 cloud file with just `file_name` and `fileId` attributes.

Used to return data from calls to :py:meth:`b2sdk.v1.Bucket.delete_file_version`.

:ivar str ~.file_id: ``fileId``
:ivar str ~.file_name: full file name (with path)
Used to return data from calls to b2_delete_file_version and b2_cancel_large_file.
"""

def __init__(self, file_id, file_name):
def __init__(self, file_id: str, file_name: str):
self.file_id = file_id
self.file_name = file_name

@classmethod
def from_cancel_or_delete_response(cls, response):
return cls(response['fileId'], response['fileName'])

def as_dict(self):
""" represents the object as a dict which looks almost exactly like the raw api output for delete_file_version """
return {'action': 'delete', 'fileId': self.file_id, 'fileName': self.file_name}

def __eq__(self, other):
return (self.file_id == other.file_id and self.file_name == other.file_name)

def __repr__(self):
return '%s(%s, %s)' % (self.__class__.__name__, repr(self.file_id), repr(self.file_name))
11 changes: 4 additions & 7 deletions b2sdk/large_file/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from b2sdk.encryption.setting import EncryptionSetting
from b2sdk.file_lock import FileRetentionSetting, LegalHold
from b2sdk.file_version import FileVersionFactory
from b2sdk.file_version import FileIdAndName
from b2sdk.large_file.part import PartFactory
from b2sdk.large_file.unfinished_large_file import UnfinishedLargeFile

Expand Down Expand Up @@ -95,7 +95,7 @@ def start_large_file(
:param str,None content_type: the MIME type, or ``None`` to accept the default based on file extension of the B2 file name
:param dict,None file_info: a file info to store with the file or ``None`` to not store anything
:param b2sdk.v1.EncryptionSetting encryption: encryption settings (``None`` if unknown)
:param bool legal_hold: legal hold setting
:param b2sdk.v1.LegalHold legal_hold: legal hold setting
:param b2sdk.v1.FileRetentionSetting file_retention: file retention setting
"""
return UnfinishedLargeFile(
Expand All @@ -111,12 +111,9 @@ def start_large_file(
)

# delete/cancel
def cancel_large_file(self, file_id):
def cancel_large_file(self, file_id: str) -> FileIdAndName:
"""
Cancel a large file upload.

:param str file_id: a file ID
:rtype: None
"""
response = self.services.session.cancel_large_file(file_id)
return FileVersionFactory.from_cancel_large_file_response(response)
return FileIdAndName.from_cancel_or_delete_response(response)
1 change: 1 addition & 0 deletions b2sdk/sync/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ def __init__(self, bucket_name, folder_name, api):
self.bucket_name = bucket_name
self.folder_name = folder_name
self.bucket = api.get_bucket_by_name(bucket_name)
self.api = api
self.prefix = '' if self.folder_name == '' else self.folder_name + '/'

def all_files(self, reporter, policies_manager=DEFAULT_SCAN_MANAGER):
Expand Down
2 changes: 1 addition & 1 deletion b2sdk/transfer/emerge/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def execute_plan(self, emerge_plan):

# Finish the large file
response = self.services.session.finish_large_file(file_id, part_sha1_array)
return FileVersionFactory.from_api_response(response)
return FileVersionFactory.from_api_response(self.services.api, response)

def _execute_step(self, execution_step):
semaphore = self._semaphore
Expand Down
2 changes: 1 addition & 1 deletion b2sdk/transfer/outbound/copy_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def _copy_small_file(
legal_hold=legal_hold,
file_retention=file_retention,
)
file_info = FileVersionFactory.from_api_response(response)
file_info = FileVersionFactory.from_api_response(self.services.api, response)
if progress_listener is not None:
progress_listener.bytes_completed(file_info.size)

Expand Down
2 changes: 1 addition & 1 deletion b2sdk/transfer/outbound/upload_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def _upload_small_file(
content_sha1 = input_stream.hash
assert content_sha1 == response[
'contentSha1'], '%s != %s' % (content_sha1, response['contentSha1'])
return FileVersionFactory.from_api_response(response)
return FileVersionFactory.from_api_response(self.services.api, response)

except B2Error as e:
if not e.should_retry_upload():
Expand Down
6 changes: 6 additions & 0 deletions b2sdk/v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@

from b2sdk import _v2 as v2
from .bucket import Bucket, BucketFactory
from .file_version import FileVersionInfo, file_version_info_from_id_and_name
from .session import B2Session


# override to use legacy no-request method of creating a bucket from bucket_id and retain `check_bucket_restrictions`
# public API method
# and to use v1.Bucket
# and to retain cancel_large_file return type
class B2Api(v2.B2Api):
SESSION_CLASS = staticmethod(B2Session)
BUCKET_FACTORY_CLASS = staticmethod(BucketFactory)
Expand All @@ -42,3 +44,7 @@ def check_bucket_restrictions(self, bucket_name):
:raises b2sdk.v1.exception.RestrictedBucket: if the account is not allowed to use this bucket
"""
self.check_bucket_name_restrictions(bucket_name)

def cancel_large_file(self, file_id: str) -> FileVersionInfo:
file_id_and_name = super().cancel_large_file(file_id)
return file_version_info_from_id_and_name(file_id_and_name)
Loading