This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new e04ba4890f57 CAMEL-23342: camel-azure-storage-blob - add
listBlobVersions container operation (#23554)
e04ba4890f57 is described below
commit e04ba4890f57319ce5d7ea2ae7b7aad910c0cf37
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed May 27 15:22:31 2026 +0200
CAMEL-23342: camel-azure-storage-blob - add listBlobVersions container
operation (#23554)
Add a new container-level producer operation that returns one BlobItem per
version of every blob in the container, allowing the full version history of
a blob to be inspected for auditing and compliance scenarios. Requires
versioning to be enabled on the storage account.
The operation reuses the same ListBlobsOptions plumbing as listBlobs (so
prefix, regex and maxResultsPerPage all still apply) and forces
BlobListDetails.setRetrieveVersions(true) before issuing the request. Each
returned BlobItem carries its own versionId and isCurrentVersion flag,
populated by the Azure SDK.
This complements CAMEL-23330, which added the read-path BLOB_VERSION_ID
header for targeting a specific version on getBlob / downloadBlobToFile /
downloadLink, by providing a way to discover the versions in the first
place.
- BlobOperationsDefinition: new listBlobVersions enum value
(container-level)
- BlobContainerOperations: new listBlobVersions(Exchange) reusing the
listBlob
options/regex filter and forcing retrieveVersions=true
- BlobProducer: switch case wired to the new operation
- Unit tests verifying retrieveVersions is set on the request and that the
regex filter still narrows the result
- Documentation update with example and upgrade guide entry
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
.../catalog/components/azure-storage-blob.json | 2 +-
.../azure/storage/blob/azure-storage-blob.json | 2 +-
.../main/docs/azure-storage-blob-component.adoc | 24 +++++++++
.../storage/blob/BlobOperationsDefinition.java | 7 +++
.../component/azure/storage/blob/BlobProducer.java | 3 ++
.../blob/operations/BlobContainerOperations.java | 20 ++++++++
.../operations/BlobContainerOperationsTest.java | 60 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 8 +++
8 files changed, 124 insertions(+), 2 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json
index bdadf4519914..fc39a1e1d0f3 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json
@@ -77,7 +77,7 @@
"sourceBlobAccessKey": { "index": 50, "kind": "property", "displayName":
"Source Blob Access Key", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.azure.storage.blob.BlobConfiguration",
"configurationField": "configuration", "description": "Source Blob Access Key:
for copyblob operation, sadly, [...]
},
"headers": {
- "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType":
"org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition",
"enum": [ "listBlobContainers", "findBlobsByTags", "createBlobContainer",
"deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob",
"downloadBlobToFile", "downloadLink", "uploadBlockBlob",
"uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlock [...]
+ "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType":
"org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition",
"enum": [ "listBlobContainers", "findBlobsByTags", "createBlobContainer",
"deleteBlobContainer", "listBlobs", "listBlobVersions", "getBlob",
"deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob",
"uploadBlockBlobChunked", "stageBlockBlobLis [...]
"CamelAzureStorageBlobHttpHeaders": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "BlobHttpHeaders", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "(uploadBlockBlob,
commitBlobBlockList, createAppendBlob, createPageBlob) Additional parameters
for a set of operations.", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_HTTP_HEA [...]
"CamelAzureStorageBlobETag": { "index": 2, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The E Tag of the blob", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#E_TAG" },
"CamelAzureStorageBlobCreationTime": { "index": 3, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Creation time of the
blob.", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#CREATION_TIME" },
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json
b/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json
index bdadf4519914..fc39a1e1d0f3 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json
+++
b/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json
@@ -77,7 +77,7 @@
"sourceBlobAccessKey": { "index": 50, "kind": "property", "displayName":
"Source Blob Access Key", "group": "security", "label": "security", "required":
false, "type": "string", "javaType": "java.lang.String", "deprecated": false,
"autowired": false, "secret": true, "security": "secret", "configurationClass":
"org.apache.camel.component.azure.storage.blob.BlobConfiguration",
"configurationField": "configuration", "description": "Source Blob Access Key:
for copyblob operation, sadly, [...]
},
"headers": {
- "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType":
"org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition",
"enum": [ "listBlobContainers", "findBlobsByTags", "createBlobContainer",
"deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob",
"downloadBlobToFile", "downloadLink", "uploadBlockBlob",
"uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlock [...]
+ "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType":
"org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition",
"enum": [ "listBlobContainers", "findBlobsByTags", "createBlobContainer",
"deleteBlobContainer", "listBlobs", "listBlobVersions", "getBlob",
"deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob",
"uploadBlockBlobChunked", "stageBlockBlobLis [...]
"CamelAzureStorageBlobHttpHeaders": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "BlobHttpHeaders", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "(uploadBlockBlob,
commitBlobBlockList, createAppendBlob, createPageBlob) Additional parameters
for a set of operations.", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_HTTP_HEA [...]
"CamelAzureStorageBlobETag": { "index": 2, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The E Tag of the blob", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#E_TAG" },
"CamelAzureStorageBlobCreationTime": { "index": 3, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Creation time of the
blob.", "constantName":
"org.apache.camel.component.azure.storage.blob.BlobConstants#CREATION_TIME" },
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc
b/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc
index d31288b14e8f..1f7621f0f247 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc
+++
b/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc
@@ -137,6 +137,7 @@ For these operations, `accountName` and `containerName` are
*required*.
|`createBlobContainer` | Create a new container within a storage account. If a
container with the same name already exists, the producer will ignore it.
|`deleteBlobContainer` | Delete the specified container in the storage
account. If the container doesn't exist, the operation fails.
|`listBlobs`| Returns a list of blobs in this container, with folder
structures flattened.
+|`listBlobVersions`| Returns a list of blobs and their versions in this
container. Each `BlobItem` in the result carries its own `versionId` and
`isCurrentVersion` flag, allowing the full version history of every blob to be
inspected. Requires versioning to be enabled on the storage account. Honours
the same `prefix`, `regex` and `maxResultsPerPage` filters as `listBlobs`.
|===
*Operations on the blob level*
@@ -408,6 +409,29 @@ from("direct:start")
--------------------------------------------------------------------------------
+- `listBlobVersions`:
+
+Returns every version of every blob in the container. Versioning must be
enabled on the storage
+account. Each `BlobItem` in the result carries its own `versionId` and
`isCurrentVersion` flag.
+The `prefix` and `regex` options can be used to narrow the result down to a
single blob name.
+
+[source,java]
+--------------------------------------------------------------------------------
+from("direct:start")
+ .setHeader(BlobConstants.PREFIX, constant("invoice.pdf"))
+
.to("azure-storage-blob://camelazure/container1?operation=listBlobVersions&serviceClient=#client")
+ .process(exchange -> {
+ @SuppressWarnings("unchecked")
+ List<BlobItem> versions = exchange.getMessage().getBody(List.class);
+ for (BlobItem v : versions) {
+ System.out.printf("%s versionId=%s isCurrent=%s%n",
+ v.getName(), v.getVersionId(), v.isCurrentVersion());
+ }
+ })
+ .to("mock:result");
+--------------------------------------------------------------------------------
+
+
- `getBlob`:
We can either set an `outputStream` in the exchange body and write the data to
it. E.g.:
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java
index 0d5259d06f97..ea0462c15e08 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java
+++
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java
@@ -47,6 +47,13 @@ public enum BlobOperationsDefinition {
* Returns a list of blobs in this container, with folder structures
flattened.
*/
listBlobs,
+ /**
+ * Returns the list of blobs and their versions in this container. Each
{@code BlobItem} in the result carries its
+ * own {@code versionId} and {@code isCurrentVersion} flag, allowing the
full version history of every blob to be
+ * inspected. Requires versioning to be enabled on the storage account.
Honours the same {@code prefix},
+ * {@code regex} and {@code maxResultsPerPage} filters as {@link
#listBlobs}.
+ */
+ listBlobVersions,
// Operations on the blob level
//
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java
index 7a3a49b9a7e8..dbffadc652b7 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java
+++
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java
@@ -74,6 +74,9 @@ public class BlobProducer extends DefaultProducer {
case listBlobs:
setResponse(exchange,
getContainerOperations(exchange).listBlobs(exchange));
break;
+ case listBlobVersions:
+ setResponse(exchange,
getContainerOperations(exchange).listBlobVersions(exchange));
+ break;
// blob operations
case getBlob:
setResponse(exchange,
getBlobOperations(exchange).getBlob(exchange));
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperations.java
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperations.java
index 463ebffd0d08..44f0c5516da4 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperations.java
+++
b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperations.java
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import com.azure.storage.blob.models.BlobItem;
+import com.azure.storage.blob.models.BlobListDetails;
import com.azure.storage.blob.models.BlobRequestConditions;
import com.azure.storage.blob.models.ListBlobsOptions;
import com.azure.storage.blob.models.PublicAccessType;
@@ -61,6 +62,25 @@ public class BlobContainerOperations {
return BlobOperationResponse.create(filteredBlobs);
}
+ public BlobOperationResponse listBlobVersions(final Exchange exchange) {
+ final ListBlobsOptions listBlobOptions =
configurationProxy.getListBlobOptions(exchange);
+ final BlobListDetails details = listBlobOptions.getDetails() != null
+ ? listBlobOptions.getDetails() : new BlobListDetails();
+ details.setRetrieveVersions(true);
+ listBlobOptions.setDetails(details);
+
+ final Duration timeout = configurationProxy.getTimeout(exchange);
+ final String regex = configurationProxy.getRegex(exchange);
+ List<BlobItem> blobs = client.listBlobs(listBlobOptions, timeout);
+ if (ObjectHelper.isEmpty(regex)) {
+ return BlobOperationResponse.create(blobs);
+ }
+ List<BlobItem> filteredBlobs = blobs.stream()
+ .filter(x -> x.getName().matches(regex))
+ .collect(Collectors.toCollection(LinkedList<BlobItem>::new));
+ return BlobOperationResponse.create(filteredBlobs);
+ }
+
public BlobOperationResponse createContainer(final Exchange exchange) {
final Map<String, String> metadata =
configurationProxy.getMetadata(exchange);
final PublicAccessType publicAccessType =
configurationProxy.getPublicAccessType(exchange);
diff --git
a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperationsTest.java
b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperationsTest.java
index cdfe3173a61d..cd2ece781d58 100644
---
a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperationsTest.java
+++
b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobContainerOperationsTest.java
@@ -21,6 +21,7 @@ import java.util.List;
import com.azure.core.http.HttpHeaders;
import com.azure.storage.blob.models.BlobItem;
+import com.azure.storage.blob.models.ListBlobsOptions;
import org.apache.camel.component.azure.storage.blob.BlobConfiguration;
import org.apache.camel.component.azure.storage.blob.BlobConstants;
import
org.apache.camel.component.azure.storage.blob.client.BlobContainerClientWrapper;
@@ -28,6 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -35,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -118,6 +121,53 @@ class BlobContainerOperationsTest {
assertTrue(items.contains("invoice5.pdf"));
}
+ @Test
+ void testListBlobVersions() {
+ when(client.listBlobs(any(),
any())).thenReturn(listBlobVersionsMock());
+
+ final BlobContainerOperations blobContainerOperations = new
BlobContainerOperations(configuration, client);
+ final BlobOperationResponse response =
blobContainerOperations.listBlobVersions(null);
+
+ assertNotNull(response);
+
+ // Verify the operation forced retrieveVersions=true on the request
+ final ArgumentCaptor<ListBlobsOptions> optionsCaptor =
ArgumentCaptor.forClass(ListBlobsOptions.class);
+ verify(client).listBlobs(optionsCaptor.capture(), any());
+ assertNotNull(optionsCaptor.getValue().getDetails());
+
assertTrue(optionsCaptor.getValue().getDetails().getRetrieveVersions());
+
+ @SuppressWarnings("unchecked")
+ final List<BlobItem> body = (List<BlobItem>) response.getBody();
+ assertEquals(3, body.size());
+ assertEquals("item-1", body.get(0).getName());
+ assertEquals("v1", body.get(0).getVersionId());
+ assertEquals("item-1", body.get(1).getName());
+ assertEquals("v2", body.get(1).getVersionId());
+ assertTrue(body.get(1).isCurrentVersion());
+ assertEquals("item-2", body.get(2).getName());
+ assertEquals("v1", body.get(2).getVersionId());
+ }
+
+ @Test
+ void testListBlobVersionsWithRegex() {
+ BlobConfiguration myConfiguration = new BlobConfiguration();
+ myConfiguration.setAccountName("cameldev");
+ myConfiguration.setContainerName("awesome2");
+ myConfiguration.setRegex("item-1");
+
+ when(client.listBlobs(any(),
any())).thenReturn(listBlobVersionsMock());
+
+ final BlobContainerOperations blobContainerOperations = new
BlobContainerOperations(myConfiguration, client);
+ final BlobOperationResponse response =
blobContainerOperations.listBlobVersions(null);
+
+ assertNotNull(response);
+
+ @SuppressWarnings("unchecked")
+ final List<BlobItem> body = (List<BlobItem>) response.getBody();
+ assertEquals(2, body.size());
+ assertTrue(body.stream().allMatch(b -> b.getName().equals("item-1")));
+ }
+
private HttpHeaders createContainerMock() {
final HttpHeaders httpHeaders = new HttpHeaders();
@@ -137,6 +187,16 @@ class BlobContainerOperationsTest {
return httpHeaders;
}
+ private List<BlobItem> listBlobVersionsMock() {
+ final List<BlobItem> items = new LinkedList<>();
+
+ items.add(new
BlobItem().setName("item-1").setVersionId("v1").setCurrentVersion(false).setDeleted(false));
+ items.add(new
BlobItem().setName("item-1").setVersionId("v2").setCurrentVersion(true).setDeleted(false));
+ items.add(new
BlobItem().setName("item-2").setVersionId("v1").setCurrentVersion(true).setDeleted(false));
+
+ return items;
+ }
+
private List<BlobItem> listBlobsMock() {
final List<BlobItem> items = new LinkedList<>();
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 20268bed3693..7b264df8c03b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -1629,3 +1629,11 @@ those sites and have not been added; DoS hardening on
the JMS path must be confi
level (for example Artemis `deserializationAllowList`, ActiveMQ Classic
`SERIALIZABLE_PACKAGES`) or via
`-Djdk.serialFilter`.
+=== camel-azure-storage-blob
+
+A new container-level operation `listBlobVersions` has been added. It returns
one `BlobItem`
+per version of every blob in the container, each carrying its own `versionId`
and
+`isCurrentVersion` flag, so the full version history can be inspected.
Requires versioning to be
+enabled on the storage account. The same `prefix`, `regex` and
`maxResultsPerPage` filters as
+`listBlobs` are honoured.
+