This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new cf374a911b5 CAMEL-24593: platform-http-starter - delete multipart
uploads when the exchange is done
cf374a911b5 is described below
commit cf374a911b5ab122f0d1d997a30ad4ac2a7d18c6
Author: croway <[email protected]>
AuthorDate: Wed Sep 2 15:03:00 2026 +0200
CAMEL-24593: platform-http-starter - delete multipart uploads when the
exchange is done
SpringBootPlatformHttpBinding.populateAttachments() copies every accepted
multipart upload into the servlet temp directory and uses that copy as the
attachment DataSource and, for a single upload, as the Path message body and
the CamelFilePath header. MultipartFile.transferTo() moves the container's
part
file, so the container's own end-of-request cleanup no longer finds it, and
nothing in the starter deleted the copy either: every upload the application
accepted stayed on disk for the life of the process.
The copy was introduced in CAMEL-21461 so the body can be a Path and the
attachment stays readable after the servlet request completed, which is a
good
reason to own the file - but owning it means removing it. The other HTTP
bindings do not leak: camel-http-common reads the part through the container
managed file, which the container deletes, and camel-platform-http-vertx has
deleteUploadedFilesOnEnd defaulting to true.
The binding now collects the temp files it created for a request and
registers
a Synchronization through ExchangeExtension.addOnCompletion that deletes
them
when the exchange is done being routed. The DataSource and the Path body
point
at the files until then, so they cannot be deleted any earlier; the consumer
writes the HTTP response before doneUoW, so the response is already out.
The opt-out mirrors the Vert.x option. Endpoint options are defined in
upstream
camel-platform-http and cannot be extended from here, so the option is a new
starter owned configuration class, SpringBootPlatformHttpServerProperties,
next to the existing camel.component.platform-http.server.undertow.accesslog
properties:
camel.component.platform-http.server.delete-uploaded-files-on-end=false
It is wired from the auto configuration through the engine and the consumer
onto the binding, and defaults to true. Existing public constructors are
unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
---
.../src/main/docs/platform-http.adoc | 21 +++
.../src/main/docs/platform-http.json | 12 ++
.../SpringBootPlatformHttpAutoConfiguration.java | 8 +-
.../springboot/SpringBootPlatformHttpBinding.java | 52 ++++++++
.../springboot/SpringBootPlatformHttpConsumer.java | 9 ++
.../springboot/SpringBootPlatformHttpEngine.java | 14 +-
.../SpringBootPlatformHttpServerProperties.java | 43 +++++++
...gBootPlatformHttpUploadCleanupDisabledTest.java | 111 ++++++++++++++++
.../SpringBootPlatformHttpUploadCleanupTest.java | 141 +++++++++++++++++++++
.../http/springboot/UploadCleanupRoute.java | 74 +++++++++++
.../modules/ROOT/pages/starters/platform-http.adoc | 3 +-
11 files changed, 482 insertions(+), 6 deletions(-)
diff --git
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
index 15053a6a48c..33336123259 100644
---
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
+++
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.adoc
@@ -17,6 +17,27 @@ carry no matrix parameters. A request to
`/greeting/John%20Doe;v=1` sets the `na
The `CamelHttpPath` header is not affected: it reports the raw request path,
with the servlet context-path
removed.
+== File uploads
+
+Every accepted multipart file upload is copied out of the servlet container
into the servlet temporary directory
+(`jakarta.servlet.ServletContext#TEMPDIR`), so that it can still be read after
the HTTP request has completed. The
+copy is what the route sees: it is the `jakarta.activation.DataSource` of the
attachment and, when the request carries
+a single file, also the `java.nio.file.Path` message body and the value of the
`CamelFilePath` header.
+
+Because Camel owns that copy, it is deleted again once the exchange is done
being routed, that is after the response
+has been written. A route that consumes the upload during routing - saving it
with the file producer, streaming it to
+a remote system, unmarshalling it - is unaffected, since the content has
already been read or copied by then.
+
+Set the following property if the application hands the temporary file over to
something that reads it *after* the
+exchange has completed, in which case the application becomes responsible for
deleting the file:
+
+[source,properties]
+----
+camel.component.platform-http.server.delete-uploaded-files-on-end=false
+----
+
+This mirrors the `deleteUploadedFilesOnEnd` option of the Vert.x platform-http
implementation and defaults to `true`.
+
== Undertow Access Log
You can enable Undertow access log to be managed by whatever logging library
you have in your camel application, you have to set the following parameters:
diff --git
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.json
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.json
index 641e67f14d7..6acdca2e9d5 100644
---
a/components-starter/camel-platform-http-starter/src/main/docs/platform-http.json
+++
b/components-starter/camel-platform-http-starter/src/main/docs/platform-http.json
@@ -11,6 +11,11 @@
"sourceType":
"org.apache.camel.component.platform.http.springboot.PlatformHttpComponentConfiguration",
"sourceMethod": "getCustomizer()"
},
+ {
+ "name": "camel.component.platform-http.server",
+ "type":
"org.apache.camel.component.platform.http.springboot.SpringBootPlatformHttpServerProperties",
+ "sourceType":
"org.apache.camel.component.platform.http.springboot.SpringBootPlatformHttpServerProperties"
+ },
{
"name": "camel.component.platform-http.server.undertow.accesslog",
"type":
"org.apache.camel.component.platform.http.springboot.customizer.UndertowAccessLogProperties",
@@ -75,6 +80,13 @@
"sourceType":
"org.apache.camel.component.platform.http.springboot.PlatformHttpComponentConfiguration",
"defaultValue": true
},
+ {
+ "name":
"camel.component.platform-http.server.delete-uploaded-files-on-end",
+ "type": "java.lang.Boolean",
+ "description": "Whether the temporary files, that multipart file uploads
are written to, are deleted when the exchange is done being routed. The
uploaded file is copied out of the servlet container into the servlet temp
directory so that it stays readable after the HTTP request has completed, which
makes Camel the owner of that copy. Turn this off only if the route hands the
file over to something that reads it after the exchange has completed - the
route is then responsible for de [...]
+ "sourceType":
"org.apache.camel.component.platform.http.springboot.SpringBootPlatformHttpServerProperties",
+ "defaultValue": true
+ },
{
"name":
"camel.component.platform-http.server.undertow.accesslog.use-camel-logging",
"type": "java.lang.Boolean",
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
index 22f3c1b9b11..d65511a6ad5 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAutoConfiguration.java
@@ -37,14 +37,16 @@ import java.util.List;
import java.util.concurrent.Executor;
@AutoConfiguration(after = { PlatformHttpComponentAutoConfiguration.class,
PlatformHttpComponentConverter.class })
-@EnableConfigurationProperties({ ComponentConfigurationProperties.class,
PlatformHttpComponentConfiguration.class })
+@EnableConfigurationProperties({ ComponentConfigurationProperties.class,
PlatformHttpComponentConfiguration.class,
+ SpringBootPlatformHttpServerProperties.class })
public class SpringBootPlatformHttpAutoConfiguration {
private static final Logger LOG =
LoggerFactory.getLogger(SpringBootPlatformHttpAutoConfiguration.class);
@Bean(name = "platform-http-engine")
@ConditionalOnMissingBean(PlatformHttpEngine.class)
public PlatformHttpEngine springBootPlatformHttpEngine(Environment env,
ServerProperties serverProperties,
- List<Executor>
executors) {
+ List<Executor>
executors,
+
SpringBootPlatformHttpServerProperties serverHttpProperties) {
if (executors == null || executors.isEmpty()) {
throw new IllegalStateException("No Executor configured");
}
@@ -87,7 +89,7 @@ public class SpringBootPlatformHttpAutoConfiguration {
LOG.debug("Using executor: {}", executor.getClass().getName());
}
int port = serverProperties.getPort() != null ?
serverProperties.getPort() : 8080;
- return new SpringBootPlatformHttpEngine(port, executor);
+ return new SpringBootPlatformHttpEngine(port, executor,
serverHttpProperties.isDeleteUploadedFilesOnEnd());
}
// Must not be @Lazy: eager beans are created before Camel starts, so the
mapping is listening before the first
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
index 6c432da6769..ac2dffd198a 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpBinding.java
@@ -38,6 +38,7 @@ import org.apache.camel.http.base.HttpHelper;
import org.apache.camel.http.common.DefaultHttpBinding;
import org.apache.camel.http.common.HttpConstants;
import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.SynchronizationAdapter;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.URISupport;
@@ -57,8 +58,10 @@ import java.io.OutputStream;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -68,6 +71,7 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
private static final Logger LOG =
LoggerFactory.getLogger(SpringBootPlatformHttpBinding.class);
private boolean streaming;
+ private boolean deleteUploadedFilesOnEnd = true;
private static final String CONTENT_TYPE_FORM_URLENCODED =
"application/x-www-form-urlencoded";
private static final List<Method> METHODS_WITH_BODY_ALLOWED =
List.of(Method.POST,
Method.PUT, Method.PATCH, Method.DELETE);
@@ -145,6 +149,9 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
boolean isSingleAttachment =
multipartHttpServletRequest.getFileMap() != null &&
multipartHttpServletRequest.getFileMap().keySet().size()
== 1;
message.setHeader(Exchange.ATTACHMENTS_SIZE,
multipartHttpServletRequest.getFileMap().keySet().size());
+ // the uploads are copied out of the servlet container and are
therefore owned by Camel, they are
+ // deleted again when the exchange is done being routed (unless
deleteUploadedFilesOnEnd is turned off)
+ final List<Path> uploadedTmpFiles = new ArrayList<>();
multipartHttpServletRequest.getFileMap().forEach((name,
multipartFile) -> {
try {
if (name != null) {
@@ -163,6 +170,7 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
Path uploadedTmpFile = Paths.get(tmpFolder.getPath(),
UUID.randomUUID().toString());
multipartFile.transferTo(uploadedTmpFile);
+ uploadedTmpFiles.add(uploadedTmpFile);
AttachmentMessage am = new
DefaultAttachmentMessage(message);
File uploadedFile = uploadedTmpFile.toFile();
@@ -185,7 +193,40 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
throw new RuntimeException(e);
}
});
+
+ if (deleteUploadedFilesOnEnd && !uploadedTmpFiles.isEmpty()) {
+ registerUploadedFilesCleanup(message, uploadedTmpFiles);
+ }
+ }
+ }
+
+ /**
+ * Deletes the temporary copies of the uploaded files when the exchange is
done being routed.
+ * <p/>
+ * The attachment {@code DataSource} and, for a single upload, the message
body point at those
+ * files for as long as the exchange is being routed, so the files can
only be deleted on completion.
+ */
+ private void registerUploadedFilesCleanup(Message message, List<Path>
uploadedTmpFiles) {
+ Exchange exchange = message.getExchange();
+ if (exchange == null) {
+ LOG.debug("Cannot delete uploaded temporary files as the message
is not associated with an exchange");
+ return;
}
+ exchange.getExchangeExtension().addOnCompletion(new
SynchronizationAdapter() {
+ @Override
+ public void onDone(Exchange doneExchange) {
+ for (Path uploadedTmpFile : uploadedTmpFiles) {
+ try {
+ if (Files.deleteIfExists(uploadedTmpFile)) {
+ LOG.trace("Deleted uploaded temporary file: {}",
uploadedTmpFile);
+ }
+ } catch (IOException e) {
+ LOG.debug("Cannot delete uploaded temporary file: {}
due to: {}. This exception is ignored.",
+ uploadedTmpFile, e.getMessage(), e);
+ }
+ }
+ }
+ });
}
/**
@@ -224,6 +265,17 @@ public class SpringBootPlatformHttpBinding extends
DefaultHttpBinding {
this.streaming = streaming;
}
+ public boolean isDeleteUploadedFilesOnEnd() {
+ return deleteUploadedFilesOnEnd;
+ }
+
+ /**
+ * Whether the temporary copies of multipart file uploads are deleted when
the exchange is done being routed.
+ */
+ public void setDeleteUploadedFilesOnEnd(boolean deleteUploadedFilesOnEnd) {
+ this.deleteUploadedFilesOnEnd = deleteUploadedFilesOnEnd;
+ }
+
public Object parseBody(HttpServletRequest request, Message message)
throws IOException {
if (request instanceof StandardMultipartHttpServletRequest ||
// In case of Spring FormContentFilter
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
index 9a6a3b1a904..26fc2756998 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
@@ -92,6 +92,15 @@ public class SpringBootPlatformHttpConsumer extends
DefaultConsumer implements P
this.binding = binding;
}
+ /**
+ * Whether the temporary copies of multipart file uploads are deleted when
the exchange is done being routed.
+ */
+ public void setDeleteUploadedFilesOnEnd(boolean deleteUploadedFilesOnEnd) {
+ if (binding instanceof SpringBootPlatformHttpBinding
springBootBinding) {
+
springBootBinding.setDeleteUploadedFilesOnEnd(deleteUploadedFilesOnEnd);
+ }
+ }
+
@Override
public PlatformHttpEndpoint getEndpoint() {
return (PlatformHttpEndpoint) super.getEndpoint();
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpEngine.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpEngine.java
index 7a32fa7ebc5..b3b7589da91 100644
---
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpEngine.java
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpEngine.java
@@ -27,23 +27,33 @@ public class SpringBootPlatformHttpEngine implements
PlatformHttpEngine {
private final int port;
private final Executor executor;
+ private final boolean deleteUploadedFilesOnEnd;
public SpringBootPlatformHttpEngine(int port) {
this(port, null);
}
public SpringBootPlatformHttpEngine(int port, Executor executor) {
+ this(port, executor, true);
+ }
+
+ public SpringBootPlatformHttpEngine(int port, Executor executor, boolean
deleteUploadedFilesOnEnd) {
this.port = port;
this.executor = executor;
+ this.deleteUploadedFilesOnEnd = deleteUploadedFilesOnEnd;
}
@Override
public PlatformHttpConsumer createConsumer(PlatformHttpEndpoint endpoint,
Processor processor) {
+ SpringBootPlatformHttpConsumer consumer;
if (executor == null) {
// engine created without an executor: let the consumer manage its
own
- return new SpringBootPlatformHttpConsumer(endpoint, processor);
+ consumer = new SpringBootPlatformHttpConsumer(endpoint, processor);
+ } else {
+ consumer = new SpringBootPlatformHttpConsumer(endpoint, processor,
executor);
}
- return new SpringBootPlatformHttpConsumer(endpoint, processor,
executor);
+ consumer.setDeleteUploadedFilesOnEnd(deleteUploadedFilesOnEnd);
+ return consumer;
}
@Override
diff --git
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpServerProperties.java
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpServerProperties.java
new file mode 100644
index 00000000000..f4d0e0d2c34
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpServerProperties.java
@@ -0,0 +1,43 @@
+/*
+ * 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.platform.http.springboot;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for the Spring Boot HTTP server serving the
platform-http endpoints.
+ */
+@ConfigurationProperties(prefix = "camel.component.platform-http.server")
+public class SpringBootPlatformHttpServerProperties {
+
+ /**
+ * Whether the temporary files, that multipart file uploads are written
to, are deleted when the exchange is done
+ * being routed. The uploaded file is copied out of the servlet container
into the servlet temp directory so that
+ * it stays readable after the HTTP request has completed, which makes
Camel the owner of that copy. Turn this off
+ * only if the route hands the file over to something that reads it after
the exchange has completed - the route is
+ * then responsible for deleting the file.
+ */
+ private boolean deleteUploadedFilesOnEnd = true;
+
+ public boolean isDeleteUploadedFilesOnEnd() {
+ return deleteUploadedFilesOnEnd;
+ }
+
+ public void setDeleteUploadedFilesOnEnd(boolean deleteUploadedFilesOnEnd) {
+ this.deleteUploadedFilesOnEnd = deleteUploadedFilesOnEnd;
+ }
+}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupDisabledTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupDisabledTest.java
new file mode 100644
index 00000000000..c01d5c7476d
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupDisabledTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.platform.http.springboot;
+
+import io.restassured.RestAssured;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the opt-out: with
+ * {@code
camel.component.platform-http.server.delete-uploaded-files-on-end=false} the
temporary copy of the upload
+ * survives the exchange and the route is responsible for it.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties =
"camel.component.platform-http.server.delete-uploaded-files-on-end=false",
+ classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpUploadCleanupDisabledTest.class,
+
SpringBootPlatformHttpUploadCleanupDisabledTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class })
+public class SpringBootPlatformHttpUploadCleanupDisabledTest {
+
+ private static final byte[] CONTENT = "upload
content".getBytes(StandardCharsets.UTF_8);
+
+ @Autowired
+ private Environment env;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.port = env.getRequiredProperty("local.server.port",
Integer.class);
+ }
+
+ @Test
+ void uploadIsKeptWhenCleanupIsTurnedOff() throws IOException {
+ String uploadPath = given().multiPart("file", "invoice.txt", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header(UploadCleanupRoute.ATTACHMENT_COUNT, is("1"))
+ .header(UploadCleanupRoute.EXISTED_DURING_ROUTING, is("true"))
+ .extract()
+ .header(UploadCleanupRoute.UPLOAD_PATHS);
+
+ Path uploadedFile = Paths.get(uploadPath);
+ try {
+ // give any (unwanted) completion driven deletion time to happen
before asserting the file is still there
+ await().pollDelay(Duration.ofMillis(500))
+ .atMost(Duration.ofSeconds(5))
+ .untilAsserted(() -> assertTrue(Files.exists(uploadedFile),
+ "Uploaded temporary file should have been kept: "
+ uploadedFile));
+ } finally {
+ // the application owns the file when the cleanup is turned off,
so do not leave it behind
+ Files.deleteIfExists(uploadedFile);
+ }
+ }
+
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
+ http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .csrf(AbstractHttpConfigurer::disable);
+ return http.build();
+ }
+
+ @Bean
+ public RouteBuilder uploadCleanupRoute() {
+ return new UploadCleanupRoute();
+ }
+ }
+}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupTest.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupTest.java
new file mode 100644
index 00000000000..c882e737681
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpUploadCleanupTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.platform.http.springboot;
+
+import io.restassured.RestAssured;
+import io.restassured.response.ExtractableResponse;
+import io.restassured.response.Response;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Multipart uploads are copied out of the servlet container into the servlet
temp directory, which makes Camel the
+ * owner of the copy. Verifies the copy is readable while the exchange is
routed and is deleted once the exchange is
+ * done, which is the default behaviour.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class,
+ SpringBootPlatformHttpUploadCleanupTest.class,
+ SpringBootPlatformHttpUploadCleanupTest.TestConfiguration.class,
+ PlatformHttpComponentAutoConfiguration.class,
SpringBootPlatformHttpAutoConfiguration.class })
+public class SpringBootPlatformHttpUploadCleanupTest {
+
+ private static final byte[] CONTENT = "upload
content".getBytes(StandardCharsets.UTF_8);
+
+ @Autowired
+ private Environment env;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.port = env.getRequiredProperty("local.server.port",
Integer.class);
+ }
+
+ @Test
+ void singleUploadIsDeletedWhenTheExchangeIsDone() {
+ ExtractableResponse<Response> response = given().multiPart("file",
"invoice.txt", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header(UploadCleanupRoute.ATTACHMENT_COUNT, is("1"))
+ .header(UploadCleanupRoute.EXISTED_DURING_ROUTING, is("true"))
+ .extract();
+
+ String uploadPath = response.header(UploadCleanupRoute.UPLOAD_PATHS);
+ // the single upload is also handed to the route as the message body
and as the CamelFilePath header
+ assertEquals(uploadPath,
response.header(UploadCleanupRoute.FILE_PATH_HEADER));
+ assertEquals(uploadPath,
response.header(UploadCleanupRoute.BODY_PATH));
+
+ Path uploadedFile = Paths.get(uploadPath);
+ await().atMost(10, TimeUnit.SECONDS)
+ .untilAsserted(() -> assertFalse(Files.exists(uploadedFile),
+ "Uploaded temporary file should have been deleted: " +
uploadedFile));
+ }
+
+ @Test
+ void allUploadsAreDeletedWhenTheExchangeIsDone() {
+ String uploadPaths = given().multiPart("first", "first.txt", CONTENT)
+ .multiPart("second", "second.txt", CONTENT)
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header(UploadCleanupRoute.ATTACHMENT_COUNT, is("2"))
+ .header(UploadCleanupRoute.EXISTED_DURING_ROUTING, is("true"))
+ .extract()
+ .header(UploadCleanupRoute.UPLOAD_PATHS);
+
+ String[] paths = uploadPaths.split(",");
+ assertEquals(2, paths.length, "Expected both uploads to be reported: "
+ uploadPaths);
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ for (String path : paths) {
+ assertFalse(Files.exists(Paths.get(path)), "Uploaded temporary
file should have been deleted: " + path);
+ }
+ });
+ }
+
+ /**
+ * A multipart request that carries no file part registers no cleanup and
keeps working.
+ */
+ @Test
+ void requestWithoutFilePartIsNotAffected() {
+ given().multiPart("field", "value")
+ .post("/upload")
+ .then()
+ .statusCode(200)
+ .header(UploadCleanupRoute.ATTACHMENT_COUNT, is("0"));
+ }
+
+ @Configuration
+ public static class TestConfiguration {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
+ http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+ .csrf(AbstractHttpConfigurer::disable);
+ return http.build();
+ }
+
+ @Bean
+ public RouteBuilder uploadCleanupRoute() {
+ return new UploadCleanupRoute();
+ }
+ }
+}
diff --git
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/UploadCleanupRoute.java
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/UploadCleanupRoute.java
new file mode 100644
index 00000000000..c435f9b3839
--- /dev/null
+++
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/UploadCleanupRoute.java
@@ -0,0 +1,74 @@
+/*
+ * 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.platform.http.springboot;
+
+import jakarta.activation.DataHandler;
+import jakarta.activation.FileDataSource;
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.attachment.AttachmentMessage;
+import org.apache.camel.builder.RouteBuilder;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * Reports back, in the response headers, where the accepted multipart uploads
were written to and whether they were
+ * still readable while the exchange was being routed. Used to assert the
temporary file handling of
+ * {@link SpringBootPlatformHttpBinding}.
+ */
+public class UploadCleanupRoute extends RouteBuilder {
+
+ static final String UPLOAD_PATHS = "uploadPaths";
+ static final String ATTACHMENT_COUNT = "attachmentCount";
+ static final String EXISTED_DURING_ROUTING = "existedDuringRouting";
+ static final String FILE_PATH_HEADER = "filePathHeader";
+ static final String BODY_PATH = "bodyPath";
+
+ @Override
+ public void configure() {
+ from("platform-http:/upload")
+ .routeId("upload")
+ .process(exchange -> {
+ AttachmentMessage am =
exchange.getMessage(AttachmentMessage.class);
+ StringBuilder paths = new StringBuilder();
+ boolean existed = true;
+ int count = 0;
+ if (am.getAttachments() != null) {
+ for (DataHandler dataHandler :
am.getAttachments().values()) {
+ File file = ((FileDataSource)
dataHandler.getDataSource()).getFile();
+ if (count > 0) {
+ paths.append(',');
+ }
+ paths.append(file.getAbsolutePath());
+ existed = existed && file.isFile();
+ count++;
+ }
+ }
+
+ Message message = exchange.getMessage();
+ Object body = message.getBody();
+ Object filePath = message.getHeader(Exchange.FILE_PATH);
+ message.setHeader(ATTACHMENT_COUNT, String.valueOf(count));
+ message.setHeader(UPLOAD_PATHS, paths.toString());
+ message.setHeader(EXISTED_DURING_ROUTING,
String.valueOf(existed));
+ message.setHeader(FILE_PATH_HEADER, filePath == null ? ""
: filePath.toString());
+ message.setHeader(BODY_PATH, body instanceof Path path ?
path.toAbsolutePath().toString() : "");
+ message.setBody("ok");
+ });
+ }
+}
diff --git a/docs/spring-boot/modules/ROOT/pages/starters/platform-http.adoc
b/docs/spring-boot/modules/ROOT/pages/starters/platform-http.adoc
index b3449d24637..4e0cdc8faa6 100644
--- a/docs/spring-boot/modules/ROOT/pages/starters/platform-http.adoc
+++ b/docs/spring-boot/modules/ROOT/pages/starters/platform-http.adoc
@@ -23,7 +23,7 @@ Please refer to the above links for usage and configuration
details.
== Spring Boot Auto-Configuration
-The starter supports 9 options, which are listed below.
+The starter supports 10 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
@@ -36,5 +36,6 @@ The starter supports 9 options, which are listed below.
| camel.component.platform-http.header-filter-strategy | To use a custom
org.apache.camel.spi.HeaderFilterStrategy to filter header to and from Camel
message. The option is a org.apache.camel.spi.HeaderFilterStrategy type. | |
HeaderFilterStrategy
| camel.component.platform-http.request-timeout | The period in milliseconds
after which the request should be timed out. | | Long
| camel.component.platform-http.server-request-validation | Whether HTTP
server should do preliminary validation of incoming requests, validating if
Content-Type/Accept header, matches what is allowed according to
consumes/produces configuration (if set). If validation fails HTTP Status
415/406 is returned. The HTTP server performs this validation before Camel is
involved, and as such if validation fails then Camel is never activated.
Setting this option to false, allows Camel to process [...]
+| camel.component.platform-http.server.delete-uploaded-files-on-end | Whether
the temporary files, that multipart file uploads are written to, are deleted
when the exchange is done being routed. The uploaded file is copied out of the
servlet container into the servlet temp directory so that it stays readable
after the HTTP request has completed, which makes Camel the owner of that copy.
Turn this off only if the route hands the file over to something that reads it
after the exchange has [...]
| camel.component.platform-http.server.undertow.accesslog.use-camel-logging |
Use camel logging for undertow http access log. | false | Boolean
|===