diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index ef20b4b146cc..08618f4e1524 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/storage/azure-storage-blob", - "Tag": "python/storage/azure-storage-blob_42c2d90038" + "Tag": "python/storage/azure-storage-blob_05e6598c4a" } diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads.py index 88d52eab5d12..0aa75e69e77c 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads.py @@ -4,15 +4,17 @@ # license information. # -------------------------------------------------------------------------- +import os from concurrent import futures from io import BytesIO, IOBase, SEEK_CUR, SEEK_END, SEEK_SET, UnsupportedOperation from itertools import islice from math import ceil from threading import Lock +from uuid import uuid4 from azure.core.tracing.common import with_current_context -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers @@ -116,7 +118,7 @@ def upload_substream_blocks( else: range_ids = [uploader.process_substream_block(b) for b in uploader.get_substream_blocks()] if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return [] @@ -257,9 +259,10 @@ def __init__(self, *args, **kwargs): self.current_length = None def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") self.service.stage_block( block_id, len(chunk_data), @@ -272,7 +275,7 @@ def _upload_chunk(self, chunk_offset, chunk_data): def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) self.service.stage_block( block_id, len(block_stream), @@ -283,7 +286,7 @@ def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads_async.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads_async.py index 6ed5ba1d0f91..9b2893f28fbe 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads_async.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/uploads_async.py @@ -6,16 +6,18 @@ import asyncio # pylint: disable=do-not-import-asyncio import inspect +import os import threading from io import UnsupportedOperation from itertools import islice from math import ceil from typing import AsyncGenerator, Union +from uuid import uuid4 -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers -from .uploads import SubStream, IterStreamer # pylint: disable=unused-import +from .uploads import IterStreamer, SubStream # pylint: disable=unused-import async def _async_parallel_uploads(uploader, pending, running): @@ -140,7 +142,7 @@ async def upload_substream_blocks( for block in uploader.get_substream_blocks(): range_ids.append(await uploader.process_substream_block(block)) if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return @@ -283,9 +285,10 @@ def __init__(self, *args, **kwargs): self.current_length = None async def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") await self.service.stage_block( block_id, len(chunk_data), @@ -298,7 +301,7 @@ async def _upload_chunk(self, chunk_offset, chunk_data): async def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) await self.service.stage_block( block_id, len(block_stream), @@ -309,7 +312,7 @@ async def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-blob/tests/conftest.py b/sdk/storage/azure-storage-blob/tests/conftest.py index 01daca17a5c9..6fb898d04ea1 100644 --- a/sdk/storage/azure-storage-blob/tests/conftest.py +++ b/sdk/storage/azure-storage-blob/tests/conftest.py @@ -9,11 +9,11 @@ import pytest from devtools_testutils import ( + add_body_regex_sanitizer, add_general_regex_sanitizer, add_header_regex_sanitizer, add_oauth_response_sanitizer, add_uri_regex_sanitizer, - test_proxy, ) @@ -36,3 +36,11 @@ def add_sanitizers(test_proxy): regex=r"(?<=[?&]sktid=)[^&#]+", value="00000000-0000-0000-0000-000000000000", ) + add_uri_regex_sanitizer( + regex=r"(?<=[?&]blockid=)[^&#]+", + value="00000000-0000-0000-0000-000000000000", + ) + add_body_regex_sanitizer( + regex=r"[^<]+", + value="00000000-0000-0000-0000-000000000000", + ) diff --git a/sdk/storage/azure-storage-blob/tests/test_block_blob.py b/sdk/storage/azure-storage-blob/tests/test_block_blob.py index 6a4069af0a85..4503ab9a3fac 100644 --- a/sdk/storage/azure-storage-blob/tests/test_block_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_block_blob.py @@ -8,6 +8,7 @@ import tempfile from datetime import datetime, timedelta from io import BytesIO +from unittest import mock import pytest import requests @@ -16,6 +17,7 @@ from devtools_testutils.storage import StorageRecordedTestCase from fake_credentials import CPK_KEY_HASH, CPK_KEY_VALUE from settings.testcase import BlobPreparer +from test_content_validation import _deterministic_urandom from test_helpers import _build_base_file_share_headers, _create_file_share_oauth, NonSeekableStream, ProgressTracker from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceModifiedError, ResourceNotFoundError @@ -1780,7 +1782,8 @@ def test_create_blob_with_md5_chunked(self, **kwargs): data = self.get_random_bytes(LARGE_BLOB_SIZE) # Act - blob.upload_blob(data, validate_content=True) + with mock.patch("os.urandom", _deterministic_urandom()): + blob.upload_blob(data, validate_content=True) # Assert diff --git a/sdk/storage/azure-storage-blob/tests/test_block_blob_async.py b/sdk/storage/azure-storage-blob/tests/test_block_blob_async.py index b5dbc367c01e..59a213c2a703 100644 --- a/sdk/storage/azure-storage-blob/tests/test_block_blob_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_block_blob_async.py @@ -8,6 +8,7 @@ import tempfile from datetime import datetime, timedelta from io import BytesIO +from unittest import mock import aiohttp import pytest @@ -16,6 +17,7 @@ from devtools_testutils.storage.aio import AsyncStorageRecordedTestCase from fake_credentials import CPK_KEY_HASH, CPK_KEY_VALUE from settings.testcase import BlobPreparer +from test_content_validation import _deterministic_urandom from test_helpers_async import ( NonSeekableStream, ProgressTracker, @@ -1923,7 +1925,8 @@ async def test_create_blob_with_md5_chunked(self, **kwargs): data = self.get_random_bytes(LARGE_BLOB_SIZE) # Act - await blob.upload_blob(data, validate_content=True) + with mock.patch("os.urandom", _deterministic_urandom()): + await blob.upload_blob(data, validate_content=True) # Assert 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 932827f24c8c..0a45613ee028 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -5,8 +5,10 @@ # -------------------------------------------------------------------------- # pylint: disable=attribute-defined-outside-init, too-many-public-methods +import base64 import os import tempfile +import threading import uuid from datetime import datetime, timedelta from enum import Enum @@ -58,6 +60,11 @@ upload_blob_to_url, ) from azure.storage.blob._generated.models import RehydratePriority +from azure.storage.blob._shared.uploads import ( + BlockBlobChunkUploader, + upload_data_chunks, + upload_substream_blocks, +) # ------------------------------------------------------------------------------ SMALL_BLOB_SIZE = 1024 @@ -66,6 +73,34 @@ # ------------------------------------------------------------------------------ +class _RecordingBlockBlobService: + """Minimal fake service that records the block IDs and data passed to stage_block.""" + + def __init__(self): + self._lock = threading.Lock() + self.blocks = {} # block_id -> staged bytes + + def stage_block(self, block_id, length, data, **kwargs): # pylint: disable=unused-argument + content = data.read() if hasattr(data, "read") else data + with self._lock: + self.blocks[block_id] = content + + +def _assert_unique_ordered_block_ids(block_ids, expected_data, staged_blocks, expected_length): + # The data was actually split into multiple blocks. + assert len(block_ids) > 1 + # Every block ID is unique (the point of the feature). + assert len(set(block_ids)) == len(block_ids) + # All block IDs are equal-length, valid base64 (Azure requires equal length per blob). + assert len({len(block_id) for block_id in block_ids}) == 1 + for block_id in block_ids: + # Length is fixed per upload path to match the previous scheme (chunk=64, substream=12). + assert len(block_id) == expected_length + base64.b64decode(block_id) # must be valid base64 + # Committing the blocks in the returned order must reproduce the original content. + assert b"".join(staged_blocks[block_id] for block_id in block_ids) == expected_data + + class TestStorageCommonBlob(StorageRecordedTestCase): def _setup(self, storage_account_name, key): self.bsc = BlobServiceClient(self.account_url(storage_account_name, "blob"), credential=key.secret) @@ -4020,4 +4055,42 @@ def test_download_blob_returns_access_tier(self, **kwargs): assert downloader.properties.blob_tier_change_time is not None assert not downloader.properties.blob_tier_inferred + def test_block_blob_upload_generates_unique_ordered_block_ids(self): + # Each staged block must get a unique block ID, and the block list must be + # committed in byte-offset order even when chunks finish out of order under concurrency. + data = b"".join(bytes([i]) * 8 for i in range(20)) # 20 distinct 8-byte chunks + service = _RecordingBlockBlobService() + + block_ids = upload_data_chunks( + service=service, + uploader_class=BlockBlobChunkUploader, + total_size=len(data), + chunk_size=8, + max_concurrency=4, + stream=BytesIO(data), + validate_content=False, + progress_hook=None, + ) + + # Chunk path uses a 48-digit zero-padded UUID -> 64-char block IDs. + _assert_unique_ordered_block_ids(block_ids, data, service.blocks, 64) + + def test_block_blob_substream_upload_generates_unique_ordered_block_ids(self): + data = b"".join(bytes([i]) * 8 for i in range(20)) + service = _RecordingBlockBlobService() + + block_ids = upload_substream_blocks( + service=service, + uploader_class=BlockBlobChunkUploader, + total_size=len(data), + chunk_size=8, + max_concurrency=4, + stream=BytesIO(data), + validate_content=False, + progress_hook=None, + ) + + # Substream path uses os.urandom(9) -> 12-char block IDs (matches old length). + _assert_unique_ordered_block_ids(block_ids, data, service.blocks, 12) + # ------------------------------------------------------------------------------ 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 88885ee7d1ff..7e320178a4d7 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 @@ -6,6 +6,7 @@ # pylint: disable=attribute-defined-outside-init, too-many-public-methods import asyncio +import base64 import os import tempfile import uuid @@ -62,6 +63,11 @@ download_blob_from_url, upload_blob_to_url, ) +from azure.storage.blob._shared.uploads_async import ( + BlockBlobChunkUploader, + upload_data_chunks, + upload_substream_blocks, +) # ------------------------------------------------------------------------------ SMALL_BLOB_SIZE = 1024 @@ -70,6 +76,34 @@ # ------------------------------------------------------------------------------ +class _RecordingBlockBlobServiceAsync: + """Minimal fake async service that records the block IDs and data passed to stage_block.""" + + def __init__(self): + self.blocks = {} # block_id -> staged bytes + + async def stage_block(self, block_id, length, data=None, body=None, **kwargs): # pylint: disable=unused-argument + content = data if data is not None else body + if hasattr(content, "read"): + content = content.read() + self.blocks[block_id] = content + + +def _assert_unique_ordered_block_ids(block_ids, expected_data, staged_blocks, expected_length): + # The data was actually split into multiple blocks. + assert len(block_ids) > 1 + # Every block ID is unique (the point of the feature). + assert len(set(block_ids)) == len(block_ids) + # All block IDs are equal-length, valid base64 (Azure requires equal length per blob). + assert len({len(block_id) for block_id in block_ids}) == 1 + for block_id in block_ids: + # Length is fixed per upload path to match the previous scheme (chunk=64, substream=12). + assert len(block_id) == expected_length + base64.b64decode(block_id) # must be valid base64 + # Committing the blocks in the returned order must reproduce the original content. + assert b"".join(staged_blocks[block_id] for block_id in block_ids) == expected_data + + class TestStorageCommonBlobAsync(AsyncStorageRecordedTestCase): # --Helpers----------------------------------------------------------------- async def _setup(self, storage_account_name, key): @@ -3928,5 +3962,47 @@ async def test_download_blob_returns_access_tier(self, **kwargs): assert downloader.properties.blob_tier_change_time is not None assert not downloader.properties.blob_tier_inferred + def test_block_blob_upload_generates_unique_ordered_block_ids(self): + # Each staged block must get a unique block ID, and the block list must be + # committed in byte-offset order even when chunks finish out of order under concurrency. + data = b"".join(bytes([i]) * 8 for i in range(20)) # 20 distinct 8-byte chunks + service = _RecordingBlockBlobServiceAsync() + + async def run(): + return await upload_data_chunks( + service=service, + uploader_class=BlockBlobChunkUploader, + total_size=len(data), + chunk_size=8, + max_concurrency=4, + stream=BytesIO(data), + validate_content=False, + progress_hook=None, + ) + + block_ids = asyncio.run(run()) + # Chunk path uses a 48-digit zero-padded UUID -> 64-char block IDs. + _assert_unique_ordered_block_ids(block_ids, data, service.blocks, 64) + + def test_block_blob_substream_upload_generates_unique_ordered_block_ids(self): + data = b"".join(bytes([i]) * 8 for i in range(20)) + service = _RecordingBlockBlobServiceAsync() + + async def run(): + return await upload_substream_blocks( + service=service, + uploader_class=BlockBlobChunkUploader, + total_size=len(data), + chunk_size=8, + max_concurrency=4, + stream=BytesIO(data), + validate_content=False, + progress_hook=None, + ) + + block_ids = asyncio.run(run()) + # Substream path uses os.urandom(9) -> 12-char block IDs (matches old length). + _assert_unique_ordered_block_ids(block_ids, data, service.blocks, 12) + # ------------------------------------------------------------------------------ diff --git a/sdk/storage/azure-storage-blob/tests/test_content_validation.py b/sdk/storage/azure-storage-blob/tests/test_content_validation.py index bc7633b087d3..280491b924a5 100644 --- a/sdk/storage/azure-storage-blob/tests/test_content_validation.py +++ b/sdk/storage/azure-storage-blob/tests/test_content_validation.py @@ -4,7 +4,9 @@ # license information. # -------------------------------------------------------------------------- +import itertools from io import BytesIO +from unittest import mock import pytest from devtools_testutils import is_live, recorded_by_proxy @@ -54,6 +56,11 @@ def assert_structured_message_get(response): assert response.http_response.headers.get("x-ms-structured-body") is not None +def _deterministic_urandom(): + counter = itertools.count(1) + return lambda size: next(counter).to_bytes(size, "big") + + class TestIter: def __init__(self, data, *, chunk_size=100): self.data = data @@ -184,26 +191,34 @@ def test_upload_blob_chunks(self, a, b, **kwargs): byte_iter = TestIter(byte_data) str_iter = TestIter(str_data) - blob.upload_blob(byte_data, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method) - assert blob.download_blob().read() == byte_data - blob.upload_blob( - str_data, blob_type=a, encoding="utf-8", validate_content=b, overwrite=True, raw_request_hook=assert_method - ) - assert blob.download_blob().read() == str_data_encoded - blob.upload_blob(byte_stream, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method) - assert blob.download_blob().read() == byte_data - blob.upload_blob(byte_iter, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method) - assert blob.download_blob().read() == byte_data - 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, - ) - assert blob.download_blob().read() == str_data_encoded + with mock.patch("os.urandom", _deterministic_urandom()): + blob.upload_blob(byte_data, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method) + assert blob.download_blob().read() == byte_data + blob.upload_blob( + str_data, + blob_type=a, + encoding="utf-8", + validate_content=b, + overwrite=True, + raw_request_hook=assert_method, + ) + assert blob.download_blob().read() == str_data_encoded + blob.upload_blob( + byte_stream, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method + ) + assert blob.download_blob().read() == byte_data + blob.upload_blob(byte_iter, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method) + assert blob.download_blob().read() == byte_data + 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, + ) + assert blob.download_blob().read() == str_data_encoded @BlobPreparer() @pytest.mark.parametrize("a", [True, "md5", "crc64"]) # a: validate_content @@ -224,7 +239,8 @@ def test_upload_blob_substream(self, a, **kwargs): io = BytesIO(data) # Act - blob.upload_blob(io, validate_content=a, raw_request_hook=assert_method) + with mock.patch("os.urandom", _deterministic_urandom()): + blob.upload_blob(io, validate_content=a, raw_request_hook=assert_method) # Assert content = blob.download_blob() diff --git a/sdk/storage/azure-storage-blob/tests/test_content_validation_async.py b/sdk/storage/azure-storage-blob/tests/test_content_validation_async.py index 00443d20cb03..c5e66a6f95c3 100644 --- a/sdk/storage/azure-storage-blob/tests/test_content_validation_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_content_validation_async.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------- from io import BytesIO +from unittest import mock import pytest from devtools_testutils import is_live @@ -23,6 +24,7 @@ assert_structured_message, assert_structured_message_get, TestIter, + _deterministic_urandom, ) from azure.core.exceptions import ResourceExistsError @@ -152,32 +154,38 @@ async def test_upload_blob_chunks(self, a, b, **kwargs): str_iter = TestIter(str_data) # Act / Assert - await blob.upload_blob( - byte_data, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method - ) - assert await (await blob.download_blob()).read() == byte_data - await blob.upload_blob( - str_data, blob_type=a, encoding="utf-8", validate_content=b, overwrite=True, raw_request_hook=assert_method - ) - assert await (await blob.download_blob()).read() == str_data_encoded - await blob.upload_blob( - byte_stream, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method - ) - assert await (await blob.download_blob()).read() == byte_data - await blob.upload_blob( - byte_iter, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method - ) - assert await (await blob.download_blob()).read() == byte_data - 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, - ) - assert await (await blob.download_blob()).read() == str_data_encoded + with mock.patch("os.urandom", _deterministic_urandom()): + await blob.upload_blob( + byte_data, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method + ) + assert await (await blob.download_blob()).read() == byte_data + await blob.upload_blob( + str_data, + blob_type=a, + encoding="utf-8", + validate_content=b, + overwrite=True, + raw_request_hook=assert_method, + ) + assert await (await blob.download_blob()).read() == str_data_encoded + await blob.upload_blob( + byte_stream, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method + ) + assert await (await blob.download_blob()).read() == byte_data + await blob.upload_blob( + byte_iter, blob_type=a, validate_content=b, overwrite=True, raw_request_hook=assert_method + ) + assert await (await blob.download_blob()).read() == byte_data + 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, + ) + assert await (await blob.download_blob()).read() == str_data_encoded @BlobPreparer() @pytest.mark.parametrize("a", [True, "md5", "crc64"]) # a: validate_content @@ -198,7 +206,8 @@ async def test_upload_blob_substream(self, a, **kwargs): io = BytesIO(data) # Act - await blob.upload_blob(io, validate_content=a, raw_request_hook=assert_method) + with mock.patch("os.urandom", _deterministic_urandom()): + await blob.upload_blob(io, validate_content=a, raw_request_hook=assert_method) # Assert content = await blob.download_blob() diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads.py index 88d52eab5d12..0aa75e69e77c 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads.py @@ -4,15 +4,17 @@ # license information. # -------------------------------------------------------------------------- +import os from concurrent import futures from io import BytesIO, IOBase, SEEK_CUR, SEEK_END, SEEK_SET, UnsupportedOperation from itertools import islice from math import ceil from threading import Lock +from uuid import uuid4 from azure.core.tracing.common import with_current_context -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers @@ -116,7 +118,7 @@ def upload_substream_blocks( else: range_ids = [uploader.process_substream_block(b) for b in uploader.get_substream_blocks()] if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return [] @@ -257,9 +259,10 @@ def __init__(self, *args, **kwargs): self.current_length = None def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") self.service.stage_block( block_id, len(chunk_data), @@ -272,7 +275,7 @@ def _upload_chunk(self, chunk_offset, chunk_data): def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) self.service.stage_block( block_id, len(block_stream), @@ -283,7 +286,7 @@ def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads_async.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads_async.py index 6ed5ba1d0f91..9b2893f28fbe 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads_async.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/uploads_async.py @@ -6,16 +6,18 @@ import asyncio # pylint: disable=do-not-import-asyncio import inspect +import os import threading from io import UnsupportedOperation from itertools import islice from math import ceil from typing import AsyncGenerator, Union +from uuid import uuid4 -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers -from .uploads import SubStream, IterStreamer # pylint: disable=unused-import +from .uploads import IterStreamer, SubStream # pylint: disable=unused-import async def _async_parallel_uploads(uploader, pending, running): @@ -140,7 +142,7 @@ async def upload_substream_blocks( for block in uploader.get_substream_blocks(): range_ids.append(await uploader.process_substream_block(block)) if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return @@ -283,9 +285,10 @@ def __init__(self, *args, **kwargs): self.current_length = None async def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") await self.service.stage_block( block_id, len(chunk_data), @@ -298,7 +301,7 @@ async def _upload_chunk(self, chunk_offset, chunk_data): async def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) await self.service.stage_block( block_id, len(block_stream), @@ -309,7 +312,7 @@ async def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-file-share/api.md b/sdk/storage/azure-storage-file-share/api.md index dec031a05dee..960008b75c15 100644 --- a/sdk/storage/azure-storage-file-share/api.md +++ b/sdk/storage/azure-storage-file-share/api.md @@ -455,6 +455,62 @@ namespace azure.storage.fileshare def values(self): ... + class azure.storage.fileshare.FileRange(DictMixin): + cleared: bool + end: int + start: int + + def __contains__(self, key): ... + + def __delitem__(self, key): ... + + def __eq__(self, other): ... + + def __getitem__(self, key): ... + + def __init__( + self, + start: int, + end: int, + *, + cleared: bool = False + ) -> None: ... + + def __len__(self): ... + + def __ne__(self, other): ... + + def __repr__(self): ... + + def __setitem__( + self, + key, + item + ): ... + + def __str__(self): ... + + def get( + self, + key, + default = None + ): ... + + def has_key(self, k): ... + + def items(self): ... + + def keys(self): ... + + def update( + self, + *args, + **kwargs + ): ... + + def values(self): ... + + class azure.storage.fileshare.FileSasPermissions: create: bool = False delete: bool = False @@ -1510,6 +1566,32 @@ namespace azure.storage.fileshare **kwargs: Any ) -> ItemPaged[Handle]: ... + @distributed_trace + def list_ranges( + self, + *, + lease: Union[ShareLeaseClient, str] = ..., + length: Optional[int] = ..., + offset: Optional[int] = ..., + results_per_page: Optional[int] = ..., + timeout: Optional[int] = ..., + **kwargs: Any + ) -> ItemPaged[FileRange]: ... + + @distributed_trace + def list_ranges_diff( + self, + previous_sharesnapshot: Union[str, Dict[str, Any]], + *, + include_renames: Optional[bool] = ..., + lease: Union[ShareLeaseClient, str] = ..., + length: Optional[int] = ..., + offset: Optional[int] = ..., + results_per_page: Optional[int] = ..., + timeout: Optional[int] = ..., + **kwargs: Any + ) -> ItemPaged[FileRange]: ... + @distributed_trace def rename_file( self, @@ -3067,6 +3149,32 @@ namespace azure.storage.fileshare.aio **kwargs: Any ) -> AsyncItemPaged[Handle]: ... + @distributed_trace + def list_ranges( + self, + *, + lease: Union[ShareLeaseClient, str] = ..., + length: Optional[int] = ..., + offset: Optional[int] = ..., + results_per_page: Optional[int] = ..., + timeout: Optional[int] = ..., + **kwargs: Any + ) -> AsyncItemPaged[FileRange]: ... + + @distributed_trace + def list_ranges_diff( + self, + previous_sharesnapshot: Union[str, Dict[str, Any]], + *, + include_renames: Optional[bool] = ..., + lease: Union[ShareLeaseClient, str] = ..., + length: Optional[int] = ..., + offset: Optional[int] = ..., + results_per_page: Optional[int] = ..., + timeout: Optional[int] = ..., + **kwargs: Any + ) -> AsyncItemPaged[FileRange]: ... + @distributed_trace_async async def rename_file( self, diff --git a/sdk/storage/azure-storage-file-share/api.metadata.yml b/sdk/storage/azure-storage-file-share/api.metadata.yml index 971c843d375e..b33bc80ad668 100644 --- a/sdk/storage/azure-storage-file-share/api.metadata.yml +++ b/sdk/storage/azure-storage-file-share/api.metadata.yml @@ -1,3 +1,3 @@ -apiMdSha256: 3c2f71c139d0f1e3430e1f6802e9b27f9940c011b1da95748721350c53a0c6f8 +apiMdSha256: bff208fab40e4aaf5c2b4056336d622ebc1861f60d37ba1fe16969b86e8b7c46 parserVersion: 0.3.28 pythonVersion: 3.13.14 diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads.py index 88d52eab5d12..0aa75e69e77c 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads.py @@ -4,15 +4,17 @@ # license information. # -------------------------------------------------------------------------- +import os from concurrent import futures from io import BytesIO, IOBase, SEEK_CUR, SEEK_END, SEEK_SET, UnsupportedOperation from itertools import islice from math import ceil from threading import Lock +from uuid import uuid4 from azure.core.tracing.common import with_current_context -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers @@ -116,7 +118,7 @@ def upload_substream_blocks( else: range_ids = [uploader.process_substream_block(b) for b in uploader.get_substream_blocks()] if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return [] @@ -257,9 +259,10 @@ def __init__(self, *args, **kwargs): self.current_length = None def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") self.service.stage_block( block_id, len(chunk_data), @@ -272,7 +275,7 @@ def _upload_chunk(self, chunk_offset, chunk_data): def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) self.service.stage_block( block_id, len(block_stream), @@ -283,7 +286,7 @@ def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads_async.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads_async.py index 6ed5ba1d0f91..9b2893f28fbe 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads_async.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads_async.py @@ -6,16 +6,18 @@ import asyncio # pylint: disable=do-not-import-asyncio import inspect +import os import threading from io import UnsupportedOperation from itertools import islice from math import ceil from typing import AsyncGenerator, Union +from uuid import uuid4 -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers -from .uploads import SubStream, IterStreamer # pylint: disable=unused-import +from .uploads import IterStreamer, SubStream # pylint: disable=unused-import async def _async_parallel_uploads(uploader, pending, running): @@ -140,7 +142,7 @@ async def upload_substream_blocks( for block in uploader.get_substream_blocks(): range_ids.append(await uploader.process_substream_block(block)) if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return @@ -283,9 +285,10 @@ def __init__(self, *args, **kwargs): self.current_length = None async def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") await self.service.stage_block( block_id, len(chunk_data), @@ -298,7 +301,7 @@ async def _upload_chunk(self, chunk_offset, chunk_data): async def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) await self.service.stage_block( block_id, len(block_stream), @@ -309,7 +312,7 @@ async def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-queue/api.metadata.yml b/sdk/storage/azure-storage-queue/api.metadata.yml index 4565e0d8f2ff..f1a6cdd3cc09 100644 --- a/sdk/storage/azure-storage-queue/api.metadata.yml +++ b/sdk/storage/azure-storage-queue/api.metadata.yml @@ -1,3 +1,3 @@ apiMdSha256: f44cff7f34be5e007ec8b69b8de79f840f53561af6372a881d4e8c56b712078d parserVersion: 0.3.28 -pythonVersion: 3.13.9 +pythonVersion: 3.13.14 diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads.py index 341d034fd07c..133ab870cb97 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads.py @@ -4,15 +4,17 @@ # license information. # -------------------------------------------------------------------------- +import os from concurrent import futures from io import BytesIO, IOBase, SEEK_CUR, SEEK_END, SEEK_SET, UnsupportedOperation from itertools import islice from math import ceil from threading import Lock +from uuid import uuid4 from azure.core.tracing.common import with_current_context -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers @@ -121,7 +123,7 @@ def upload_substream_blocks( else: range_ids = [uploader.process_substream_block(b) for b in uploader.get_substream_blocks()] if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return [] @@ -265,9 +267,10 @@ def __init__(self, *args, **kwargs): self.current_length = None def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") self.service.stage_block( block_id, len(chunk_data), @@ -280,7 +283,7 @@ def _upload_chunk(self, chunk_offset, chunk_data): def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) self.service.stage_block( block_id, len(block_stream), @@ -291,7 +294,7 @@ def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader): diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads_async.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads_async.py index 388429a288a4..2e0403740a6e 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads_async.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/uploads_async.py @@ -6,16 +6,18 @@ import asyncio # pylint: disable=do-not-import-asyncio import inspect +import os import threading from io import UnsupportedOperation from itertools import islice from math import ceil from typing import AsyncGenerator, Union +from uuid import uuid4 -from . import encode_base64, url_quote +from . import encode_base64 from .request_handlers import get_length from .response_handlers import return_response_headers -from .uploads import SubStream, IterStreamer # pylint: disable=unused-import +from .uploads import IterStreamer, SubStream # pylint: disable=unused-import async def _async_parallel_uploads(uploader, pending, running): @@ -140,7 +142,7 @@ async def upload_substream_blocks( for block in uploader.get_substream_blocks(): range_ids.append(await uploader.process_substream_block(block)) if any(range_ids): - return sorted(range_ids) + return [block_id for _, block_id in sorted(range_ids, key=lambda r: r[0])] return @@ -286,9 +288,10 @@ def __init__(self, *args, **kwargs): self.current_length = None async def _upload_chunk(self, chunk_offset, chunk_data): - # TODO: This is incorrect, but works with recording. + # Generate a unique block ID for each staged block. The chunk offset is + # still returned so the block list can be committed in the correct order. index = f"{chunk_offset:032d}" - block_id = encode_base64(url_quote(encode_base64(index))) + block_id = encode_base64(f"{uuid4().int:048d}") await self.service.stage_block( block_id, len(chunk_data), @@ -301,7 +304,7 @@ async def _upload_chunk(self, chunk_offset, chunk_data): async def _upload_substream_block(self, index, block_stream): try: - block_id = f"BlockId{(index//self.chunk_size):05}" + block_id = encode_base64(os.urandom(9)) await self.service.stage_block( block_id, len(block_stream), @@ -312,7 +315,7 @@ async def _upload_substream_block(self, index, block_stream): ) finally: block_stream.close() - return block_id + return index, block_id class PageBlobChunkUploader(_ChunkUploader):