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..a2523602eb27 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
@@ -9,10 +9,11 @@
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 +117,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 +258,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(chunk_data),
@@ -272,7 +274,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(block_stream),
@@ -283,7 +285,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..4d576e49ab6d 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
@@ -11,11 +11,12 @@
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 +141,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 +284,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(chunk_data),
@@ -298,7 +300,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(block_stream),
@@ -309,7 +311,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_common_blob.py b/sdk/storage/azure-storage-blob/tests/test_common_blob.py
index 932827f24c8c..1d5c2ad0d54d 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,32 @@
# ------------------------------------------------------------------------------
+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):
+ # 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:
+ assert len(base64.b64decode(block_id)) == 16
+ # 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 +4053,40 @@ 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,
+ )
+
+ _assert_unique_ordered_block_ids(block_ids, data, service.blocks)
+
+ 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,
+ )
+
+ _assert_unique_ordered_block_ids(block_ids, data, service.blocks)
+
# ------------------------------------------------------------------------------
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..5655099e7525 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,32 @@
# ------------------------------------------------------------------------------
+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):
+ # 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:
+ assert len(base64.b64decode(block_id)) == 16
+ # 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 +3960,45 @@ 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())
+ _assert_unique_ordered_block_ids(block_ids, data, service.blocks)
+
+ 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())
+ _assert_unique_ordered_block_ids(block_ids, data, service.blocks)
+
# ------------------------------------------------------------------------------
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..a2523602eb27 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
@@ -9,10 +9,11 @@
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 +117,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 +258,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(chunk_data),
@@ -272,7 +274,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(block_stream),
@@ -283,7 +285,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..4d576e49ab6d 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
@@ -11,11 +11,12 @@
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 +141,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 +284,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(chunk_data),
@@ -298,7 +300,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(block_stream),
@@ -309,7 +311,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/azure/storage/fileshare/_shared/uploads.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/uploads.py
index 88d52eab5d12..a2523602eb27 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
@@ -9,10 +9,11 @@
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 +117,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 +258,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(chunk_data),
@@ -272,7 +274,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(block_stream),
@@ -283,7 +285,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..4d576e49ab6d 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
@@ -11,11 +11,12 @@
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 +141,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 +284,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(chunk_data),
@@ -298,7 +300,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(block_stream),
@@ -309,7 +311,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..c0b482bc7f22 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
@@ -9,10 +9,11 @@
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 +122,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 +266,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(chunk_data),
@@ -280,7 +282,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(uuid4().bytes)
self.service.stage_block(
block_id,
len(block_stream),
@@ -291,7 +293,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..68af18bade0c 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
@@ -11,11 +11,12 @@
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 +141,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 +287,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(chunk_data),
@@ -301,7 +303,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(uuid4().bytes)
await self.service.stage_block(
block_id,
len(block_stream),
@@ -312,7 +314,7 @@ async def _upload_substream_block(self, index, block_stream):
)
finally:
block_stream.close()
- return block_id
+ return index, block_id
class PageBlobChunkUploader(_ChunkUploader):