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 e7c3e7ed2eeb CAMEL-25789: camel-aws2-s3 - streaming upload with the
random or timestamp naming strategy names the first file too (#28137)
e7c3e7ed2eeb is described below
commit e7c3e7ed2eeb7928ffc043e74c800fb8089c5ab5
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 20 18:37:52 2026 +0530
CAMEL-25789: camel-aws2-s3 - streaming upload with the random or timestamp
naming strategy names the first file too (#28137)
The first upload of a stream upload producer was named after keyName alone
also with the random and
timestamp naming strategies; only the later uploads got the UUID or
timestamp. A producer started again
(route or application restart), or a second producer with the same keyName,
wrote its first upload to
the same key and replaced the first file written before. Every upload now
gets the UUID or timestamp.
Follow-ups:
- camel-aws2-s3 - stream upload ITs expect the first file to be named by
the strategy too
- camel-aws2-s3 - stream upload ITs start from an empty bucket, fix the
Javadoc of the restart test
Closes #28137
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/docs/aws2-s3-streaming.adoc | 4 +-
.../src/main/docs/aws2-s3-streaming.adoc | 4 +-
.../aws2/s3/stream/AWS2S3StreamUploadProducer.java | 31 ++--
.../s3/integration/S3StreamUploadTimeoutIT.java | 21 ++-
.../S3StreamUploadTimestampNamingStrategyIT.java | 52 ++++---
.../S3StreamUploadTimestampTimeoutIT.java | 44 ++++--
.../AWS2S3StreamUploadRandomNamingRestartTest.java | 165 +++++++++++++++++++++
.../aws2/s3/stream/AWSS3NamingStrategyTest.java | 6 +-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 +
9 files changed, 269 insertions(+), 66 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-s3-streaming.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-s3-streaming.adoc
index 1c9258dc10cb..8bb1352c9381 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-s3-streaming.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-s3-streaming.adoc
@@ -7,8 +7,10 @@ xref:ROOT:aws2-s3-component.adoc[Back to AWS S3 Component]
With the stream mode enabled, users will be able to upload data to S3 without
knowing ahead of time the dimension of the data, by leveraging multipart upload.
The upload will be completed when the batchSize has been completed or the
batchMessageNumber has been reached.
-There are two possible naming strategies: progressive and random.
+There are three possible naming strategies: progressive, random and timestamp.
With the progressive strategy, each file will have the name composed by
keyName option and a progressive counter, and eventually the file extension (if
any), while with the random strategy a UUID will be added after keyName and
eventually the file extension will be appended.
+With the timestamp strategy, the time the upload started (in milliseconds) is
added after keyName instead of the UUID.
+With the random and timestamp strategies every file gets this part, the first
file of the producer too, so a producer that is started again does not replace
the files written before.
Additionally, streaming upload mode supports timestamp-based file grouping,
which allows messages to be automatically grouped into time windows based on
their timestamps.
diff --git
a/components/camel-aws/camel-aws2-s3/src/main/docs/aws2-s3-streaming.adoc
b/components/camel-aws/camel-aws2-s3/src/main/docs/aws2-s3-streaming.adoc
index 1c9258dc10cb..8bb1352c9381 100644
--- a/components/camel-aws/camel-aws2-s3/src/main/docs/aws2-s3-streaming.adoc
+++ b/components/camel-aws/camel-aws2-s3/src/main/docs/aws2-s3-streaming.adoc
@@ -7,8 +7,10 @@ xref:ROOT:aws2-s3-component.adoc[Back to AWS S3 Component]
With the stream mode enabled, users will be able to upload data to S3 without
knowing ahead of time the dimension of the data, by leveraging multipart upload.
The upload will be completed when the batchSize has been completed or the
batchMessageNumber has been reached.
-There are two possible naming strategies: progressive and random.
+There are three possible naming strategies: progressive, random and timestamp.
With the progressive strategy, each file will have the name composed by
keyName option and a progressive counter, and eventually the file extension (if
any), while with the random strategy a UUID will be added after keyName and
eventually the file extension will be appended.
+With the timestamp strategy, the time the upload started (in milliseconds) is
added after keyName instead of the UUID.
+With the random and timestamp strategies every file gets this part, the first
file of the producer too, so a producer that is started again does not replace
the files written before.
Additionally, streaming upload mode supports timestamp-based file grouping,
which allows messages to be automatically grouped into time windows based on
their timestamps.
diff --git
a/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadProducer.java
b/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadProducer.java
index 0ed57fd2e99c..2e7cc2c75a16 100644
---
a/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadProducer.java
+++
b/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadProducer.java
@@ -517,33 +517,20 @@ public class AWS2S3StreamUploadProducer extends
DefaultProducer {
}
break;
case random:
- if (part > 0) {
- if (ObjectHelper.isNotEmpty(ext)) {
- dynamicKeyName = fileName + "-" + id.toString() + ext;
- } else {
- dynamicKeyName = fileName + "-" + id.toString();
- }
+ // every upload, the first one of the producer too: a producer
that is started again, or another
+ // producer with the same keyName, would otherwise replace the
first object with its own
+ if (ObjectHelper.isNotEmpty(ext)) {
+ dynamicKeyName = fileName + "-" + id.toString() + ext;
} else {
- if (ObjectHelper.isNotEmpty(ext)) {
- dynamicKeyName = fileName + ext;
- } else {
- dynamicKeyName = fileName;
- }
+ dynamicKeyName = fileName + "-" + id.toString();
}
break;
case timestamp:
- if (part > 0) {
- if (ObjectHelper.isNotEmpty(ext)) {
- dynamicKeyName = fileName + "-" + timestamp + ext;
- } else {
- dynamicKeyName = fileName + "-" + timestamp;
- }
+ // every upload, as for random
+ if (ObjectHelper.isNotEmpty(ext)) {
+ dynamicKeyName = fileName + "-" + timestamp + ext;
} else {
- if (ObjectHelper.isNotEmpty(ext)) {
- dynamicKeyName = fileName + ext;
- } else {
- dynamicKeyName = fileName;
- }
+ dynamicKeyName = fileName + "-" + timestamp;
}
break;
default:
diff --git
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimeoutIT.java
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimeoutIT.java
index e04f20ba7353..48887aef5f49 100644
---
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimeoutIT.java
+++
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimeoutIT.java
@@ -27,6 +27,7 @@ import org.apache.camel.component.aws2.s3.AWS2S3Constants;
import org.apache.camel.component.aws2.s3.AWS2S3Operations;
import org.apache.camel.component.mock.MockEndpoint;
import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.s3.model.S3Object;
@@ -40,11 +41,26 @@ public class S3StreamUploadTimeoutIT extends Aws2S3Base {
@EndpointInject("mock:result")
private MockEndpoint result;
+ @BeforeEach
+ void removeTheObjectsOfAnEarlierRun() {
+ // a surefire rerun uses the same bucket: start from an empty bucket,
so only the objects of this run are counted
+ Exchange ex = template.request("direct:listObjects",
+ exchange ->
exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.listObjects));
+ List<S3Object> objects = ex.getMessage().getBody(List.class);
+ for (S3Object object : objects) {
+ template.request("direct:listObjects", exchange -> {
+ exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.deleteObject);
+ exchange.getIn().setHeader(AWS2S3Constants.KEY, object.key());
+ });
+ }
+ }
+
@Test
public void sendIn() throws Exception {
for (int i = 1; i <= 2; i++) {
int count = i * 23;
+ int uploads = i;
result.expectedMessageCount(count);
@@ -54,12 +70,15 @@ public class S3StreamUploadTimeoutIT extends Aws2S3Base {
MockEndpoint.assertIsSatisfied(context, 11, TimeUnit.SECONDS);
+ // each iteration is uploaded by the timeout to its own object
(random naming names the first one too)
Awaitility.await().atMost(11, TimeUnit.SECONDS)
.untilAsserted(() -> {
Exchange ex = template.request("direct:listObjects",
this::process);
List<S3Object> resp =
ex.getMessage().getBody(List.class);
- assertEquals(1, resp.size());
+ assertEquals(uploads, resp.size());
+ // 23 messages of "Andrea\n" per iteration
+ assertEquals(7L * 23 * uploads,
resp.stream().mapToLong(S3Object::size).sum());
});
}
}
diff --git
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampNamingStrategyIT.java
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampNamingStrategyIT.java
index 914fb20d0581..190fba87fc68 100644
---
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampNamingStrategyIT.java
+++
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampNamingStrategyIT.java
@@ -26,6 +26,7 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.aws2.s3.AWS2S3Constants;
import org.apache.camel.component.aws2.s3.AWS2S3Operations;
import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.s3.model.S3Object;
@@ -40,6 +41,20 @@ public class S3StreamUploadTimestampNamingStrategyIT extends
Aws2S3Base {
@EndpointInject("mock:result")
private MockEndpoint result;
+ @BeforeEach
+ void removeTheObjectsOfAnEarlierRun() {
+ // a surefire rerun uses the same bucket: start from an empty bucket,
so only the objects of this run are counted
+ Exchange ex = template.request("direct:listObjects",
+ exchange ->
exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.listObjects));
+ List<S3Object> objects = ex.getMessage().getBody(List.class);
+ for (S3Object object : objects) {
+ template.request("direct:listObjects", exchange -> {
+ exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.deleteObject);
+ exchange.getIn().setHeader(AWS2S3Constants.KEY, object.key());
+ });
+ }
+ }
+
@Test
public void sendInWithTimestampNamingStrategy() throws Exception {
result.expectedMessageCount(1000);
@@ -65,34 +80,23 @@ public class S3StreamUploadTimestampNamingStrategyIT
extends Aws2S3Base {
List<S3Object> resp = ex.getMessage().getBody(List.class);
assertEquals(40, resp.size());
- // Verify that uploaded files use timestamp naming strategy
- // Files should have names like: fileTest.txt, fileTest-<timestamp>.txt
- boolean foundBaseFile = false;
- boolean foundTimestampFile = false;
-
+ // Every upload is named with its timestamp, the first one too:
fileTest-<timestamp>.txt, never fileTest.txt
for (S3Object s3Object : resp) {
String key = s3Object.key();
-
- if ("fileTest.txt".equals(key)) {
- foundBaseFile = true;
- } else if (key.startsWith("fileTest-") && key.endsWith(".txt")) {
- foundTimestampFile = true;
-
- // Extract timestamp from filename and verify it's within
expected range
- String timestampStr = key.substring("fileTest-".length(),
key.length() - ".txt".length());
- try {
- long timestamp = Long.parseLong(timestampStr);
- assertTrue(timestamp >= beforeUpload && timestamp <=
afterUpload,
- "Timestamp " + timestamp + " should be between " +
beforeUpload + " and " + afterUpload);
- } catch (NumberFormatException e) {
- // This shouldn't happen with timestamp naming strategy
- throw new AssertionError("Expected numeric timestamp in
filename: " + key, e);
- }
+ assertTrue(key.startsWith("fileTest-") && key.endsWith(".txt"),
+ "Expected a timestamp-named file
(fileTest-<timestamp>.txt): " + key);
+
+ // Extract timestamp from filename and verify it's within expected
range
+ String timestampStr = key.substring("fileTest-".length(),
key.length() - ".txt".length());
+ try {
+ long timestamp = Long.parseLong(timestampStr);
+ assertTrue(timestamp >= beforeUpload && timestamp <=
afterUpload,
+ "Timestamp " + timestamp + " should be between " +
beforeUpload + " and " + afterUpload);
+ } catch (NumberFormatException e) {
+ // This shouldn't happen with timestamp naming strategy
+ throw new AssertionError("Expected numeric timestamp in
filename: " + key, e);
}
}
-
- assertTrue(foundBaseFile, "Should find base file (fileTest.txt)");
- assertTrue(foundTimestampFile, "Should find timestamp-named files
(fileTest-<timestamp>.txt)");
}
@Override
diff --git
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampTimeoutIT.java
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampTimeoutIT.java
index 65a62a7e2638..d076fe4d9f4f 100644
---
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampTimeoutIT.java
+++
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3StreamUploadTimestampTimeoutIT.java
@@ -27,6 +27,7 @@ import org.apache.camel.component.aws2.s3.AWS2S3Constants;
import org.apache.camel.component.aws2.s3.AWS2S3Operations;
import org.apache.camel.component.mock.MockEndpoint;
import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.s3.model.S3Object;
@@ -41,16 +42,31 @@ public class S3StreamUploadTimestampTimeoutIT extends
Aws2S3Base {
@EndpointInject("mock:result")
private MockEndpoint result;
+ @BeforeEach
+ void removeTheObjectsOfAnEarlierRun() {
+ // a surefire rerun uses the same bucket: start from an empty bucket,
so only the objects of this run are counted
+ Exchange ex = template.request("direct:listObjects",
+ exchange ->
exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.listObjects));
+ List<S3Object> objects = ex.getMessage().getBody(List.class);
+ for (S3Object object : objects) {
+ template.request("direct:listObjects", exchange -> {
+ exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION,
AWS2S3Operations.deleteObject);
+ exchange.getIn().setHeader(AWS2S3Constants.KEY, object.key());
+ });
+ }
+ }
+
@Test
public void sendInWithTimestampAndTimeout() throws Exception {
+ long firstUpload = System.currentTimeMillis();
+
for (int i = 1; i <= 2; i++) {
int count = i * 23;
+ int uploads = i;
result.expectedMessageCount(count);
- long beforeUpload = System.currentTimeMillis();
-
for (int j = 0; j < 23; j++) {
template.sendBody("direct:stream1", "TestData\n");
}
@@ -59,33 +75,31 @@ public class S3StreamUploadTimestampTimeoutIT extends
Aws2S3Base {
MockEndpoint.assertIsSatisfied(context, 11, TimeUnit.SECONDS);
+ // each iteration is uploaded by the timeout to its own object,
named with its timestamp (the first one too)
Awaitility.await().atMost(11, TimeUnit.SECONDS)
.untilAsserted(() -> {
Exchange ex = template.request("direct:listObjects",
this::process);
List<S3Object> resp =
ex.getMessage().getBody(List.class);
- assertEquals(1, resp.size());
-
- // Verify the uploaded file uses timestamp naming
strategy
- S3Object s3Object = resp.get(0);
- String key = s3Object.key();
+ assertEquals(uploads, resp.size());
- // The file should either be the base name or have a
timestamp suffix
- if ("fileTest.txt".equals(key)) {
- // This is fine - it's the base file
- } else if (key.startsWith("fileTest-") &&
key.endsWith(".txt")) {
- // Extract and validate timestamp
+ long size = 0;
+ for (S3Object s3Object : resp) {
+ String key = s3Object.key();
+ assertTrue(key.startsWith("fileTest-") &&
key.endsWith(".txt"),
+ "Expected a timestamp-named file
(fileTest-<timestamp>.txt): " + key);
String timestampStr =
key.substring("fileTest-".length(), key.length() - ".txt".length());
try {
long timestamp = Long.parseLong(timestampStr);
- assertTrue(timestamp >= beforeUpload &&
timestamp <= afterUpload + 11000, // Allow extra time for timeout
+ assertTrue(timestamp >= firstUpload &&
timestamp <= afterUpload + 11000, // Allow extra time for timeout
"Timestamp " + timestamp + " should be
within expected range");
} catch (NumberFormatException e) {
throw new AssertionError("Expected numeric
timestamp in filename: " + key, e);
}
- } else {
- throw new AssertionError("Unexpected filename
format: " + key);
+ size += s3Object.size();
}
+ // 23 messages of "TestData\n" per iteration
+ assertEquals(9L * 23 * uploads, size);
});
}
}
diff --git
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadRandomNamingRestartTest.java
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadRandomNamingRestartTest.java
new file mode 100644
index 000000000000..2486053b5ca4
--- /dev/null
+++
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWS2S3StreamUploadRandomNamingRestartTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.aws2.s3.stream;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.component.aws2.s3.AWS2S3Configuration;
+import org.apache.camel.component.aws2.s3.AWS2S3Endpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.http.SdkHttpResponse;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
+import
software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse;
+import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
+import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse;
+import software.amazon.awssdk.services.s3.model.UploadPartRequest;
+import software.amazon.awssdk.services.s3.model.UploadPartResponse;
+
+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.Mockito.when;
+
+/**
+ * With namingStrategy=random every upload must get the UUID, the first one of
a producer too: a producer that is
+ * started again (or a second producer with the same keyName) must not replace
the objects written before. Uses a mocked
+ * S3 client that keeps the completed objects by key, like a bucket.
+ */
+public class AWS2S3StreamUploadRandomNamingRestartTest {
+
+ @Mock
+ private AWS2S3Endpoint endpoint;
+
+ @Mock
+ private AWS2S3Configuration configuration;
+
+ @Mock
+ private S3Client s3Client;
+
+ // the bucket: completed objects by key
+ private final Map<String, String> bucket = new ConcurrentHashMap<>();
+ private final Map<String, String> keysByUploadId = new
ConcurrentHashMap<>();
+ private final Map<String, ByteArrayOutputStream> partsByUploadId = new
ConcurrentHashMap<>();
+ private final AtomicInteger uploadIds = new AtomicInteger();
+
+ private DefaultCamelContext camelContext;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ camelContext = new DefaultCamelContext();
+
+ when(endpoint.getConfiguration()).thenReturn(configuration);
+ when(endpoint.getCamelContext()).thenReturn(camelContext);
+ when(endpoint.getS3Client()).thenReturn(s3Client);
+
+ when(configuration.getBucketName()).thenReturn("test-bucket");
+ when(configuration.getKeyName()).thenReturn("camel.txt");
+ when(configuration.isMultiPartUpload()).thenReturn(false);
+ when(configuration.getPartSize()).thenReturn(25L * 1024 * 1024);
+
when(configuration.getNamingStrategy()).thenReturn(AWSS3NamingStrategyEnum.random);
+
when(configuration.getRestartingPolicy()).thenReturn(AWSS3RestartingPolicyEnum.override);
+ // the defaults of the endpoint
+ when(configuration.getBatchMessageNumber()).thenReturn(10);
+ when(configuration.getBatchSize()).thenReturn(1000000);
+ when(configuration.getBufferSize()).thenReturn(1000000);
+
+ SdkHttpResponse httpResponse =
SdkHttpResponse.builder().statusCode(200).build();
+
+
when(s3Client.createMultipartUpload(any(CreateMultipartUploadRequest.class))).thenAnswer(invocation
-> {
+ CreateMultipartUploadRequest request = invocation.getArgument(0);
+ String uploadId = "upload-" + uploadIds.incrementAndGet();
+ keysByUploadId.put(uploadId, request.key());
+ partsByUploadId.put(uploadId, new ByteArrayOutputStream());
+ return
CreateMultipartUploadResponse.builder().uploadId(uploadId).sdkHttpResponse(httpResponse).build();
+ });
+
+ when(s3Client.uploadPart(any(UploadPartRequest.class),
any(RequestBody.class))).thenAnswer(invocation -> {
+ UploadPartRequest request = invocation.getArgument(0);
+ RequestBody body = invocation.getArgument(1);
+
partsByUploadId.get(request.uploadId()).write(body.contentStreamProvider().newStream().readAllBytes());
+ return
UploadPartResponse.builder().eTag("etag").sdkHttpResponse(httpResponse).build();
+ });
+
+
when(s3Client.completeMultipartUpload(any(CompleteMultipartUploadRequest.class))).thenAnswer(invocation
-> {
+ CompleteMultipartUploadRequest request = invocation.getArgument(0);
+ // like S3: the completed object replaces any object of the same
key
+ bucket.put(keysByUploadId.get(request.uploadId()),
+
partsByUploadId.get(request.uploadId()).toString(StandardCharsets.UTF_8));
+ return
CompleteMultipartUploadResponse.builder().eTag("final-etag").sdkHttpResponse(httpResponse).build();
+ });
+ }
+
+ @Test
+ public void aProducerStartedAgainDoesNotReplaceTheObjectsOfTheFirstOne()
throws Exception {
+ // the first run of the route: 15 messages, one object of 10 messages
and 5 messages flushed at stop
+ AWS2S3StreamUploadProducer first = new
AWS2S3StreamUploadProducer(endpoint);
+ first.start();
+ send(first, 0, 15);
+ first.stop();
+ assertEquals(2, bucket.size(), "the first run wrote two objects: " +
bucket.keySet());
+
+ // the route is started again: a new producer, same endpoint options
+ AWS2S3StreamUploadProducer second = new
AWS2S3StreamUploadProducer(endpoint);
+ second.start();
+ send(second, 15, 15);
+ second.stop();
+
+ assertEquals(4, uploadIds.get(), "four uploads were completed");
+ assertEquals(4, bucket.size(), "every upload must be its own object,
but the bucket has " + bucket.keySet());
+ List<String> stored = new ArrayList<>();
+ for (String content : bucket.values()) {
+ for (String line : content.split("\n")) {
+ stored.add(line);
+ }
+ }
+ assertEquals(30, stored.size(), "every message sent must be in the
bucket: " + bucket);
+ }
+
+ @Test
+ public void everyObjectHasTheKeyNameAndARandomPart() throws Exception {
+ AWS2S3StreamUploadProducer producer = new
AWS2S3StreamUploadProducer(endpoint);
+ send(producer, 0, 20);
+
+ assertEquals(2, bucket.size(), bucket.keySet().toString());
+ for (String key : bucket.keySet()) {
+ assertTrue(key.matches("camel-[0-9a-f-]{36}\\.txt"), key);
+ }
+ }
+
+ private void send(AWS2S3StreamUploadProducer producer, int from, int
count) throws Exception {
+ for (int i = from; i < from + count; i++) {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getIn().setBody("message-" + i + "\n");
+ producer.process(exchange);
+ }
+ }
+}
diff --git
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWSS3NamingStrategyTest.java
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWSS3NamingStrategyTest.java
index b28589411cec..1b602b170a61 100644
---
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWSS3NamingStrategyTest.java
+++
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/stream/AWSS3NamingStrategyTest.java
@@ -61,8 +61,9 @@ public class AWSS3NamingStrategyTest extends CamelTestSupport
{
UUID testUuid =
UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
+ // the first upload of a producer gets the random part too
String result = (String) method.invoke(producer, "testFile",
AWSS3NamingStrategyEnum.random, ".txt", 0, testUuid, 0L);
- assertEquals("testFile.txt", result);
+ assertEquals("testFile-123e4567-e89b-12d3-a456-426614174000.txt",
result);
result = (String) method.invoke(producer, "testFile",
AWSS3NamingStrategyEnum.random, ".txt", 1, testUuid, 0L);
assertEquals("testFile-123e4567-e89b-12d3-a456-426614174000.txt",
result);
@@ -82,9 +83,10 @@ public class AWSS3NamingStrategyTest extends
CamelTestSupport {
long testTimestamp = 1632468273000L; // September 24, 2021 1:04:33 PM
GMT
+ // the first upload of a producer gets the timestamp too
String result = (String) method.invoke(producer, "testFile",
AWSS3NamingStrategyEnum.timestamp, ".txt", 0, null,
testTimestamp);
- assertEquals("testFile.txt", result);
+ assertEquals("testFile-1632468273000.txt", result);
result = (String) method.invoke(producer, "testFile",
AWSS3NamingStrategyEnum.timestamp, ".txt", 1, null,
testTimestamp);
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 6872b0344856..951f78c9f1e8 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
@@ -1323,6 +1323,14 @@ handshake; an IP address must be listed as a subject
alternative name of the cer
host name (such as `mllp://8888`) cannot pass the check. Use a certificate
that matches the host of the endpoint, or set
the new `hostnameVerification` option to `false` to keep the previous
behaviour.
+=== camel-aws2-s3 - streaming upload with the random or timestamp naming
strategy names the first file too
+
+With `streamingUploadMode=true` and `namingStrategy=random` (or `timestamp`),
the first file uploaded by a producer was
+named after `keyName` alone (for example `camel.txt`), and only the later
files got a UUID (or a timestamp) after
+`keyName`. A producer that was started again (a restart of the route or of the
application), or another producer with
+the same `keyName`, wrote its first file to the same key and replaced the
first file written before. Every file now
+gets the UUID (or the timestamp), the first one too: `camel-<uuid>.txt`. The
progressive naming strategy is unchanged.
+
=== camel-atom - sortEntries sorts by the updated date and routes the oldest
entry first
With `sortEntries=true` the `atom` consumer sorted the entries of a feed by
their published date and routed them from