Skip to content

Commit c0507d1

Browse files
Merge pull request #441 from Backblaze/header-args-v3
Header args v3
2 parents 52b5199 + 73a3fea commit c0507d1

30 files changed

+728
-150
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Added
1212
* Add `*_PART_SIZE`, `BUCKET_NAME_*`, `STDOUT_FILEPATH` constants
1313
* Add `points_to_fifo`, `points_to_stdout` functions
14+
* Add `expires`, `content_disposition`, `content_encoding`, `content_language` arguments to various `Bucket` methods
1415

1516
### Changed
1617
* Mark `TempDir` as deprecated in favor of `tempfile.TemporaryDirectory`

b2sdk/_v3/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from b2sdk.file_version import FileVersionFactory
8585
from b2sdk.large_file.part import Part
8686
from b2sdk.large_file.unfinished_large_file import UnfinishedLargeFile
87+
from b2sdk.large_file.services import LargeFileServices
8788
from b2sdk.utils.range_ import Range
8889

8990
# file lock

b2sdk/api.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Services:
7171
UPLOAD_MANAGER_CLASS = staticmethod(UploadManager)
7272
COPY_MANAGER_CLASS = staticmethod(CopyManager)
7373
DOWNLOAD_MANAGER_CLASS = staticmethod(DownloadManager)
74+
LARGE_FILE_SERVICES_CLASS = staticmethod(LargeFileServices)
7475

