This is an automated email from the ASF dual-hosted git repository.

davsclaus 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 9d34c02e4dea CAMEL-26583: camel-azure-storage-datalake - apply ranges 
to streaming downloads (#29156)
9d34c02e4dea is described below

commit 9d34c02e4dea4c63a4a8d00781325fd458af9f4f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 4 19:53:40 2026 +0100

    CAMEL-26583: camel-azure-storage-datalake - apply ranges to streaming 
downloads (#29156)
    
    Streaming downloads of the camel-azure-storage-datalake getFile operation 
now honour the requested file range and request conditions, instead of always 
reading the whole file. Upgrade guide and docs updated.
    
    AI-generated by Codex via OSS Helper on behalf of @oscerd.
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Co-authored-by: OpenAI Codex <[email protected]>
---
 .../docs/azure-storage-datalake-component.adoc     |   8 ++
 .../docs/azure-storage-datalake-component.adoc     |   8 ++
 .../datalake/client/DataLakeFileClientWrapper.java |   7 +
 .../operations/DataLakeFileOperations.java         |   6 +-
 .../operations/DataLakeFileOperationTest.java      | 150 +++++++++++++++------
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 6 files changed, 150 insertions(+), 40 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-storage-datalake-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-storage-datalake-component.adoc
index 526f4fbcbad5..a21964ee4631 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-storage-datalake-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-storage-datalake-component.adoc
@@ -414,6 +414,14 @@ from("direct:start")
 
 -  `getFile`
 
+The `CamelAzureStorageDataLakeFileRange` header selects the byte range to 
read. Otherwise, set both
+`fileOffset` and `dataCount` on the endpoint to select a range. This applies 
whether the operation
+returns an `InputStream` or writes to an `OutputStream` supplied as the 
message body.
+The `CamelAzureStorageDataLakeRequestCondition` header applies request 
conditions, such as an ETag
+or lease, to both download paths, including streaming reads without a range.
+Consumers without `fileDir` also use the streaming path and honor the endpoint 
`fileOffset` and
+`dataCount` options. Remove these options to consume the entire file.
+
 This can be done in two ways. We can either set an `OutputStream` in the 
exchange body:
 
 ._Java-only: sets an `OutputStream` for the file content to be written to_
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/main/docs/azure-storage-datalake-component.adoc
 
b/components/camel-azure/camel-azure-storage-datalake/src/main/docs/azure-storage-datalake-component.adoc
index 526f4fbcbad5..a21964ee4631 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/main/docs/azure-storage-datalake-component.adoc
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/main/docs/azure-storage-datalake-component.adoc
@@ -414,6 +414,14 @@ from("direct:start")
 
 -  `getFile`
 
+The `CamelAzureStorageDataLakeFileRange` header selects the byte range to 
read. Otherwise, set both
+`fileOffset` and `dataCount` on the endpoint to select a range. This applies 
whether the operation
+returns an `InputStream` or writes to an `OutputStream` supplied as the 
message body.
+The `CamelAzureStorageDataLakeRequestCondition` header applies request 
conditions, such as an ETag
+or lease, to both download paths, including streaming reads without a range.
+Consumers without `fileDir` also use the streaming path and honor the endpoint 
`fileOffset` and
+`dataCount` options. Remove these options to consume the entire file.
+
 This can be done in two ways. We can either set an `OutputStream` in the 
exchange body:
 
 ._Java-only: sets an `OutputStream` for the file content to be written to_
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/client/DataLakeFileClientWrapper.java
 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/client/DataLakeFileClientWrapper.java
index d5f888597a39..99b15d07931b 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/client/DataLakeFileClientWrapper.java
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/client/DataLakeFileClientWrapper.java
@@ -35,6 +35,7 @@ import com.azure.storage.file.datalake.models.PathHttpHeaders;
 import com.azure.storage.file.datalake.models.PathInfo;
 import com.azure.storage.file.datalake.models.PathProperties;
 import com.azure.storage.file.datalake.options.DataLakeFileAppendOptions;
+import com.azure.storage.file.datalake.options.DataLakeFileInputStreamOptions;
 import com.azure.storage.file.datalake.options.FileParallelUploadOptions;
 import com.azure.storage.file.datalake.options.FileQueryOptions;
 import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues;
@@ -66,6 +67,12 @@ public class DataLakeFileClientWrapper {
         return client.openInputStream().getInputStream();
     }
 
+    public InputStream openInputStream(final FileRange range, final 
DataLakeRequestConditions requestConditions) {
+        return client
+                .openInputStream(new 
DataLakeFileInputStreamOptions().setRange(range).setRequestConditions(requestConditions))
+                .getInputStream();
+    }
+
     public Response<InputStream> openQueryInputStreamWithResponse(final 
FileQueryOptions queryOptions) {
         return client.openQueryInputStreamWithResponse(queryOptions);
     }
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
index 6f4c13984fe9..117e18a85815 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/main/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperations.java
@@ -73,12 +73,14 @@ public class DataLakeFileOperations {
         }
 
         final DataLakeFileClientWrapper fileClientWrapper = 
getFileClientWrapper(exchange);
+        final FileRange fileRange = configurationProxy.getFileRange(exchange);
         if (outputStream == null) {
-            InputStream fileInputStream = fileClientWrapper.openInputStream();
+            final DataLakeRequestConditions requestConditions = 
configurationProxy.getDataLakeRequestConditions(exchange);
+            InputStream fileInputStream = fileRange == null && 
requestConditions == null
+                    ? fileClientWrapper.openInputStream() : 
fileClientWrapper.openInputStream(fileRange, requestConditions);
             return new DataLakeOperationResponse(fileInputStream);
         }
 
-        final FileRange fileRange = configurationProxy.getFileRange(exchange);
         final FileCommonRequestOptions fileCommonRequestOptions = 
getCommonRequestOptions(exchange);
 
         final DownloadRetryOptions downloadRetryOptions = 
getDownloadRetryOptions(configurationProxy);
diff --git 
a/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
 
b/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
index 6aec092b50d3..d41945dd20b7 100644
--- 
a/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
+++ 
b/components/camel-azure/camel-azure-storage-datalake/src/test/java/org/apache/camel/component/azure/storage/datalake/operations/DataLakeFileOperationTest.java
@@ -27,8 +27,12 @@ import java.time.OffsetDateTime;
 import com.azure.core.http.HttpHeaders;
 import com.azure.core.http.rest.ResponseBase;
 import com.azure.storage.file.datalake.DataLakeFileClient;
+import 
com.azure.storage.file.datalake.models.DataLakeFileOpenInputStreamResult;
+import com.azure.storage.file.datalake.models.DataLakeRequestConditions;
+import com.azure.storage.file.datalake.models.FileRange;
 import com.azure.storage.file.datalake.models.PathInfo;
 import com.azure.storage.file.datalake.models.PathProperties;
+import com.azure.storage.file.datalake.options.DataLakeFileInputStreamOptions;
 import org.apache.camel.Exchange;
 import org.apache.camel.component.azure.storage.datalake.DataLakeConfiguration;
 import org.apache.camel.component.azure.storage.datalake.DataLakeConstants;
@@ -39,11 +43,14 @@ 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;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -97,6 +104,62 @@ public class DataLakeFileOperationTest extends 
CamelTestSupport {
         assertNotNull(secondResponse.getHeaders());
     }
 
+    @Test
+    void testDownloadToFileRejectsPathTraversalName() {
+        // a file name is influenced by whoever writes to the file system and 
may contain ../ segments;
+        // the download must stay within the configured fileDir
+        configuration.setFileDir("/tmp/camel-azure-datalake-download");
+        when(client.getFileName()).thenReturn("../escaped/PROOF_PWNED");
+
+        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
+        final Exchange exchange = new DefaultExchange(context);
+
+        assertThrows(IllegalArgumentException.class, () -> 
operations.downloadToFile(exchange));
+    }
+
+    @Test
+    void testUploadFile() throws Exception {
+        final OffsetDateTime time = OffsetDateTime.now();
+        final PathInfo pathInfo = new PathInfo("testTag", time);
+        final HttpHeaders httpHeaders = new HttpHeaders();
+        when(client.uploadWithResponse(any(), any()))
+                .thenReturn(new ResponseBase<>(null, 200, httpHeaders, 
pathInfo, null));
+
+        final Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody(new 
ByteArrayInputStream("testing".getBytes(Charset.defaultCharset())));
+
+        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
+        final DataLakeOperationResponse response = operations.upload(exchange);
+
+        assertNotNull(response);
+        assertTrue((boolean) response.getBody());
+        assertNotNull(response.getHeaders());
+        assertEquals("testTag", 
response.getHeaders().get(DataLakeConstants.E_TAG));
+        assertEquals(time, 
response.getHeaders().get(DataLakeConstants.LAST_MODIFIED));
+    }
+
+    @Test
+    void testServiceClientOverride() throws Exception {
+        final HttpHeaders httpHeaders = new HttpHeaders();
+        final byte[] testing = "testing".getBytes(Charset.defaultCharset());
+        when(alternateClient.appendWithResponse(any(), anyLong(), anyLong(), 
any(), any(), any()))
+                .thenReturn(new ResponseBase<>(null, 200, httpHeaders, null, 
null));
+        final PathProperties properties = mock(PathProperties.class);
+        
when(properties.getFileSize()).thenReturn(Long.valueOf(testing.length));
+        when(alternateClient.getProperties()).thenReturn(properties);
+
+        final Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setHeader(DataLakeConstants.FILE_CLIENT, 
alternateClient);
+        exchange.getIn().setBody(new ByteArrayInputStream(testing));
+
+        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
+        final DataLakeOperationResponse response = 
operations.appendToFile(exchange);
+
+        assertNotNull(response);
+        assertTrue((boolean) response.getBody());
+        assertNotNull(response.getHeaders());
+    }
+
     @Test
     void testFlushUsesAbsoluteHeaderPosition() {
         when(client.flushWithResponse(anyLong(), any(), any(), any(), any(), 
any()))
@@ -142,58 +205,69 @@ public class DataLakeFileOperationTest extends 
CamelTestSupport {
     }
 
     @Test
-    void testDownloadToFileRejectsPathTraversalName() {
-        // a file name is influenced by whoever writes to the file system and 
may contain ../ segments;
-        // the download must stay within the configured fileDir
-        configuration.setFileDir("/tmp/camel-azure-datalake-download");
-        when(client.getFileName()).thenReturn("../escaped/PROOF_PWNED");
-
-        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
+    void testGetFileUsesHeaderRangeForInputStream() throws IOException {
+        configuration.setFileOffset(2L);
+        configuration.setDataCount(3L);
         final Exchange exchange = new DefaultExchange(context);
-
-        assertThrows(IllegalArgumentException.class, () -> 
operations.downloadToFile(exchange));
+        exchange.getIn().setHeader(DataLakeConstants.FILE_RANGE, new 
FileRange(4, 2L));
+        assertInputStreamRange(exchange, 4, 2L);
     }
 
     @Test
-    void testUploadFile() throws Exception {
-        final OffsetDateTime time = OffsetDateTime.now();
-        final PathInfo pathInfo = new PathInfo("testTag", time);
-        final HttpHeaders httpHeaders = new HttpHeaders();
-        when(client.uploadWithResponse(any(), any()))
-                .thenReturn(new ResponseBase<>(null, 200, httpHeaders, 
pathInfo, null));
+    void testGetFileUsesConfiguredRangeForInputStream() throws IOException {
+        configuration.setFileOffset(2L);
+        configuration.setDataCount(3L);
+        assertInputStreamRange(new DefaultExchange(context), 2, 3L);
+    }
 
+    private void assertInputStreamRange(Exchange exchange, long offset, long 
count) throws IOException {
+        final DataLakeFileInputStreamOptions options = 
inputStreamOptions(exchange);
+        assertEquals(offset, options.getRange().getOffset());
+        assertEquals(count, options.getRange().getCount());
+    }
+
+    @Test
+    void testGetFileAppliesConditionsWithHeaderRange() throws IOException {
+        configuration.setFileOffset(2L);
+        configuration.setDataCount(3L);
         final Exchange exchange = new DefaultExchange(context);
-        exchange.getIn().setBody(new 
ByteArrayInputStream("testing".getBytes(Charset.defaultCharset())));
+        final DataLakeRequestConditions conditions = new 
DataLakeRequestConditions().setIfMatch("etag");
+        exchange.getIn().setHeader(DataLakeConstants.FILE_RANGE, new 
FileRange(4, 2L));
+        
exchange.getIn().setHeader(DataLakeConstants.DATALAKE_REQUEST_CONDITION, 
conditions);
 
-        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
-        final DataLakeOperationResponse response = operations.upload(exchange);
+        final DataLakeFileInputStreamOptions options = 
inputStreamOptions(exchange);
 
-        assertNotNull(response);
-        assertTrue((boolean) response.getBody());
-        assertNotNull(response.getHeaders());
-        assertEquals("testTag", 
response.getHeaders().get(DataLakeConstants.E_TAG));
-        assertEquals(time, 
response.getHeaders().get(DataLakeConstants.LAST_MODIFIED));
+        assertEquals(4, options.getRange().getOffset());
+        assertEquals(2L, options.getRange().getCount());
+        assertSame(conditions, options.getRequestConditions());
     }
 
     @Test
-    void testServiceClientOverride() throws Exception {
-        final HttpHeaders httpHeaders = new HttpHeaders();
-        final byte[] testing = "testing".getBytes(Charset.defaultCharset());
-        when(alternateClient.appendWithResponse(any(), anyLong(), anyLong(), 
any(), any(), any()))
-                .thenReturn(new ResponseBase<>(null, 200, httpHeaders, null, 
null));
-        final PathProperties properties = mock(PathProperties.class);
-        
when(properties.getFileSize()).thenReturn(Long.valueOf(testing.length));
-        when(alternateClient.getProperties()).thenReturn(properties);
-
+    void testGetFileAppliesConditionsWithoutRange() throws IOException {
         final Exchange exchange = new DefaultExchange(context);
+        final DataLakeRequestConditions conditions = new 
DataLakeRequestConditions().setLeaseId("lease");
+        
exchange.getIn().setHeader(DataLakeConstants.DATALAKE_REQUEST_CONDITION, 
conditions);
+
+        final DataLakeFileInputStreamOptions options = 
inputStreamOptions(exchange);
+
+        assertNull(options.getRange());
+        assertSame(conditions, options.getRequestConditions());
+    }
+
+    private DataLakeFileInputStreamOptions inputStreamOptions(Exchange 
exchange) throws IOException {
+        final InputStream stream = new 
ByteArrayInputStream("range".getBytes(Charset.defaultCharset()));
+        final DataLakeFileOpenInputStreamResult result = 
mock(DataLakeFileOpenInputStreamResult.class);
+        when(result.getInputStream()).thenReturn(stream);
+        
when(alternateClient.openInputStream(any(DataLakeFileInputStreamOptions.class))).thenReturn(result);
         exchange.getIn().setHeader(DataLakeConstants.FILE_CLIENT, 
alternateClient);
-        exchange.getIn().setBody(new ByteArrayInputStream(testing));
 
-        final DataLakeFileOperations operations = new 
DataLakeFileOperations(configuration, client);
-        final DataLakeOperationResponse response = 
operations.appendToFile(exchange);
+        final DataLakeOperationResponse response = new 
DataLakeFileOperations(configuration, client).getFile(exchange);
 
-        assertNotNull(response);
-        assertTrue((boolean) response.getBody());
-        assertNotNull(response.getHeaders());
+        assertEquals(stream, response.getBody());
+        final ArgumentCaptor<DataLakeFileInputStreamOptions> options
+                = 
ArgumentCaptor.forClass(DataLakeFileInputStreamOptions.class);
+        verify(alternateClient).openInputStream(options.capture());
+        return options.getValue();
     }
+
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index b05c3cc968c5..fc8c275c33b7 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -4654,6 +4654,17 @@ lists the file). A file deleted after the listing is 
skipped as well; before, it
 Before, any other failure failed the poll, and as every poll lists the file 
system again, one such file stopped the
 consumer from consuming any file. The poll still fails when none of the listed 
files could be got.
 
+=== camel-azure-storage-datalake - streaming downloads honor ranges and 
request conditions
+
+The `getFile` operation now honors the `CamelAzureStorageDataLakeFileRange` 
header, or the endpoint
+`fileOffset` and `dataCount` options, when it returns an `InputStream`. 
Previously, these settings
+only applied when the message body supplied an `OutputStream`; streaming reads 
returned the entire file.
+This also affects consumers without `fileDir`: they now deliver the configured 
byte range instead of the whole file.
+Remove the range header and options if your route needs the entire file.
+
+The streaming path also honors the `CamelAzureStorageDataLakeRequestCondition` 
header, whether or not a range
+is set. An ETag mismatch or an invalid lease can now fail a streaming read 
that previously ignored the condition.
+
 === camel-chatscript - the first message of a conversation is sent, and the 
headers are kept
 
 The first message of a ChatScript endpoint, and every message with 
`resetChat=true`, used to start the conversation

Reply via email to