Skip to content

Commit a5c2e39

Browse files
committed
[Storage][102] CRC64 content validation - part 5 - datalake (#46034)
1 parent 98afe7b commit a5c2e39

15 files changed

Lines changed: 673 additions & 73 deletions

File tree

sdk/storage/azure-storage-blob/tests/test_block_blob.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
ImmutabilityPolicy,
3434
StandardBlobTier,
3535
)
36-
from azure.storage.blob._shared.policies import StorageContentValidation
36+
from azure.storage.blob._shared.validation import calculate_content_md5
3737

3838

3939
# ------------------------------------------------------------------------------
@@ -555,7 +555,7 @@ def test_upload_blob_from_url_with_source_content_md5(self, **kwargs):
555555
source_blob = self._create_blob(data=b"This is test data to be copied over.")
556556
source_blob_props = source_blob.get_blob_properties()
557557
source_md5 = source_blob_props.content_settings.content_md5
558-
bad_source_md5 = StorageContentValidation.get_content_md5(b"this is bad data")
558+
bad_source_md5 = calculate_content_md5(b"this is bad data")
559559
sas = self.generate_sas(
560560
generate_blob_sas,
561561
account_name=storage_account_name,

sdk/storage/azure-storage-blob/tests/test_block_blob_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ async def test_upload_blob_from_url_with_source_content_md5(self, **kwargs):
580580
source_blob = await self._create_blob(data=b"This is test data to be copied over.")
581581
source_blob_props = await source_blob.get_blob_properties()
582582
source_md5 = source_blob_props.content_settings.content_md5
583-
bad_source_md5 = StorageContentValidation.get_content_md5(b"this is bad data")
583+
bad_source_md5 = calculate_content_md5(b"this is bad data")
584584
sas = self.generate_sas(
585585
generate_blob_sas,
586586
account_name=storage_account_name,

sdk/storage/azure-storage-blob/tests/test_content_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,10 @@ def test_download_blob_large_chunks(self, **kwargs):
465465
blob.upload_blob(data, overwrite=True, max_concurrency=5)
466466

467467
# Act
468-
downloader = blob.download_blob(validate_content='crc64', max_concurrency=3)
468+
downloader = blob.download_blob(validate_content='crc64', max_concurrency=5)
469469
content = downloader.read()
470470

471-
downloader = blob.download_blob(offset=5 * 1024 * 1024, length=25 *1024 * 1024)
471+
downloader = blob.download_blob(offset=5 * 1024 * 1024, length=25 * 1024 * 1024, validate_content='crc64')
472472
partial = downloader.read()
473473

474474
# Assert

sdk/storage/azure-storage-blob/tests/test_content_validation_async.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import pytest
1010
from azure.core.exceptions import ResourceExistsError
11-
from azure.storage.blob import BlobBlock, BlobType
11+
from azure.storage.blob import BlobBlock, BlobType, ContainerClient as SyncContainerClient
1212
from azure.storage.blob.aio import (
1313
BlobClient,
1414
BlobServiceClient,
@@ -46,11 +46,16 @@ async def _setup(self, account_name):
4646
except ResourceExistsError:
4747
pass
4848

49-
# TODO: Figure out how to get this to run automatically
50-
async def _teardown(self):
49+
def teardown_method(self, _):
50+
# Use sync client as teardown_method must be sync
5151
if self.container:
52+
sync_credential = self.get_credential(SyncContainerClient, is_async=False)
53+
sync_container = SyncContainerClient.from_container_url(
54+
self.container.url,
55+
credential=sync_credential)
56+
5257
try:
53-
await self.container.delete_container()
58+
sync_container.delete_container()
5459
except:
5560
pass
5661

@@ -74,6 +79,9 @@ async def test_encryption_blocked_crc64(self, **kwargs):
7479
with pytest.raises(ValueError):
7580
await blob.upload_blob(b'123', validate_content='crc64')
7681

82+
# Needed for teardown
83+
self.container = None
84+
7785
@BlobPreparer()
7886
@pytest.mark.parametrize('a', [BlobType.BLOCKBLOB, BlobType.PAGEBLOB, BlobType.APPENDBLOB]) # a: blob_type
7987
@pytest.mark.parametrize('b', [True, "auto", 'md5', 'crc64']) # b: validate_content
@@ -106,8 +114,6 @@ async def test_upload_blob(self, a, b, **kwargs):
106114
await blob.upload_blob(str_iter, blob_type=a, length=len(str_data_encoded), encoding='utf-8', validate_content=b, overwrite=True, raw_request_hook=assert_method)
107115
assert await (await blob.download_blob()).read() == str_data_encoded
108116

109-
await self._teardown()
110-
111117
@BlobPreparer()
112118
@pytest.mark.parametrize('a', [BlobType.BLOCKBLOB, BlobType.PAGEBLOB, BlobType.APPENDBLOB]) # a: blob_type
113119
@pytest.mark.parametrize('b', [True, 'md5', 'crc64']) # b: validate_content
@@ -143,8 +149,6 @@ async def test_upload_blob_chunks(self, a, b, **kwargs):
143149
await blob.upload_blob(str_iter, blob_type=a, length=len(str_data_encoded), encoding='utf-8', validate_content=b, overwrite=True, raw_request_hook=assert_method)
144150
assert await (await blob.download_blob()).read() == str_data_encoded
145151

146-
await self._teardown()
147-
148152
@BlobPreparer()
149153
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
150154
@GenericTestProxyParametrize1()
@@ -169,7 +173,6 @@ async def test_upload_blob_substream(self, a, **kwargs):
169173
# Assert
170174
content = await blob.download_blob()
171175
assert await content.read() == data
172-
await self._teardown()
173176

174177
@BlobPreparer()
175178
@pytest.mark.parametrize('a', [True, 'auto', 'md5', 'crc64']) # a: validate_content
@@ -200,7 +203,6 @@ def generator():
200203
# Assert
201204
content = await blob.download_blob()
202205
assert await content.read() == data1 + data2.encode('utf-8-sig') + data1
203-
await self._teardown()
204206

205207
@BlobPreparer()
206208
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -221,7 +223,6 @@ async def test_stage_block_streaming(self, a, **kwargs):
221223
# Assert
222224
result = await blob.download_blob()
223225
assert await result.read() == content
224-
await self._teardown()
225226

226227
@BlobPreparer()
227228
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -274,7 +275,6 @@ def generator():
274275
# Assert
275276
content = await blob.download_blob()
276277
assert await content.readall() == data1 + data2.encode('utf-16') + data1
277-
await self._teardown()
278278

279279
@BlobPreparer()
280280
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -294,7 +294,6 @@ async def test_append_block_streaming(self, a, **kwargs):
294294

295295
result = await blob.download_blob()
296296
assert await result.read() == content
297-
await self._teardown()
298297

299298
@BlobPreparer()
300299
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -317,7 +316,6 @@ async def test_append_block_streaming_large(self, a, **kwargs):
317316

318317
result = await blob.download_blob()
319318
assert await result.read() == data1 + data2 + data3
320-
await self._teardown()
321319

322320
@BlobPreparer()
323321
@pytest.mark.parametrize('a', [True, 'auto', 'md5', 'crc64']) # a: validate_content
@@ -341,7 +339,6 @@ async def test_upload_page(self, a, **kwargs):
341339
# Assert
342340
content = await blob.download_blob(offset=0, length=len(data1) + len(data2_encoded))
343341
assert await content.read() == data1 + data2_encoded
344-
await self._teardown()
345342

346343
@BlobPreparer()
347344
@pytest.mark.parametrize('a', [True, 'auto', 'md5', 'crc64']) # a: validate_content
@@ -368,7 +365,6 @@ async def test_download_blob(self, a, **kwargs):
368365
# Assert
369366
assert content == data
370367
assert stream.read() == data
371-
await self._teardown()
372368

373369
@BlobPreparer()
374370
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -403,7 +399,6 @@ async def test_download_blob_chunks(self, a, **kwargs):
403399
assert content == data
404400
assert stream.read() == data
405401
assert read_content == data
406-
await self._teardown()
407402

408403
@BlobPreparer()
409404
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -432,7 +427,6 @@ async def test_download_blob_chunks_partial(self, a, **kwargs):
432427
# Assert
433428
assert content == data[10:1010]
434429
assert stream.read() == data[10:1010]
435-
await self._teardown()
436430

437431
@BlobPreparer()
438432
@pytest.mark.live_test_only
@@ -447,16 +441,15 @@ async def test_download_blob_large_chunks(self, **kwargs):
447441
await blob.upload_blob(data, overwrite=True, max_concurrency=5)
448442

449443
# Act
450-
downloader = await blob.download_blob(validate_content='crc64', max_concurrency=3)
444+
downloader = await blob.download_blob(validate_content='crc64', max_concurrency=5)
451445
content = await downloader.read()
452446

453-
downloader = await blob.download_blob(offset=5 * 1024 * 1024, length=25 * 1024 * 1024)
447+
downloader = await blob.download_blob(offset=5 * 1024 * 1024, length=25 * 1024 * 1024, validate_content='crc64')
454448
partial = await downloader.read()
455449

456450
# Assert
457451
assert content == data
458452
assert partial == data[5 * 1024 * 1024: 30 * 1024 * 1024]
459-
await self._teardown()
460453

461454
@BlobPreparer()
462455
@pytest.mark.parametrize('a', [True, 'md5', 'crc64']) # a: validate_content
@@ -488,4 +481,3 @@ async def test_download_blob_chars(self, a, **kwargs):
488481

489482
result += await stream.readall()
490483
assert result == data
491-
await self._teardown()

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -427,15 +427,11 @@ def upload_data(
427427
If a date is passed in without timezone info, it is assumed to be UTC.
428428
Specify this header to perform the operation only if
429429
the resource has not been modified since the specified date/time.
430-
:keyword bool validate_content:
431-
If true, calculates an MD5 hash for each chunk of the file. The storage
432-
service checks the hash of the content that has arrived with the hash
433-
that was sent. This is primarily valuable for detecting bitflips on
434-
the wire if using http instead of https, as https (the default), will
435-
already validate. Note that this MD5 hash is not stored with the
436-
blob. Also note that if enabled, the memory-efficient upload algorithm
437-
will not be used because computing the MD5 hash requires buffering
438-
entire blocks, and doing so defeats the purpose of the memory-efficient algorithm.
430+
:keyword validate_content:
431+
Enables checksum validation for the transfer. Any checksum calculated is NOT stored with the blob.
432+
Choose "auto" (let the SDK choose the best algorithm), "crc64", or "md5". The use of bool is deprecated.
433+
NOTE: The use of "auto" or "crc64" requires the `azure-storage-extensions` package to be installed.
434+
:paramtype validate_content: Union[bool, Literal['auto', 'crc64', 'md5']]
439435
:keyword str etag:
440436
An ETag value, or the wildcard character (*). Used to check if the resource has changed,
441437
and act according to the condition specified by the `match_condition` parameter.
@@ -497,13 +493,11 @@ def append_data(
497493
:type length: int or None
498494
:keyword bool flush:
499495
If true, will commit the data after it is appended.
500-
:keyword bool validate_content:
501-
If true, calculates an MD5 hash of the block content. The storage
502-
service checks the hash of the content that has arrived
503-
with the hash that was sent. This is primarily valuable for detecting
504-
bitflips on the wire if using http instead of https as https (the default)
505-
will already validate. Note that this MD5 hash is not stored with the
506-
file.
496+
:keyword validate_content:
497+
Enables checksum validation for the transfer. Any checksum calculated is NOT stored with the blob.
498+
Choose "auto" (let the SDK choose the best algorithm), "crc64", or "md5". The use of bool is deprecated.
499+
NOTE: The use of "auto" or "crc64" requires the `azure-storage-extensions` package to be installed.
500+
:paramtype validate_content: Union[bool, Literal['auto', 'crc64', 'md5']]
507501
:keyword lease_action:
508502
Used to perform lease operations along with appending data.
509503
@@ -714,6 +708,11 @@ def download_file(
714708
:paramtype progress_hook: ~typing.Callable[[int, int], None]
715709
:keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be
716710
decompressed automatically before being returned. Default value is True.
711+
:keyword validate_content:
712+
Enables checksum validation for the transfer. Any checksum calculated is NOT stored with the blob.
713+
Choose "auto" (let the SDK choose the best algorithm), "crc64", or "md5". The use of bool is deprecated.
714+
NOTE: The use of "auto" or "crc64" requires the `azure-storage-extensions` package to be installed.
715+
:paramtype validate_content: Union[bool, Literal['auto', 'crc64', 'md5']]
717716
:keyword int timeout:
718717
Sets the server-side timeout for the operation in seconds. For more details see
719718
https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations.

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ class DataLakeFileClient(PathClient):
149149
if_unmodified_since: Optional[datetime] = None,
150150
etag: Optional[str] = None,
151151
match_condition: Optional[MatchConditions] = None,
152-
validate_content: Optional[bool] = None,
152+
validate_content: Optional[Union[bool, Literal['auto', 'crc64', 'md5']]] = None,
153153
cpk: Optional[CustomerProvidedEncryptionKey] = None,
154154
max_concurrency: Optional[int] = None,
155155
chunk_size: Optional[int] = None,
@@ -166,7 +166,7 @@ class DataLakeFileClient(PathClient):
166166
length: Optional[int] = None,
167167
*,
168168
flush: Optional[bool] = None,
169-
validate_content: Optional[bool] = None,
169+
validate_content: Optional[Union[bool, Literal['auto', 'crc64', 'md5']]] = None,
170170
lease_action: Optional[Literal["acquire", "auto-renew", "release", "acquire-release"]] = None,
171171
lease_duration: int = -1,
172172
lease: Optional[Union[DataLakeLeaseClient, str]] = None,
@@ -207,6 +207,7 @@ class DataLakeFileClient(PathClient):
207207
cpk: Optional[CustomerProvidedEncryptionKey] = None,
208208
max_concurrency: Optional[int] = None,
209209
progress_hook: Optional[Callable[[int, int], None]] = None,
210+
validate_content: Optional[Union[bool, Literal['auto', 'crc64', 'md5']]] = None,
210211
timeout: Optional[int] = None,
211212
**kwargs: Any
212213
) -> StorageStreamDownloader: ...

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client_helpers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ._shared.response_handlers import return_response_headers
2525
from ._shared.uploads import IterStreamer
2626
from ._shared.uploads_async import AsyncIterStreamer
27+
from ._shared.validation import parse_validation_option
2728

2829
if TYPE_CHECKING:
2930
from ._generated.operations import PathOperations
@@ -47,14 +48,16 @@ def _append_data_options(
4748
if isinstance(data, bytes):
4849
data = data[:length]
4950

51+
validate_content = parse_validation_option(kwargs.pop('validate_content', None))
52+
5053
cpk_info = get_cpk_info(scheme, kwargs)
5154
kwargs.update(get_lease_action_properties(kwargs))
5255

5356
options = {
5457
'body': data,
5558
'position': offset,
5659
'content_length': length,
57-
'validate_content': kwargs.pop('validate_content', False),
60+
'validate_content': validate_content,
5861
'cpk_info': cpk_info,
5962
'timeout': kwargs.pop('timeout', None),
6063
'cls': return_response_headers
@@ -122,7 +125,7 @@ def _upload_options(
122125
else:
123126
raise TypeError(f"Unsupported data type: {type(data)}")
124127

125-
validate_content = kwargs.pop('validate_content', False)
128+
validate_content = parse_validation_option(kwargs.pop('validate_content', None))
126129
content_settings = kwargs.pop('content_settings', None)
127130
metadata = kwargs.pop('metadata', None)
128131
max_concurrency = kwargs.pop('max_concurrency', None)

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client_async.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@
3636
from .parser import DEVSTORE_ACCOUNT_KEY, _get_development_storage_endpoint
3737
from .policies import (
3838
QueueMessagePolicy,
39-
StorageContentValidation,
4039
StorageHeadersPolicy,
4140
StorageHosts,
4241
StorageRequestHook,
4342
)
44-
from .policies_async import AsyncStorageBearerTokenCredentialPolicy, AsyncStorageResponseHook
43+
from .policies_async import AsyncStorageBearerTokenCredentialPolicy, AsyncContentValidationPolicy, AsyncStorageResponseHook
4544
from .response_handlers import PartialBatchErrorException, process_storage_error
4645
from .._shared_access_signature import _is_credential_sastoken
4746

@@ -130,7 +129,7 @@ def _create_pipeline(
130129
QueueMessagePolicy(),
131130
config.proxy_policy,
132131
config.user_agent_policy,
133-
StorageContentValidation(),
132+
AsyncContentValidationPolicy(),
134133
ContentDecodePolicy(response_encoding="utf-8"),
135134
AsyncRedirectPolicy(**kwargs),
136135
StorageHosts(hosts=hosts, **kwargs),

0 commit comments

Comments
 (0)