7576
def __init__(
7677
self,
@@ -95,7 +96,7 @@ def __init__(
9596
"""
9697
self.api = api
9798
self.session = api.session
98-
self.large_file = LargeFileServices(self)
99+
self.large_file = self.LARGE_FILE_SERVICES_CLASS(self)
99100
self.upload_manager = self.UPLOAD_MANAGER_CLASS(
100101
services=self, max_workers=max_upload_workers
101102
)

b2sdk/bucket.py

+241-14
Large diffs are not rendered by default.

b2sdk/file_version.py

+59-23
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
######################################################################
1010
from __future__ import annotations
1111

12+
import datetime as dt
1213
import re
1314
from copy import deepcopy
1415
from typing import TYPE_CHECKING, Any
@@ -19,6 +20,7 @@
1920
from .progress import AbstractProgressListener
2021
from .replication.types import ReplicationStatus
2122
from .utils import Sha1HexDigest, b2_url_decode
23+
from .utils.http_date import parse_http_date
2224
from .utils.range_ import Range
2325

2426
if TYPE_CHECKING:
@@ -49,7 +51,6 @@ class BaseFileVersion:
4951
'file_retention',
5052
'mod_time_millis',
5153
'replication_status',
52-
'cache_control',
5354
]
5455
_TYPE_MATCHER = re.compile('[a-z0-9]+_[a-z0-9]+_f([0-9]).*')
5556
_FILE_TYPE = {
@@ -73,7 +74,6 @@ def __init__(
7374
file_retention: FileRetentionSetting = NO_RETENTION_FILE_SETTING,
7475
legal_hold: LegalHold = LegalHold.UNSET,
7576
replication_status: ReplicationStatus | None = None,
76-
cache_control: str | None = None,
7777
):
7878
self.api = api
7979
self.id_ = id_
@@ -87,7 +87,6 @@ def __init__(
8787
self.file_retention = file_retention
8888
self.legal_hold = legal_hold
8989
self.replication_status = replication_status
90-
self.cache_control = cache_control
9190

9291
if SRC_LAST_MODIFIED_MILLIS in self.file_info:
9392
self.mod_time_millis = int(self.file_info[SRC_LAST_MODIFIED_MILLIS])
@@ -128,7 +127,6 @@ def _get_args_for_clone(self):
128127
'file_retention': self.file_retention,
129128
'legal_hold': self.legal_hold,
130129
'replication_status': self.replication_status,
131-
'cache_control': self.cache_control,
132130
} # yapf: disable
133131

134132
def as_dict(self):
@@ -140,7 +138,6 @@ def as_dict(self):
140138
'serverSideEncryption': self.server_side_encryption.as_dict(),
141139
'legalHold': self.legal_hold.value,
142140
'fileRetention': self.file_retention.as_dict(),
143-
'cacheControl': self.cache_control,
144141
}
145142

146143
if self.size is not None:
@@ -259,7 +256,6 @@ def __init__(
259256
file_retention: FileRetentionSetting = NO_RETENTION_FILE_SETTING,
260257
legal_hold: LegalHold = LegalHold.UNSET,
261258
replication_status: ReplicationStatus | None = None,
262-
cache_control: str | None = None,
263259
):
264260
self.account_id = account_id
265261
self.bucket_id = bucket_id
@@ -279,9 +275,36 @@ def __init__(
279275
file_retention=file_retention,
280276
legal_hold=legal_hold,
281277
replication_status=replication_status,
282-
cache_control=cache_control,
283278
)
284279

280+
@property
281+
def cache_control(self) -> str | None:
282+
return self.file_info.get('b2-cache-control')
283+
284+
@property
285+
def expires(self) -> str | None:
286+
return self.file_info.get('b2-expires')
287+
288+
def expires_parsed(self) -> dt.datetime | None:
289+
"""Return the expiration date as a datetime object, or None if there is no expiration date.
290+
Raise ValueError if `expires` property is not a valid HTTP-date."""
291+
292+
if self.expires is None:
293+
return None
294+
return parse_http_date(self.expires)
295+
296+
@property
297+
def content_disposition(self) -> str | None:
298+
return self.file_info.get('b2-content-disposition')
299+
300+
@property
301+
def content_encoding(self) -> str | None:
302+
return self.file_info.get('b2-content-encoding')
303+
304+
@property
305+
def content_language(self) -> str | None:
306+
return self.file_info.get('b2-content-language')
307+
285308
def _get_args_for_clone(self):
286309
args = super()._get_args_for_clone()
287310
args.update(
@@ -352,7 +375,6 @@ def _get_upload_headers(self) -> bytes:
352375
server_side_encryption=sse,
353376
file_retention=self.file_retention,
354377
legal_hold=self.legal_hold,
355-
cache_control=self.cache_control,
356378
)
357379

358380
headers_str = ''.join(
@@ -381,8 +403,8 @@ class DownloadVersion(BaseFileVersion):
381403
'content_disposition',
382404
'content_length',
383405
'content_language',
384-
'_expires',
385-
'_cache_control',
406+
'expires',
407+
'cache_control',
386408
'content_encoding',
387409
]
388410

@@ -412,8 +434,8 @@ def __init__(
412434
self.content_disposition = content_disposition
413435
self.content_length = content_length
414436
self.content_language = content_language
415-
self._expires = expires # TODO: parse the string representation of this timestamp to datetime in DownloadVersionFactory
416-
self._cache_control = cache_control # TODO: parse the string representation of this mapping to dict in DownloadVersionFactory
437+
self.expires = expires
438+
self.cache_control = cache_control
417439
self.content_encoding = content_encoding
418440

419441
super().__init__(
@@ -429,9 +451,30 @@ def __init__(
429451
file_retention=file_retention,
430452
legal_hold=legal_hold,
431453
replication_status=replication_status,
432-
cache_control=cache_control,
433454
)
434455

456+
def expires_parsed(self) -> dt.datetime | None:
457+
"""Return the expiration date as a datetime object, or None if there is no expiration date.
458+
Raise ValueError if `expires` property is not a valid HTTP-date."""
459+
460+
if self.expires is None:
461+
return None
462+
return parse_http_date(self.expires)
463+
464+
def as_dict(self) -> dict:
465+
result = super().as_dict()
466+
if self.cache_control is not None:
467+
result['cacheControl'] = self.cache_control
468+
if self.expires is not None:
469+
result['expires'] = self.expires
470+
if self.content_disposition is not None:
471+
result['contentDisposition'] = self.content_disposition
472+
if self.content_encoding is not None:
473+
result['contentEncoding'] = self.content_encoding
474+
if self.content_language is not None:
475+
result['contentLanguage'] = self.content_language
476+
return result
477+
435478
def _get_args_for_clone(self):
436479
args = super()._get_args_for_clone()
437480
args.update(
@@ -440,8 +483,8 @@ def _get_args_for_clone(self):
440483
'content_disposition': self.content_disposition,
441484
'content_length': self.content_length,
442485
'content_language': self.content_language,
443-
'expires': self._expires,
444-
'cache_control': self._cache_control,
486+
'expires': self.expires,
487+
'cache_control': self.cache_control,
445488
'content_encoding': self.content_encoding,
446489
}
447490
)
@@ -516,7 +559,6 @@ def from_api_response(self, file_version_dict, force_action=None):
516559
replication_status_value = file_version_dict.get('replicationStatus')
517560
replication_status = replication_status_value and ReplicationStatus[
518561
replication_status_value.upper()]
519-
cache_control = file_version_dict.get('cacheControl')
520562

521563
return self.FILE_VERSION_CLASS(
522564
self.api,
@@ -535,7 +577,6 @@ def from_api_response(self, file_version_dict, force_action=None):
535577
file_retention,
536578
legal_hold,
537579
replication_status,
538-
cache_control,
539580
)
540581

541582

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

578-
if 'Cache-Control' in headers:
579-
cache_control = b2_url_decode(headers['Cache-Control'])
580-
else:
581-
cache_control = None
582-
583619
return DownloadVersion(
584620
api=self.api,
585621
id_=headers['x-bz-file-id'],
@@ -595,7 +631,7 @@ def from_response_headers(self, headers):
595631
content_length=content_length,
596632
content_language=headers.get('Content-Language'),
597633
expires=headers.get('Expires'),
598-
cache_control=cache_control,
634+
cache_control=headers.get('Cache-Control'),
599635
content_encoding=headers.get('Content-Encoding'),
600636
file_retention=FileRetentionSetting.from_response_headers(headers),
601637
legal_hold=LegalHold.from_response_headers(headers),

b2sdk/large_file/services.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818

1919
class LargeFileServices:
20+
21+
UNFINISHED_LARGE_FILE_CLASS = staticmethod(UnfinishedLargeFile)
22+
2023
def __init__(self, services):
2124
self.services = services
2225

@@ -57,7 +60,7 @@ def list_unfinished_large_files(
5760
bucket_id, start_file_id, batch_size, prefix
5861
)
5962
for file_dict in batch['files']:
60-
yield UnfinishedLargeFile(file_dict)
63+
yield self.UNFINISHED_LARGE_FILE_CLASS(file_dict)
6164
start_file_id = batch.get('nextFileId')
6265
if start_file_id is None:
6366
break
@@ -86,7 +89,6 @@ def start_large_file(
8689
encryption: EncryptionSetting | None = None,
8790
file_retention: FileRetentionSetting | None = None,
8891
legal_hold: LegalHold | None = None,
89-
cache_control: str | None = None,
9092
):
9193
"""
9294
Start a large file transfer.
@@ -97,9 +99,8 @@ def start_large_file(
9799
:param b2sdk.v2.EncryptionSetting encryption: encryption settings (``None`` if unknown)
98100
:param b2sdk.v2.FileRetentionSetting file_retention: file retention setting
99101
:param b2sdk.v2.LegalHold legal_hold: legal hold setting
100-
: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'.
101102
"""
102-
return UnfinishedLargeFile(
103+
return self.UNFINISHED_LARGE_FILE_CLASS(
103104
self.services.session.start_large_file(
104105
bucket_id,
105106
file_name,
@@ -108,7 +109,6 @@ def start_large_file(
108109
server_side_encryption=encryption,
109110
file_retention=file_retention,
110111
legal_hold=legal_hold,
111-
cache_control=cache_control,
112112
)
113113
)
114114

b2sdk/large_file/unfinished_large_file.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
######################################################################
1010
from __future__ import annotations
1111

12+
import datetime as dt
13+
1214
from b2sdk.encryption.setting import EncryptionSettingFactory
1315
from b2sdk.file_lock import FileRetentionSetting, LegalHold
16+
from b2sdk.utils.http_date import parse_http_date
1417

1518

1619
class UnfinishedLargeFile:
@@ -38,7 +41,34 @@ def __init__(self, file_dict):
3841
self.encryption = EncryptionSettingFactory.from_file_version_dict(file_dict)
3942
self.file_retention = FileRetentionSetting.from_file_version_dict(file_dict)
4043
self.legal_hold = LegalHold.from_file_version_dict(file_dict)
41-
self.cache_control = file_dict.get('cacheControl')
44+
45+
@property
46+
def cache_control(self) -> str | None:
47+
return (self.file_info or {}).get('b2-cache-control')
48+
49+
@property
50+
def expires(self) -> str | None:
51+
return (self.file_info or {}).get('b2-expires')
52+
53+
def expires_parsed(self) -> dt.datetime | None:
54+
"""Return the expiration date as a datetime object, or None if there is no expiration date.
55+
Raise ValueError if `expires` property is not a valid HTTP-date."""
56+
57+
if self.expires is None:
58+
return None
59+
return parse_http_date(self.expires)
60+
61+
@property
62+
def content_disposition(self) -> str | None:
63+
return (self.file_info or {}).get('b2-content-disposition')
64+
65+
@property
66+
def content_encoding(self) -> str | None:
67+
return (self.file_info or {}).get('b2-content-encoding')
68+
69+
@property
70+
def content_language(self) -> str | None:
71+
return (self.file_info or {}).get('b2-content-language')
4272

4373
def __repr__(self):
4474
return f'<{self.__class__.__name__} {self.bucket_id} {self.file_name}>'

0 commit comments

Comments
 (0)