diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py index 0d0b53475e62..1e3f7ea3d6ca 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.py @@ -732,6 +732,8 @@ def download_blob( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the total size of the download. :paramtype progress_hook: Callable[[int, int], None] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.pyi b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.pyi index e55fc01aec3a..4b50f447948e 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.pyi +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_blob_client.pyi @@ -210,6 +210,7 @@ class BlobClient(StorageAccountHostsMixin, StorageEncryptionMixin): max_concurrency: int = 1, encoding: str, progress_hook: Optional[Callable[[int, int], None]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader[str]: ... @@ -231,6 +232,7 @@ class BlobClient(StorageAccountHostsMixin, StorageEncryptionMixin): max_concurrency: int = 1, encoding: None = None, progress_hook: Optional[Callable[[int, int], None]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader[bytes]: ... @@ -252,6 +254,7 @@ class BlobClient(StorageAccountHostsMixin, StorageEncryptionMixin): max_concurrency: int = 1, encoding: Optional[str] = None, progress_hook: Optional[Callable[[int, int], None]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> Union[StorageStreamDownloader[str], StorageStreamDownloader[bytes]]: ... diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py index 2fec8f18c13b..490b86a42854 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_download.py @@ -422,6 +422,10 @@ def __len__(self): def _get_encryption_data_request(self) -> None: # Save current request cls download_cls = self._request_options.pop('cls', None) + + # Temporarily removing this for the get properties request + decompress = self._request_options.pop('decompress', None) + # Adjust cls for get_properties self._request_options['cls'] = deserialize_blob_properties @@ -434,6 +438,10 @@ def _get_encryption_data_request(self) -> None: # Restore cls for download self._request_options['cls'] = download_cls + # Decompression does not work with client-side encryption + if decompress is not None: + self._request_options['decompress'] = False + @property def _download_complete(self): if is_encryption_v2(self._encryption_data): diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py index b449b26d3c2c..3166093a736d 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.py @@ -745,6 +745,8 @@ async def download_blob( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the total size of the download. :paramtype progress_hook: Callable[[int, int], Awaitable[None]] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.pyi b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.pyi index 975740f6fb87..94dd817efdf0 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.pyi +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_blob_client_async.pyi @@ -212,6 +212,7 @@ class BlobClient( # type: ignore[misc] max_concurrency: int = 1, encoding: str, progress_hook: Optional[Callable[[int, int], Awaitable[None]]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader[str]: ... @@ -233,6 +234,7 @@ class BlobClient( # type: ignore[misc] max_concurrency: int = 1, encoding: None = None, progress_hook: Optional[Callable[[int, int], Awaitable[None]]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader[bytes]: ... @@ -254,6 +256,7 @@ class BlobClient( # type: ignore[misc] max_concurrency: int = 1, encoding: Optional[str] = None, progress_hook: Optional[Callable[[int, int], Awaitable[None]]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> Union[StorageStreamDownloader[str], StorageStreamDownloader[bytes]]: ... diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py index a620883a1a64..5fb01dfe4a12 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/aio/_download_async.py @@ -292,6 +292,10 @@ def __len__(self): async def _get_encryption_data_request(self) -> None: # Save current request cls download_cls = self._request_options.pop('cls', None) + + # Temporarily removing this for the get properties request + decompress = self._request_options.pop('decompress', None) + # Adjust cls for get_properties self._request_options['cls'] = deserialize_blob_properties @@ -304,6 +308,10 @@ async def _get_encryption_data_request(self) -> None: # Restore cls for download self._request_options['cls'] = download_cls + # Decompression does not work with client-side encryption + if decompress is not None: + self._request_options['decompress'] = False + async def _setup(self) -> None: if self._encryption_options.get("key") is not None or self._encryption_options.get("resolver") is not None: await self._get_encryption_data_request() diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2.py b/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2.py index fd78184903c6..0823bc5bfe1e 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2.py @@ -14,7 +14,7 @@ import pytest from azure.core import MatchConditions from azure.core.exceptions import HttpResponseError -from azure.storage.blob import BlobServiceClient, BlobType +from azure.storage.blob import BlobServiceClient, BlobType, ContentSettings from azure.storage.blob._encryption import ( _dict_to_encryption_data, _validate_and_unwrap_cek, @@ -196,6 +196,27 @@ def test_encryption_kek(self, **kwargs): # Assert assert content == data + @pytest.mark.live_test_only + @BlobPreparer() + def test_decompression_with_encryption(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + kek = KeyWrapper('key1') + self.enable_encryption_v2(kek) + + blob = self.bsc.get_blob_client(self.container_name, self._get_blob_reference()) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + # Cannot directly set content settings on encrypted blobs + blob.upload_blob(data=compressed_data, overwrite=True, content_settings=content_settings) + + result = blob.download_blob(decompress=False).readall() + assert result == compressed_data + @pytest.mark.live_test_only @BlobPreparer() def test_encryption_kek_rsa(self, **kwargs): diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2_async.py b/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2_async.py index 39697234d3fa..6cdd99651ecc 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_encryption_v2_async.py @@ -14,7 +14,7 @@ import pytest from azure.core import MatchConditions from azure.core.exceptions import HttpResponseError -from azure.storage.blob import BlobType +from azure.storage.blob import BlobType, ContentSettings from azure.storage.blob.aio import BlobServiceClient from azure.storage.blob._encryption import ( _dict_to_encryption_data, @@ -199,6 +199,27 @@ async def test_encryption_kek(self, **kwargs): # Assert assert content == data + @pytest.mark.live_test_only + @BlobPreparer() + async def test_decompression_with_encryption(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + await self._setup(storage_account_name, storage_account_key) + kek = KeyWrapper('key1') + self.enable_encryption_v2(kek) + + blob = self.bsc.get_blob_client(self.container_name, self._get_blob_reference()) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + # Cannot directly set content settings on encrypted blobs + await blob.upload_blob(data=compressed_data, overwrite=True, content_setting=content_settings) + + result = await (await blob.download_blob(decompress=False)).readall() + assert result == compressed_data + @pytest.mark.live_test_only @BlobPreparer() async def test_encryption_kek_rsa(self, **kwargs): diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob.py b/sdk/storage/azure-storage-blob/tests/test_common_blob.py index ddd6eea519f2..713107c6c258 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -3658,4 +3658,53 @@ def test_blob_user_delegation_oid(self, **kwargs): return variables + @pytest.mark.live_test_only + @BlobPreparer() + def test_download_blob_decompress(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + self._setup(storage_account_name, storage_account_key) + blob_name = self._get_blob_reference() + blob = self.bsc.get_blob_client(self.container_name, blob_name) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + blob.upload_blob(data=compressed_data, overwrite=True, content_settings=content_settings) + + result = blob.download_blob(decompress=True).readall() + assert result == decompressed_data + + result = blob.download_blob(decompress=False).readall() + assert result == compressed_data + + @pytest.mark.live_test_only + @BlobPreparer() + def test_download_blob_no_decompress_chunks(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + self._setup(storage_account_name, storage_account_key) + blob_name = self._get_blob_reference() + blob = BlobClient( + account_url=self.account_url(storage_account_name, "blob"), + container_name=self.container_name, + blob_name = blob_name, + credential=storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + blob.upload_blob(data=compressed_data, overwrite=True, content_settings=content_settings) + + result = blob.download_blob(decompress=False).readall() + assert result == compressed_data + # ------------------------------------------------------------------------------ \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py index 70e4c2890a85..74de8d8168a9 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py @@ -3592,4 +3592,55 @@ async def test_blob_user_delegation_oid(self, **kwargs): return variables + @pytest.mark.live_test_only + @BlobPreparer() + async def test_download_blob_decompress(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + await self._setup(storage_account_name, storage_account_key) + blob_name = self._get_blob_reference() + blob = self.bsc.get_blob_client(self.container_name, blob_name) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await blob.upload_blob(data=compressed_data, overwrite=True, content_settings=content_settings) + + downloaded = await blob.download_blob(decompress=True) + result = await downloaded.readall() + assert result == decompressed_data + + downloaded = await blob.download_blob(decompress=False) + result = await downloaded.readall() + assert result == compressed_data + + @pytest.mark.live_test_only + @BlobPreparer() + async def test_download_blob_no_decompress_chunks(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + # Arrange + await self._setup(storage_account_name, storage_account_key) + blob_name = self._get_blob_reference() + blob = BlobClient( + account_url=self.account_url(storage_account_name, "blob"), + container_name=self.container_name, + blob_name=blob_name, + credential=storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await blob.upload_blob(data=compressed_data, overwrite=True, content_settings=content_settings) + + result = await (await blob.download_blob(decompress=False)).readall() + assert result == compressed_data + # ------------------------------------------------------------------------------ diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py index bb52bc19498e..d49a2afd9eb6 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_data_lake_file_client.py @@ -693,6 +693,8 @@ def download_file( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the total size of the download. :paramtype progress_hook: ~typing.Callable[[int, int], None] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations. diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py index 6f8eaba8acba..db16eb6d68a1 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/aio/_data_lake_file_client_async.py @@ -709,6 +709,8 @@ async def download_file( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the total size of the download. :paramtype progress_hook: ~typing.Callable[[int, Optional[int]], Awaitable[None]] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations. diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file.py b/sdk/storage/azure-storage-file-datalake/tests/test_file.py index e01aaa5bc854..85a63832f6c1 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file.py @@ -1719,6 +1719,66 @@ def test_progress_hook_upload_data(self, **kwargs): # Assert progress.assert_complete() + @pytest.mark.live_test_only + @DataLakePreparer() + def test_download_file_decompress(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + datalake_storage_account_key = kwargs.pop("datalake_storage_account_key") + + # Arrange + self._setUp(datalake_storage_account_name, datalake_storage_account_key) + file_client = self._create_file_and_return_client() + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + file_client.upload_data( + data=compressed_data, + length=len(compressed_data), + overwrite=True, + content_settings=content_settings + ) + + result = file_client.download_file(decompress=True).readall() + assert result == decompressed_data + + result = file_client.download_file(decompress=False).readall() + assert result == compressed_data + + @pytest.mark.live_test_only + @DataLakePreparer() + def test_download_file_no_decompress_chunks(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + datalake_storage_account_key = kwargs.pop("datalake_storage_account_key") + + self._setUp(datalake_storage_account_name, datalake_storage_account_key) + + file_name = self._get_file_reference() + file_client = DataLakeFileClient( + self.account_url(datalake_storage_account_name, 'dfs'), + self.file_system_name, + file_name, + credential=datalake_storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + file_client.create_file() + + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + file_client.upload_data( + data=compressed_data, + length=len(compressed_data), + overwrite=True, + content_settings=content_settings + ) + + result = file_client.download_file(decompress=False).readall() + assert result == compressed_data + # ------------------------------------------------------------------------------ if __name__ == '__main__': unittest.main() diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py index 29271e90e832..428383995abc 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py @@ -1621,6 +1621,68 @@ async def test_progress_hook_upload_data(self, **kwargs): # Assert progress.assert_complete() + @pytest.mark.live_test_only + @DataLakePreparer() + async def test_download_file_decompress(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + datalake_storage_account_key = kwargs.pop("datalake_storage_account_key") + + # Arrange + await self._setUp(datalake_storage_account_name, datalake_storage_account_key) + file_client = await self._create_file_and_return_client() + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await file_client.upload_data( + data=compressed_data, + length=len(compressed_data), + overwrite=True, + content_settings=content_settings + ) + + file_data = await file_client.download_file(decompress=True) + result = await file_data.readall() + assert result == decompressed_data + + file_data = await file_client.download_file(decompress=False) + result = await file_data.readall() + assert result == compressed_data + + @pytest.mark.live_test_only + @DataLakePreparer() + async def test_download_file_no_decompress_chunks(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + datalake_storage_account_key = kwargs.pop("datalake_storage_account_key") + + await self._setUp(datalake_storage_account_name, datalake_storage_account_key) + + file_name = self._get_file_reference() + file_client = DataLakeFileClient( + self.account_url(datalake_storage_account_name, 'dfs'), + self.file_system_name, + file_name, + credential=datalake_storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + await file_client.create_file() + + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await file_client.upload_data( + data=compressed_data, + length=len(compressed_data), + overwrite=True, + content_settings=content_settings + ) + + result = await (await file_client.download_file(decompress=False)).readall() + assert result == compressed_data + # ------------------------------------------------------------------------------ if __name__ == '__main__': unittest.main() diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py index 7a73cb7a894a..d3d21af0a23b 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.py @@ -897,6 +897,8 @@ def download_file( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the total size of the download. :paramtype progress_hook: Callable[[int, int], None] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-file-service-operations. diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.pyi b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.pyi index 390e10b2eecb..5d7f73e7d812 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.pyi +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_file_client.pyi @@ -202,6 +202,7 @@ class ShareFileClient(StorageAccountHostsMixin): validate_content: bool = False, lease: Optional[Union[ShareLeaseClient, str]] = None, progress_hook: Optional[Callable[[int, Optional[int]], None]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader: ... diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py index 4e57ca894336..cb66e81a1d7e 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.py @@ -892,6 +892,8 @@ async def download_file( function(current: int, total: int) where current is the number of bytes transferred so far, and total is the size of the blob or None if the size is unknown. :paramtype progress_hook: Callable[[int, int], Awaitable[None]] + :keyword bool decompress: If True, any compressed content, identified by the Content-Encoding header, will be + decompressed automatically before being returned. Default value is True. :keyword int timeout: Sets the server-side timeout for the operation in seconds. For more details see https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-file-service-operations. diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.pyi b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.pyi index 91aac0ed7002..43a20ccb3ad7 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.pyi +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/aio/_file_client_async.pyi @@ -202,6 +202,7 @@ class ShareFileClient(AsyncStorageAccountHostsMixin, StorageAccountHostsMixin): validate_content: bool = False, lease: Optional[Union[ShareLeaseClient, str]] = None, progress_hook: Optional[Callable[[int, Optional[int]], Awaitable[None]]] = None, + decompress: Optional[bool] = None, timeout: Optional[int] = None, **kwargs: Any ) -> StorageStreamDownloader: ... diff --git a/sdk/storage/azure-storage-file-share/tests/test_file.py b/sdk/storage/azure-storage-file-share/tests/test_file.py index 36931159ac82..f9502f059702 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file.py @@ -141,8 +141,8 @@ def _wait_for_async_copy(self, share_name, file_path): properties = file_client.get_file_properties() assert properties.copy.status == 'success' - def assertFileEqual(self, file_client, expected_data): - actual_data = file_client.download_file().readall() + def assertFileEqual(self, file_client, expected_data, **kwargs): + actual_data = file_client.download_file(**kwargs).readall() assert actual_data == expected_data class NonSeekableFile(object): @@ -3986,4 +3986,53 @@ def test_create_file_with_data(self, **kwargs): downloaded_data = file_client.download_file().readall() assert downloaded_data == data + @pytest.mark.live_test_only + @FileSharePreparer() + def test_download_file_decompress(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + file_name = self._get_file_reference() + file_client = ShareFileClient( + self.account_url(storage_account_name, "file"), + share_name=self.share_name, + file_path=file_name, + credential=storage_account_key, + max_range_size=4 * 1024 + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + file_client.upload_file(data=compressed_data, content_settings=content_settings) + self.assertFileEqual(file_client, decompressed_data, decompress=True) + self.assertFileEqual(file_client, compressed_data, decompress=False) + + @pytest.mark.live_test_only + @FileSharePreparer() + def test_download_file_no_decompress_chunks(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + file_name = self._get_file_reference() + file_client = ShareFileClient( + self.account_url(storage_account_name, "file"), + share_name=self.share_name, + file_path=file_name, + credential=storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + file_client.upload_file(data=compressed_data, content_settings=content_settings) + + result = file_client.download_file(decompress=False).readall() + assert result == compressed_data + # ------------------------------------------------------------------------------ diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_async.py b/sdk/storage/azure-storage-file-share/tests/test_file_async.py index 51a7cf8e14c8..dc2da5dfcda7 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_async.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_async.py @@ -4112,3 +4112,54 @@ async def test_create_file_with_data(self, **kwargs): await file_client.create_file(len(data), data=data) downloaded_data = await (await file_client.download_file()).readall() assert downloaded_data == data + + @pytest.mark.live_test_only + @FileSharePreparer() + async def test_download_file_decompress(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + await self._setup_share(storage_account_name, storage_account_key) + file_name = self._get_file_reference() + file_client = ShareFileClient( + self.account_url(storage_account_name, "file"), + share_name=self.share_name, + file_path=file_name, + credential=storage_account_key, + max_range_size=4 * 1024 + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + decompressed_data = b"hello from gzip" + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await file_client.upload_file(data=compressed_data, content_settings=content_settings) + await self.assertFileEqual(file_client, decompressed_data, decompress=True) + await self.assertFileEqual(file_client, compressed_data, decompress=False) + + @pytest.mark.live_test_only + @FileSharePreparer() + async def test_download_file_no_decompress_chunks(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + self._setup(storage_account_name, storage_account_key) + await self._setup_share(storage_account_name, storage_account_key) + file_name = self._get_file_reference() + file_client = ShareFileClient( + self.account_url(storage_account_name, "file"), + share_name=self.share_name, + file_path=file_name, + credential=storage_account_key, + max_chunk_get_size=4, + max_single_get_size=4, + ) + compressed_data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcaH\xcd\xc9\xc9WH+\xca\xcfUH\xaf\xca,\x00\x00\x00\x00\xff\xff\x03\x00d\xaa\x8e\xb5\x0f\x00\x00\x00' + content_settings = ContentSettings(content_encoding='gzip') + + # Act / Assert + await file_client.upload_file(data=compressed_data, content_settings=content_settings) + + result = await (await file_client.download_file(decompress=False)).readall() + assert result == compressed_data