Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/storage/azure-storage-blob-batch/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
### Breaking Changes

### Bugs Fixed
- Fixed an issue where `BlobBatchSetBlobAccessTierOptions` would not properly handle blob names with special characters.

### Other Changes
- Added support for container names with special characters when using OneLake.

## 12.28.0 (2025-10-21)

Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azure-storage-blob-batch/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/storage/azure-storage-blob-batch",
"Tag": "java/storage/azure-storage-blob-batch_469f26eff9"
"Tag": "java/storage/azure-storage-blob-batch_1951fdc3fd"
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,14 @@ public final class BlobBatch {
* <!-- end com.azure.storage.blob.batch.BlobBatch.deleteBlob#String-String -->
*
* @param containerName The container of the blob.
* @param blobName The name of the blob.
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
* submitted.
* @throws UnsupportedOperationException If this batch has already added an operation of another type.
*/
public Response<Void> deleteBlob(String containerName, String blobName) {
return deleteBlobHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), null, null);
return deleteBlobHelper(Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)),
null, null);
}

/**
Expand All @@ -149,7 +150,7 @@ public Response<Void> deleteBlob(String containerName, String blobName) {
* <!-- end com.azure.storage.blob.batch.BlobBatch.deleteBlob#String-String-DeleteSnapshotsOptionType-BlobRequestConditions -->
*
* @param containerName The container of the blob.
* @param blobName The name of the blob.
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
* @param deleteOptions Delete options for the blob and its snapshots.
* @param blobRequestConditions Additional access conditions that must be met to allow this operation.
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
Expand All @@ -158,8 +159,8 @@ public Response<Void> deleteBlob(String containerName, String blobName) {
*/
public Response<Void> deleteBlob(String containerName, String blobName, DeleteSnapshotsOptionType deleteOptions,
BlobRequestConditions blobRequestConditions) {
return deleteBlobHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), deleteOptions,
blobRequestConditions);
return deleteBlobHelper(Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)),
deleteOptions, blobRequestConditions);
}

/**
Expand Down Expand Up @@ -227,15 +228,16 @@ private Response<Void> deleteBlobHelper(String urlPath, DeleteSnapshotsOptionTyp
* <!-- end com.azure.storage.blob.batch.BlobBatch.setBlobAccessTier#String-String-AccessTier -->
*
* @param containerName The container of the blob.
* @param blobName The name of the blob.
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
* @param accessTier The tier to set on the blob.
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
* submitted.
* @throws UnsupportedOperationException If this batch has already added an operation of another type.
*/
public Response<Void> setBlobAccessTier(String containerName, String blobName, AccessTier accessTier) {
return setBlobAccessTierHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier,
null, null, null);
return setBlobAccessTierHelper(
Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier, null,
null, null);
}

