Skip to content

Commit

Permalink
Merge pull request #441 from Backblaze/header-args-v3
Browse files Browse the repository at this point in the history
Header args v3
  • Loading branch information
mpnowacki-reef authored Nov 17, 2023
2 parents 52b5199 + 73a3fea commit c0507d1
Show file tree
Hide file tree
Showing 30 changed files with 728 additions and 150 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
* Add `*_PART_SIZE`, `BUCKET_NAME_*`, `STDOUT_FILEPATH` constants
* Add `points_to_fifo`, `points_to_stdout` functions
* Add `expires`, `content_disposition`, `content_encoding`, `content_language` arguments to various `Bucket` methods

### Changed
* Mark `TempDir` as deprecated in favor of `tempfile.TemporaryDirectory`
Expand Down
1 change: 1 addition & 0 deletions b2sdk/_v3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from b2sdk.file_version import FileVersionFactory
from b2sdk.large_file.part import Part
from b2sdk.large_file.unfinished_large_file import UnfinishedLargeFile
from b2sdk.large_file.services import LargeFileServices
from b2sdk.utils.range_ import Range

# file lock
Expand Down
3 changes: 2 additions & 1 deletion b2sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Services:
UPLOAD_MANAGER_CLASS = staticmethod(UploadManager)
COPY_MANAGER_CLASS = staticmethod(CopyManager)
DOWNLOAD_MANAGER_CLASS = staticmethod(DownloadManager)
LARGE_FILE_SERVICES_CLASS = staticmethod(LargeFileServices)

