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 c9bb20b2fb2b CAMEL-25726: camel-azure-files - create the file with the 
length of the body, not a stale CamelFileLength header (#28034)
c9bb20b2fb2b is described below

commit c9bb20b2fb2bc99719350778f133c1389a7335ea
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 18 21:00:44 2026 +0530

    CAMEL-25726: camel-azure-files - create the file with the length of the 
body, not a stale CamelFileLength header (#28034)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/azure-files-component.adoc  |   6 +
 components/camel-azure/camel-azure-files/pom.xml   |   6 +
 .../src/main/docs/azure-files-component.adoc       |   6 +
 .../component/file/azure/FilesOperations.java      |  23 +++-
 .../file/azure/FilesOperationsStoreLengthTest.java | 129 +++++++++++++++++++++
 5 files changed, 164 insertions(+), 6 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-files-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-files-component.adoc
index 2f5e97b7cf23..05fa8088add4 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-files-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-files-component.adoc
@@ -184,6 +184,12 @@ and then rewritten with body content. Any inconsistency 
between
 declared file length and stream length results in a corrupted
 remote file.
 
+The size is the length of the body when it is known without reading the
+body (a file, a `byte[]`, a cached stream). The `CamelFileLength` header is
+only used for a stream whose length is not known otherwise: a file consumer
+sets this header, and it is not updated when the route changes the body.
+Any other body is converted to `byte[]`.
+
 ==== Limitations
 
 The underlying Azure Files service does not allow growing files. The file
diff --git a/components/camel-azure/camel-azure-files/pom.xml 
b/components/camel-azure/camel-azure-files/pom.xml
index 5fdde7982b03..ec9e67ac37c8 100644
--- a/components/camel-azure/camel-azure-files/pom.xml
+++ b/components/camel-azure/camel-azure-files/pom.xml
@@ -57,6 +57,12 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/components/camel-azure/camel-azure-files/src/main/docs/azure-files-component.adoc
 
b/components/camel-azure/camel-azure-files/src/main/docs/azure-files-component.adoc
index 2f5e97b7cf23..05fa8088add4 100644
--- 
a/components/camel-azure/camel-azure-files/src/main/docs/azure-files-component.adoc
+++ 
b/components/camel-azure/camel-azure-files/src/main/docs/azure-files-component.adoc
@@ -184,6 +184,12 @@ and then rewritten with body content. Any inconsistency 
between
 declared file length and stream length results in a corrupted
 remote file.
 
+The size is the length of the body when it is known without reading the
+body (a file, a `byte[]`, a cached stream). The `CamelFileLength` header is
+only used for a stream whose length is not known otherwise: a file consumer
+sets this header, and it is not updated when the route changes the body.
+Any other body is converted to `byte[]`.
+
 ==== Limitations
 
 The underlying Azure Files service does not allow growing files. The file
diff --git 
a/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
 
b/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
index 4a64d43deff6..2ea3064b12a4 100644
--- 
a/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
+++ 
b/components/camel-azure/camel-azure-files/src/main/java/org/apache/camel/component/file/azure/FilesOperations.java
@@ -52,6 +52,7 @@ import org.apache.camel.component.file.GenericFileHelper;
 import org.apache.camel.component.file.GenericFileOperationFailedException;
 import org.apache.camel.component.file.remote.RemoteFile;
 import org.apache.camel.component.file.remote.RemoteFileConfiguration;
+import org.apache.camel.support.PayloadHelper;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.ObjectHelper;
@@ -472,7 +473,7 @@ public class FilesOperations extends NormalizedOperations {
         }
 
         InputStream is = null;
-        int length = 0;
+        long length = 0;
         if (exchange.getIn().getBody() == null) {
             // Do an explicit test for a null body and decide what to do
             if (endpoint.isAllowNullBody()) {
@@ -485,12 +486,22 @@ public class FilesOperations extends NormalizedOperations 
{
 
         try {
             if (is == null) {
-                var knownLength = 
exchange.getIn().getHeader(Exchange.FILE_LENGTH, Long.class);
-                if (knownLength != null) {
+                // the file is created with its final size before the content 
is written, so the size must be the
+                // length of the body. The CamelFileLength header can be stale 
once the route changed the body (a
+                // shorter body leaves zero bytes at the end of the file), so 
it is only used for a stream body
+                // whose length cannot be known otherwise.
+                Object body = exchange.getIn().getBody();
+                Long knownLength = 
exchange.getIn().getHeader(Exchange.FILE_LENGTH, Long.class);
+                length = PayloadHelper.getBodyLength(exchange.getIn());
+                if (length >= 0) {
                     is = exchange.getIn().getMandatoryBody(InputStream.class);
-                    length = knownLength.intValue();
+                } else if (body instanceof InputStream stream && knownLength 
!= null) {
+                    is = stream;
+                    length = knownLength;
                 } else {
-                    log.warn("No file length header, so converting body to 
byte[].  It might be memory intensive.");
+                    if (body instanceof InputStream) {
+                        log.warn("No file length header, so converting body to 
byte[].  It might be memory intensive.");
+                    }
                     var bytes = 
exchange.getIn().getMandatoryBody(byte[].class);
                     length = bytes.length;
                     is = new ByteArrayInputStream(bytes);
@@ -551,7 +562,7 @@ public class FilesOperations extends NormalizedOperations {
         }
     }
 
-    private boolean createRemote(ShareFileClient fileClient, int length) {
+    private boolean createRemote(ShareFileClient fileClient, long length) {
         var code = fileClient.createWithResponse(length, null, null, null, 
null,
                 endpoint.getMetadataTimeout(), Context.NONE).getStatusCode();
         return code == HTTP_CREATED || code == HTTP_OK;
diff --git 
a/components/camel-azure/camel-azure-files/src/test/java/org/apache/camel/component/file/azure/FilesOperationsStoreLengthTest.java
 
b/components/camel-azure/camel-azure-files/src/test/java/org/apache/camel/component/file/azure/FilesOperationsStoreLengthTest.java
new file mode 100644
index 000000000000..02cecf37e8cf
--- /dev/null
+++ 
b/components/camel-azure/camel-azure-files/src/test/java/org/apache/camel/component/file/azure/FilesOperationsStoreLengthTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.file.azure;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.azure.core.http.rest.Response;
+import com.azure.core.http.rest.SimpleResponse;
+import com.azure.core.util.Context;
+import com.azure.storage.file.share.FileSmbProperties;
+import com.azure.storage.file.share.ShareDirectoryClient;
+import com.azure.storage.file.share.ShareFileClient;
+import com.azure.storage.file.share.StorageFileOutputStream;
+import com.azure.storage.file.share.models.ShareFileHttpHeaders;
+import com.azure.storage.file.share.models.ShareFileInfo;
+import org.apache.camel.Exchange;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The Azure file is created with its size before the content is written, so 
the size must be the length of the body,
+ * not a CamelFileLength header left from the file the route consumed.
+ */
+class FilesOperationsStoreLengthTest extends CamelTestSupport {
+
+    private final AtomicLong createdSize = new AtomicLong(-1);
+    private final ByteArrayOutputStream written = new ByteArrayOutputStream();
+    private FilesOperations operations;
+
+    @BeforeEach
+    void setupOperations() throws Exception {
+        ShareDirectoryClient dir = mock(ShareDirectoryClient.class);
+        ShareFileClient file = mock(ShareFileClient.class);
+        StorageFileOutputStream out = mock(StorageFileOutputStream.class);
+
+        Response<Boolean> deleted = new SimpleResponse<>(null, 404, null, 
false);
+        when(dir.deleteFileIfExistsWithResponse(anyString(), 
nullable(Duration.class), any(Context.class)))
+                .thenReturn(deleted);
+        when(dir.getFileClient(anyString())).thenReturn(file);
+        when(file.createWithResponse(anyLong(), 
nullable(ShareFileHttpHeaders.class), nullable(FileSmbProperties.class),
+                nullable(String.class), nullable(Map.class), 
nullable(Duration.class), any(Context.class)))
+                .thenAnswer(inv -> {
+                    createdSize.set(inv.getArgument(0));
+                    Response<ShareFileInfo> created = new 
SimpleResponse<>(null, 201, null, null);
+                    return created;
+                });
+        when(file.getFileOutputStream()).thenReturn(out);
+        doAnswer(inv -> {
+            written.write((byte[]) inv.getArgument(0), inv.getArgument(1), 
inv.getArgument(2));
+            return null;
+        }).when(out).write(any(byte[].class), anyInt(), anyInt());
+
+        FilesEndpoint endpoint = context.getEndpoint(
+                
"azure-files://account.file.core.windows.net/share?credentialType=AZURE_IDENTITY",
 FilesEndpoint.class);
+        operations = new FilesOperations(endpoint);
+        operations.restore(new ArrayDeque<>(List.of(dir)));
+    }
+
+    private Exchange exchange(Object body, long fileLengthHeader) {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody(body);
+        exchange.getIn().setHeader(Exchange.FILE_LENGTH, fileLengthHeader);
+        return exchange;
+    }
+
+    @Test
+    void bodyShorterThanTheFileLengthHeader() {
+        // e.g. a file was consumed, and the route split or transformed it 
before storing the result
+        assertTrue(operations.storeFile("out.txt", 
exchange("abc".getBytes(StandardCharsets.UTF_8), 10), -1));
+
+        assertArrayEquals("abc".getBytes(StandardCharsets.UTF_8), 
written.toByteArray());
+        assertEquals(3, createdSize.get(), "the file must not be longer than 
the body (zero bytes at its end)");
+    }
+
+    @Test
+    void stringBodyLongerThanTheFileLengthHeader() {
+        assertTrue(operations.storeFile("out.txt", 
exchange("abcdefghijklmnopqrst", 10), -1));
+
+        assertEquals(20, written.size());
+        assertEquals(20, createdSize.get(), "the file must be created with the 
size of the body");
+    }
+
+    @Test
+    void streamOfUnknownLengthUsesTheFileLengthHeader() {
+        // guard: the documented streaming case (a stream whose length is only 
given by the header) is kept
+        InputStream stream = new BufferedInputStream(new 
ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)));
+        assertTrue(operations.storeFile("out.txt", exchange(stream, 5), -1));
+
+        assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8), 
written.toByteArray());
+        assertEquals(5, createdSize.get());
+    }
+}

Reply via email to