/**
Expand All @@ -251,7 +253,7 @@ public Response<Void> setBlobAccessTier(String containerName, String blobName, A
* <!-- end com.azure.storage.blob.batch.BlobBatch.setBlobAccessTier#String-String-AccessTier-String -->
*
* @param containerName The container of the blob.
* @param blobName The name of the blob.
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
* @param accessTier The tier to set on the blob.
* @param leaseId The lease ID the active lease on the blob must match.
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
Expand All @@ -260,8 +262,9 @@ public Response<Void> setBlobAccessTier(String containerName, String blobName, A
*/
public Response<Void> setBlobAccessTier(String containerName, String blobName, AccessTier accessTier,
String leaseId) {
return setBlobAccessTierHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier,
null, leaseId, null);
return setBlobAccessTierHelper(
Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier, null,
leaseId, null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public String getBlobName() {
* @return Identifier of the blob to set its access tier.
*/
public String getBlobIdentifier() {
String basePath = blobUrlParts.getBlobContainerName() + "/" + blobUrlParts.getBlobName();
String basePath = Utility.urlEncode(blobUrlParts.getBlobContainerName()) + "/"
+ Utility.urlEncode(blobUrlParts.getBlobName());
String snapshot = blobUrlParts.getSnapshot();
String versionId = blobUrlParts.getVersionId();
if (snapshot != null && versionId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.azure.storage.blob.specialized.BlobClientBase;
import com.azure.storage.blob.specialized.BlockBlobClient;
import com.azure.storage.blob.specialized.PageBlobClient;
import com.azure.storage.common.Utility;
import com.azure.storage.common.sas.AccountSasPermission;
import com.azure.storage.common.sas.AccountSasResourceType;
import com.azure.storage.common.sas.AccountSasService;
Expand Down Expand Up @@ -794,4 +795,157 @@ public void submitBatchWithContainerSasCredentialsError() {
= assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
assertEquals(2, getIterableSize(ex.getBatchExceptions()));
}

// Tests container name encoding for BlobBatch.deleteBlob. Container names with special characters are not supported
// by the service, however, the names should still be encoded.
@Test
public void deleteBlobContainerNameEncoding() {
String containerName = "my container";
String blobName = generateBlobName();

BlobBatch batch = batchClient.getBlobBatch();
Response<Void> response = batch.deleteBlob(containerName, blobName);

assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);

assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
}

// Tests blob name encoding for BlobBatch.deleteBlob.
@Test
public void deleteBlobNameEncoding() {
String containerName = generateContainerName();
String blobName = generateBlobName() + "enc!";
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
containerClient.getBlobClient(blobName).getPageBlobClient().create(0);

BlobBatch batch = batchClient.getBlobBatch();
Response<Void> response = batch.deleteBlob(containerName, blobName);
batchClient.submitBatch(batch);

assertEquals(202, response.getStatusCode());
}

// Tests container name encoding for BlobBatch.setBlobAccessTier. Container names with special characters are not supported
// by the service, however, the names should still be encoded.
@Test
public void setTierContainerNameEncoding() {
String containerName = "my container";
String blobName = generateBlobName();

BlobBatch batch = batchClient.getBlobBatch();
Response<Void> response = batch.setBlobAccessTier(containerName, blobName, AccessTier.HOT);

assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);

assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
}

// Tests blob name encoding for BlobBatch.setBlobAccessTier
@Test
public void setTierBlobNameEncoding() {
String containerName = generateContainerName();
String blobName = generateBlobName() + "enc!";
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
containerClient.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultBinaryData());

BlobBatch batch = batchClient.getBlobBatch();
Response<Void> response = batch.setBlobAccessTier(containerName, blobName, AccessTier.HOT);
batchClient.submitBatch(batch);

assertEquals(200, response.getStatusCode());
}

// Tests container name encoding for BlobBatchSetBlobAccessTierOptions constructor. Container names with special characters are not supported
// by the service, however, the names should still be encoded.
@Test
public void setTierContainerNameEncodingOptionsConstructor() {
String containerName = "my container";
String blobName = generateBlobName();

BlobBatch batch = batchClient.getBlobBatch();
BlobBatchSetBlobAccessTierOptions options
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);
Response<Void> response = batch.setBlobAccessTier(options);

assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);

assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
}

//Tests blob name encoding for BlobBatchSetBlobAccessTierOptions constructor
@Test
public void setTierBlobNameEncodingOptionsConstructor() {
String containerName = generateContainerName();
String blobName = generateBlobName() + "enc!";
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
containerClient.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultBinaryData());

BlobBatch batch = batchClient.getBlobBatch();
BlobBatchSetBlobAccessTierOptions options
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);
Response<Void> response = batch.setBlobAccessTier(options);
batchClient.submitBatch(batch);

assertEquals(200, response.getStatusCode());
String identifier = options.getBlobIdentifier();
assertTrue(identifier.contains(Utility.urlEncode(blobName)));
}

// Tests getters return unencoded names (constructor with separate names)
@Test
public void getBlobNameAndContainerNameOptionsConstructor() {
String containerName = "my container";
String blobName = "my blob";

BlobBatchSetBlobAccessTierOptions options
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);

assertEquals(containerName, options.getBlobContainerName());
assertEquals(blobName, options.getBlobName());

String identifier = options.getBlobIdentifier();
assertTrue(identifier.contains("my%20container"));
assertTrue(identifier.contains("my%20blob"));
}

// Tests getters return unencoded names (constructor with full blob URL)
@Test
public void getBlobNameAndContainerNameUrlConstructor() {
String containerName = "my container";
String blobName = "my blob";
BlockBlobClient blockBlobClient = primaryBlobServiceClient.getBlobContainerClient(containerName)
.getBlobClient(blobName)
.getBlockBlobClient();

BlobBatchSetBlobAccessTierOptions options
= new BlobBatchSetBlobAccessTierOptions(blockBlobClient.getBlobUrl(), AccessTier.HOT);

assertEquals(containerName, options.getBlobContainerName());
assertEquals(blobName, options.getBlobName());

String identifier = options.getBlobIdentifier();
assertTrue(identifier.contains("my%20container"));
assertTrue(identifier.contains("my%20blob"));
}

@Test
public void blobBatchSetBlobAccessTierOptionsHandlesSpecialChars() {
String blobName = "my blob";
String containerName = generateContainerName();

BlobBatch batch = batchClient.getBlobBatch();
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);

