Skip to content

Commit

Permalink
Merge pull request #281 from reef-technologies/file_version_methods
Browse files Browse the repository at this point in the history
File version manipulation methods (.delete etc.) methods
  • Loading branch information
mpnowacki-reef authored Aug 6, 2021
2 parents 60efe54 + 8d50281 commit 0f7d8be
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 40 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
* The `importlib-metadata` requirement is less strictly bound now (just >=3.3.0 for python > 3.5).
* `B2Api` `update_file_legal_hold` and `update_file_retention_setting` now return the set values

### Added
* `BucketIdNotFound` thrown based on B2 cloud response
* `_clone` method to `FileVersion` and `DownloadVersion`
* `delete`, `update_legal_hold`, `update_retention` and `download` methods added to `FileVersion`

### Fixed
* FileSimulator returns special file info headers properly

## [1.11.0] - 2021-06-24

Expand Down
26 changes: 15 additions & 11 deletions b2sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,24 +246,28 @@ def update_file_retention(
file_name: str,
file_retention: FileRetentionSetting,
bypass_governance: bool = False,
):
self.session.update_file_retention(
file_id,
file_name,
file_retention,
bypass_governance,
) -> FileRetentionSetting:
return FileRetentionSetting.from_server_response(
self.session.update_file_retention(
file_id,
file_name,
file_retention,
bypass_governance,
)
)

def update_file_legal_hold(
self,
file_id: str,
file_name: str,
legal_hold: LegalHold,
):
self.session.update_file_legal_hold(
file_id,
file_name,
legal_hold,
) -> LegalHold:
return LegalHold.from_server_response(
self.session.update_file_legal_hold(
file_id,
file_name,
legal_hold,
)
)

def get_bucket_by_id(self, bucket_id: str) -> Bucket:
Expand Down
8 changes: 8 additions & 0 deletions b2sdk/file_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def from_file_retention_value_dict(
file_retention_value_dict['retainUntilTimestamp'],
)

@classmethod
def from_server_response(cls, server_response: dict) -> 'FileRetentionSetting':
return cls.from_file_retention_value_dict(server_response['fileRetention'])

@classmethod
def from_response_headers(cls, headers) -> 'FileRetentionSetting':
retention_mode_header = 'X-Bz-File-Retention-Mode'
Expand Down Expand Up @@ -214,6 +218,10 @@ def from_file_version_dict(cls, file_version_dict: dict) -> 'LegalHold':
return cls.UNKNOWN
return cls.from_string_or_none(file_version_dict['legalHold']['value'])

@classmethod
def from_server_response(cls, server_response: dict) -> 'LegalHold':
return cls.from_string_or_none(server_response['legalHold'])

@classmethod
def from_string_or_none(cls, string: Optional[str]) -> 'LegalHold':
return cls(string)
Expand Down
87 changes: 85 additions & 2 deletions b2sdk/file_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@
#
######################################################################

from typing import Dict, Optional, Union, TYPE_CHECKING
from abc import ABC, abstractmethod
from typing import Dict, Optional, Union, Tuple, TYPE_CHECKING

from .encryption.setting import EncryptionSetting, EncryptionSettingFactory
from .http_constants import FILE_INFO_HEADER_PREFIX_LOWER, SRC_LAST_MODIFIED_MILLIS
from .file_lock import FileRetentionSetting, LegalHold, NO_RETENTION_FILE_SETTING
from .progress import AbstractProgressListener
from .utils.range_ import Range

if TYPE_CHECKING:
from .api import B2Api
from .transfer.inbound.downloaded_file import DownloadedFile


class BaseFileVersion:
class BaseFileVersion(ABC):
"""
Base class for representing file metadata in B2 cloud.
Expand Down Expand Up @@ -71,6 +74,13 @@ def __init__(
else:
self.mod_time_millis = self.upload_timestamp

@abstractmethod
def _clone(self, **new_attributes):
"""
Create new instance based on the old one, overriding attributes with :code:`new_attributes`
(only applies to arguments passed to __init__)
"""

def as_dict(self):
""" represents the object as a dict which looks almost exactly like the raw api output for upload/list """
result = {
Expand Down Expand Up @@ -113,6 +123,23 @@ def _all_slots(self):
all_slots.extend(getattr(klass, '__slots__', []))
return all_slots

def delete(self) -> 'FileIdAndName':
return self.api.delete_file_version(self.id_, self.file_name)

def update_legal_hold(self, legal_hold: LegalHold) -> 'BaseFileVersion':
legal_hold = self.api.update_file_legal_hold(self.id_, self.file_name, legal_hold)
return self._clone(legal_hold=legal_hold)

def update_retention(
self,
file_retention: FileRetentionSetting,
bypass_governance: bool = False,
) -> 'BaseFileVersion':
file_retention = self.api.update_file_retention(
self.id_, self.file_name, file_retention, bypass_governance
)
return self._clone(file_retention=file_retention)


class FileVersion(BaseFileVersion):
"""
Expand Down Expand Up @@ -170,6 +197,26 @@ def __init__(
legal_hold=legal_hold,
)

