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());
+ }
+}