BlobClient blobClient1 = containerClient.getBlobClient(blobName);
blobClient1.getBlockBlobClient().upload(DATA.getDefaultBinaryData());

Response<Void> response
= batch.setBlobAccessTier(new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT));
batchClient.submitBatch(batch);
assertEquals(200, response.getStatusCode());
}
}
1 change: 1 addition & 0 deletions sdk/storage/azure-storage-blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Bugs Fixed

### Other Changes
- Added support for container names with special characters when using OneLake.

## 12.32.0 (2025-10-21)

Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azure-storage-blob/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/storage/azure-storage-blob",
"Tag": "java/storage/azure-storage-blob_16534d98da"
"Tag": "java/storage/azure-storage-blob_c018337a13"
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import com.azure.storage.blob.options.FindBlobsOptions;
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.Utility;
import com.azure.storage.common.implementation.SasImplUtils;
import com.azure.storage.common.implementation.StorageImplUtils;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -220,7 +221,7 @@ public String getAccountUrl() {
* @return the URL.
*/
public String getBlobContainerUrl() {
return azureBlobStorage.getUrl() + "/" + containerName;
return azureBlobStorage.getUrl() + "/" + Utility.urlEncode(containerName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import com.azure.storage.blob.options.FindBlobsOptions;
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.Utility;
import com.azure.storage.common.implementation.SasImplUtils;
import com.azure.storage.common.implementation.StorageImplUtils;

Expand Down Expand Up @@ -232,7 +233,7 @@ public String getAccountUrl() {
* @return the URL.
*/
public String getBlobContainerUrl() {
return azureBlobStorage.getUrl() + "/" + containerName;
return azureBlobStorage.getUrl() + "/" + Utility.urlEncode(containerName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,27 +110,40 @@ public BlobUrlParts setHost(String host) {
}

/**
* Gets the container name that will be used as part of the URL path.
* Gets the decoded container name that will be used as part of the URL path.
* <p> Note:
* This value may differ from the original value provided to {@link #setContainerName(String)}
* because the setter and getter do not guarantee round-trip consistency.
* This behavior is intentional to normalize names that may or may not be URL encoded. </p>
*
* @return the container name.
* @return the decoded container name.
*/
public String getBlobContainerName() {
return containerName;
return (containerName == null) ? null : Utility.urlDecode(containerName);
}

/**
* Sets the container name that will be used as part of the URL path.
* <p> Note:
* The setter and getter do not guarantee round-trip consistency.
* This is because container names with special characters may need URL encoding,
* and this method normalizes the input by decoding and then encoding it.
* If the container name contains special characters, it is recommended to URL encode it. </p>
*
* @param containerName The container nme.
* @param containerName The container name. If the container name contains special characters, it should be URL encoded.
* @return the updated BlobUrlParts object.
*/
public BlobUrlParts setContainerName(String containerName) {
this.containerName = containerName;
this.containerName = Utility.urlEncode(Utility.urlDecode(containerName));
return this;
}

/**
* Decodes and gets the blob name that will be used as part of the URL path.
* Gets the decoded blob name that will be used as part of the URL path.
* <p> Note:
* This value may differ from the original value provided to {@link #setBlobName(String)}
* because the setter and getter do not guarantee round-trip consistency.
* This behavior is intentional to normalize names that may or may not be URL encoded. </p>
*
* @return the decoded blob name.
*/
Expand All @@ -140,9 +153,13 @@ public String getBlobName() {

/**
* Sets the blob name that will be used as part of the URL path.
* <p> Note:
* The setter and getter do not guarantee round-trip consistency.
* This is because blob names with special characters may need URL encoding,
* and this method normalizes the input by decoding and then encoding it.
* If the blob name contains special characters, it is recommended to URL encode it. </p>
*
* @param blobName The blob name. If the blob name contains special characters, pass in the url encoded version
* of the blob name.
* @param blobName The blob name. If the blob name contains special characters, it should be URL encoded.
* @return the updated BlobUrlParts object.
*/
public BlobUrlParts setBlobName(String blobName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ public String getAccountUrl() {
* @return the URL.
*/
public String getBlobUrl() {
String blobUrl = azureBlobStorage.getUrl() + "/" + containerName + "/" + Utility.urlEncode(blobName);
String blobUrl
= azureBlobStorage.getUrl() + "/" + Utility.urlEncode(containerName) + "/" + Utility.urlEncode(blobName);
if (this.isSnapshot()) {
blobUrl = Utility.appendQueryParameter(blobUrl, "snapshot", getSnapshotId());
}
Expand Down
Loading
Loading