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):