def _clone(self, **new_attributes: Dict[str, object]):
args = {
'api': self.api,
'id_': self.id_,
'file_name': self.file_name,
'size': self.size,
'content_type': self.content_type,
'content_sha1': self.content_sha1,
'file_info': self.file_info,
'upload_timestamp': self.upload_timestamp,
'account_id': self.account_id,
'bucket_id': self.bucket_id,
'action': self.action,
'content_md5': self.content_md5,
'server_side_encryption': self.server_side_encryption,
'file_retention': self.file_retention,
'legal_hold': self.legal_hold,
}
return self.__class__(**{**args, **new_attributes})

def as_dict(self):
result = super().as_dict()
result['accountId'] = self.account_id
Expand All @@ -189,6 +236,19 @@ def get_fresh_state(self) -> 'FileVersion':
"""
return self.api.get_file_info(self.id_)

def download(
self,
progress_listener: Optional[AbstractProgressListener] = None,
range_: Optional[Tuple[int, int]] = None,
encryption: Optional[EncryptionSetting] = None,
) -> 'DownloadedFile':
return self.api.download_file_by_id(
self.id_,
progress_listener=progress_listener,
range_=range_,
encryption=encryption,
)


class DownloadVersion(BaseFileVersion):
"""
Expand Down Expand Up @@ -247,6 +307,29 @@ def __init__(
legal_hold=legal_hold,
)

def _clone(self, **new_attributes: Dict[str, object]):
args = {
'api': self.api,
'id_': self.id_,
'file_name': self.file_name,
'size': self.size,
'content_type': self.content_type,
'content_sha1': self.content_sha1,
'file_info': self.file_info,
'upload_timestamp': self.upload_timestamp,
'server_side_encryption': self.server_side_encryption,
'range_': self.range_,
'content_disposition': self.content_disposition,
'content_length': self.content_length,
'content_language': self.content_language,
'expires': self._expires,
'cache_control': self._cache_control,
'content_encoding': self.content_encoding,
'file_retention': self.file_retention,
'legal_hold': self.legal_hold,
}
return self.__class__(**{**args, **new_attributes})


class FileVersionFactory(object):
"""
Expand Down
15 changes: 12 additions & 3 deletions b2sdk/raw_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ class FileSimulator(object):
"""

CHECK_ENCRYPTION = True
SPECIAL_FILE_INFOS = { # when downloading, these file info keys are translated to specialized headers
'b2-content-disposition': 'Content-Disposition',
'b2-content-language': 'Content-Language',
'b2-expires': 'Expires',
'b2-cache-control': 'Cache-Control',
'b2-content-encoding': 'Content-Encoding',
}

def __init__(
self,
Expand Down Expand Up @@ -221,7 +228,11 @@ def as_download_headers(self, account_auth_token_or_none, range_=None):
}
)
for key, value in self.file_info.items():
headers[FILE_INFO_HEADER_PREFIX + key] = value
key_lower = key.lower()
if key_lower in self.SPECIAL_FILE_INFOS:
headers[self.SPECIAL_FILE_INFOS[key_lower]] = value
else:
headers[FILE_INFO_HEADER_PREFIX + key] = value

if account_auth_token_or_none is not None and self.bucket.is_file_lock_enabled:
not_permitted = []
Expand Down Expand Up @@ -260,8 +271,6 @@ def as_download_headers(self, account_auth_token_or_none, range_=None):
headers['Content-Range'] = 'bytes %d-%d/%d' % (
range_[0], range_[0] + content_length - 1, len(self.data_bytes)
) # yapf: disable
for key, value in self.file_info.items():
headers[FILE_INFO_HEADER_PREFIX + key] = value
return headers

def as_upload_result(self, account_auth_token):
Expand Down
Loading

0 comments on commit 0f7d8be

Please sign in to comment.