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
 |===

Reply via email to