def __init__(
self,
Expand All @@ -95,7 +96,7 @@ def __init__(
"""
self.api = api
self.session = api.session
self.large_file = LargeFileServices(self)
self.large_file = self.LARGE_FILE_SERVICES_CLASS(self)
self.upload_manager = self.UPLOAD_MANAGER_CLASS(
services=self, max_workers=max_upload_workers
)
Expand Down
255 changes: 241 additions & 14 deletions b2sdk/bucket.py

Large diffs are not rendered by default.

82 changes: 59 additions & 23 deletions b2sdk/file_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
######################################################################
from __future__ import annotations

import datetime as dt
import re
from copy import deepcopy
from typing import TYPE_CHECKING, Any
Expand All @@ -19,6 +20,7 @@
from .progress import AbstractProgressListener
from .replication.types import ReplicationStatus
from .utils import Sha1HexDigest, b2_url_decode
from .utils.http_date import parse_http_date
from .utils.range_ import Range

if TYPE_CHECKING:
Expand Down Expand Up @@ -49,7 +51,6 @@ class BaseFileVersion:
'file_retention',
'mod_time_millis',
'replication_status',
'cache_control',
]
_TYPE_MATCHER = re.compile('[a-z0-9]+_[a-z0-9]+_f([0-9]).*')
_FILE_TYPE = {
Expand All @@ -73,7 +74,6 @@ def __init__(
file_retention: FileRetentionSetting = NO_RETENTION_FILE_SETTING,
legal_hold: LegalHold = LegalHold.UNSET,
replication_status: ReplicationStatus | None = None,
cache_control: str | None = None,
):
self.api = api
self.id_ = id_
Expand All @@ -87,7 +87,6 @@ def __init__(
self.file_retention = file_retention
self.legal_hold = legal_hold
self.replication_status = replication_status
self.cache_control = cache_control

if SRC_LAST_MODIFIED_MILLIS in self.file_info:
self.mod_time_millis = int(self.file_info[SRC_LAST_MODIFIED_MILLIS])
Expand Down Expand Up @@ -128,7 +127,6 @@ def _get_args_for_clone(self):
'file_retention': self.file_retention,
'legal_hold': self.legal_hold,
'replication_status': self.replication_status,
'cache_control': self.cache_control,
} # yapf: disable

def as_dict(self):
Expand All @@ -140,7 +138,6 @@ def as_dict(self):
'serverSideEncryption': self.server_side_encryption.as_dict(),
'legalHold': self.legal_hold.value,
'fileRetention': self.file_retention.as_dict(),
'cacheControl': self.cache_control,
}

if self.size is not None:
Expand Down Expand Up @@ -259,7 +256,6 @@ def __init__(
file_retention: FileRetentionSetting = NO_RETENTION_FILE_SETTING,
legal_hold: LegalHold = LegalHold.UNSET,
replication_status: ReplicationStatus | None = None,
cache_control: str | None = None,
):
self.account_id = account_id
self.bucket_id = bucket_id
Expand All @@ -279,9 +275,36 @@ def __init__(
file_retention=file_retention,
legal_hold=legal_hold,
replication_status=replication_status,
cache_control=cache_control,
)

@property
def cache_control(self) -> str | None:
return self.file_info.get('b2-cache-control')

@property
def expires(self) -> str | None:
return self.file_info.get('b2-expires')

def expires_parsed(self) -> dt.datetime | None:
"""Return the expiration date as a datetime object, or None if there is no expiration date.
Raise ValueError if `expires` property is not a valid HTTP-date."""

if self.expires is None:
return None
return parse_http_date(self.expires)

@property
def content_disposition(self) -> str | None:
return self.file_info.get('b2-content-disposition')

@property
def content_encoding(self) -> str | None:
return self.file_info.get('b2-content-encoding')

@property
def content_language(self) -> str | None:
return self.file_info.get('b2-content-language')

def _get_args_for_clone(self):
args = super()._get_args_for_clone()
args.update(
Expand Down Expand Up @@ -352,7 +375,6 @@ def _get_upload_headers(self) -> bytes:
server_side_encryption=sse,
file_retention=self.file_retention,
legal_hold=self.legal_hold,
cache_control=self.cache_control,
)

headers_str = ''.join(
Expand Down Expand Up @@ -381,8 +403,8 @@ class DownloadVersion(BaseFileVersion):
'content_disposition',
'content_length',
'content_language',
'_expires',
'_cache_control',
'expires',
'cache_control',
'content_encoding',
]

Expand Down Expand Up @@ -412,8 +434,8 @@ def __init__(
self.content_disposition = content_disposition
self.content_length = content_length
self.content_language = content_language
self._expires = expires # TODO: parse the string representation of this timestamp to datetime in DownloadVersionFactory
self._cache_control = cache_control # TODO: parse the string representation of this mapping to dict in DownloadVersionFactory
self.expires = expires
self.cache_control = cache_control
self.content_encoding = content_encoding

super().__init__(
Expand All @@ -429,9 +451,30 @@ def __init__(
file_retention=file_retention,
legal_hold=legal_hold,
replication_status=replication_status,
cache_control=cache_control,
)

def expires_parsed(self) -> dt.datetime | None:
"""Return the expiration date as a datetime object, or None if there is no expiration date.
Raise ValueError if `expires` property is not a valid HTTP-date."""

if self.expires is None:
return None
return parse_http_date(self.expires)

def as_dict(self) -> dict:
result = super().as_dict()
if self.cache_control is not None:
result['cacheControl'] = self.cache_control
if self.expires is not None:
result['expires'] = self.expires
if self.content_disposition is not None:
result['contentDisposition'] = self.content_disposition
if self.content_encoding is not None:
result['contentEncoding'] = self.content_encoding
if self.content_language is not None:
result['contentLanguage'] = self.content_language
return result

def _get_args_for_clone(self):
args = super()._get_args_for_clone()
args.update(
Expand All @@ -440,8 +483,8 @@ def _get_args_for_clone(self):
'content_disposition': self.content_disposition,
'content_length': self.content_length,
'content_language': self.content_language,
'expires': self._expires,
'cache_control': self._cache_control,
'expires': self.expires,
'cache_control': self.cache_control,
'content_encoding': self.content_encoding,
}
)
Expand Down Expand Up @@ -516,7 +559,6 @@ def from_api_response(self, file_version_dict, force_action=None):
replication_status_value = file_version_dict.get('replicationStatus')
replication_status = replication_status_value and ReplicationStatus[
replication_status_value.upper()]
cache_control = file_version_dict.get('cacheControl')

return self.FILE_VERSION_CLASS(
self.api,
Expand All @@ -535,7 +577,6 @@ def from_api_response(self, file_version_dict, force_action=None):
file_retention,
legal_hold,
replication_status,
cache_control,
)


Expand Down Expand Up @@ -575,11 +616,6 @@ def from_response_headers(self, headers):
size = content_length = int(headers['Content-Length'])
range_ = Range(0, max(size - 1, 0))

if 'Cache-Control' in headers:
cache_control = b2_url_decode(headers['Cache-Control'])
else:
cache_control = None

return DownloadVersion(
api=self.api,
id_=headers['x-bz-file-id'],
Expand All @@ -595,7 +631,7 @@ def from_response_headers(self, headers):
content_length=content_length,
content_language=headers.get('Content-Language'),
expires=headers.get('Expires'),
cache_control=cache_control,
cache_control=headers.get('Cache-Control'),
content_encoding=headers.get('Content-Encoding'),
file_retention=FileRetentionSetting.from_response_headers(headers),
legal_hold=LegalHold.from_response_headers(headers),
Expand Down
10 changes: 5 additions & 5 deletions b2sdk/large_file/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@


class LargeFileServices:

UNFINISHED_LARGE_FILE_CLASS = staticmethod(UnfinishedLargeFile)

def __init__(self, services):
self.services = services

Expand Down Expand Up @@ -57,7 +60,7 @@ def list_unfinished_large_files(
bucket_id, start_file_id, batch_size, prefix
)
for file_dict in batch['files']:
yield UnfinishedLargeFile(file_dict)
yield self.UNFINISHED_LARGE_FILE_CLASS(file_dict)
start_file_id = batch.get('nextFileId')
if start_file_id is None:
break
Expand Down Expand Up @@ -86,7 +89,6 @@ def start_large_file(
encryption: EncryptionSetting | None = None,
file_retention: FileRetentionSetting | None = None,
legal_hold: LegalHold | None = None,
cache_control: str | None = None,
):
"""
Start a large file transfer.
Expand All @@ -97,9 +99,8 @@ def start_large_file(
:param b2sdk.v2.EncryptionSetting encryption: encryption settings (``None`` if unknown)
:param b2sdk.v2.FileRetentionSetting file_retention: file retention setting
:param b2sdk.v2.LegalHold legal_hold: legal hold setting
:param str,None cache_control: an optional cache control setting. Syntax based on the section 14.9 of RFC 2616. Example string value: 'public, max-age=86400, s-maxage=3600, no-transform'.
"""
return UnfinishedLargeFile(
return self.UNFINISHED_LARGE_FILE_CLASS(
self.services.session.start_large_file(
bucket_id,
file_name,
Expand All @@ -108,7 +109,6 @@ def start_large_file(
server_side_encryption=encryption,
file_retention=file_retention,
legal_hold=legal_hold,
cache_control=cache_control,
)
)

Expand Down
32 changes: 31 additions & 1 deletion b2sdk/large_file/unfinished_large_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
######################################################################
from __future__ import annotations

import datetime as dt

from b2sdk.encryption.setting import EncryptionSettingFactory
from b2sdk.file_lock import FileRetentionSetting, LegalHold
from b2sdk.utils.http_date import parse_http_date


class UnfinishedLargeFile:
Expand Down Expand Up @@ -38,7 +41,34 @@ def __init__(self, file_dict):
self.encryption = EncryptionSettingFactory.from_file_version_dict(file_dict)
self.file_retention = FileRetentionSetting.from_file_version_dict(file_dict)
self.legal_hold = LegalHold.from_file_version_dict(file_dict)
self.cache_control = file_dict.get('cacheControl')

@property
def cache_control(self) -> str | None:
return (self.file_info or {}).get('b2-cache-control')

@property
def expires(self) -> str | None:
return (self.file_info or {}).get('b2-expires')

def expires_parsed(self) -> dt.datetime | None:
"""Return the expiration date as a datetime object, or None if there is no expiration date.
Raise ValueError if `expires` property is not a valid HTTP-date."""

if self.expires is None:
return None
return parse_http_date(self.expires)

@property
def content_disposition(self) -> str | None:
return (self.file_info or {}).get('b2-content-disposition')

@property
def content_encoding(self) -> str | None:
return (self.file_info or {}).get('b2-content-encoding')

@property
def content_language(self) -> str | None:
return (self.file_info or {}).get('b2-content-language')

def __repr__(self):
return f'<{self.__class__.__name__} {self.bucket_id} {self.file_name}>'
Expand Down
Loading

0 comments on commit c0507d1

Please sign in to comment.