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 deleting the file.", + "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 8e60598d4a9..e924f361261 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 @@ -39,13 +39,15 @@ @Configuration(proxyBeanMethods = false) @AutoConfigureAfter(name = { "org.apache.camel.component.servlet.springboot.PlatformHttpComponentAutoConfiguration", "org.apache.camel.component.servlet.springboot.PlatformHttpComponentConverter" }) -@EnableConfigurationProperties({ComponentConfigurationProperties.class,PlatformHttpComponentConfiguration.class, WebMvcProperties.class}) +@EnableConfigurationProperties({ComponentConfigurationProperties.class,PlatformHttpComponentConfiguration.class, WebMvcProperties.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, List executors) { + public PlatformHttpEngine springBootPlatformHttpEngine(Environment env, List executors, + SpringBootPlatformHttpServerProperties serverHttpProperties) { Executor executor; if (executors != null && !executors.isEmpty()) { @@ -92,7 +94,7 @@ public PlatformHttpEngine springBootPlatformHttpEngine(Environment env, List METHODS_WITH_BODY_ALLOWED = List.of(Method.POST, Method.PUT, Method.PATCH, Method.DELETE); @@ -145,6 +149,9 @@ protected void populateAttachments(HttpServletRequest request, Message message) 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 uploadedTmpFiles = new ArrayList<>(); multipartHttpServletRequest.getFileMap().forEach((name, multipartFile) -> { try { if (name != null) { @@ -163,6 +170,7 @@ protected void populateAttachments(HttpServletRequest request, Message message) 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 @@ protected void populateAttachments(HttpServletRequest request, Message message) 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. + *

+ * 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 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 void setStreaming(boolean streaming) { 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 dcf883385b1..a3f27529fb4 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 @@ -78,6 +78,15 @@ void setBinding(HttpBinding binding) { 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 79542a0fc8e..2b8810dcc44 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,6 +27,7 @@ public class SpringBootPlatformHttpEngine implements PlatformHttpEngine { private final int port; private Executor executor; + private boolean deleteUploadedFilesOnEnd = true; public SpringBootPlatformHttpEngine(int port) { this.port = port; @@ -37,9 +38,16 @@ public SpringBootPlatformHttpEngine(int port, Executor executor) { this.executor = executor; } + public SpringBootPlatformHttpEngine(int port, Executor executor, boolean deleteUploadedFilesOnEnd) { + this(port, executor); + this.deleteUploadedFilesOnEnd = deleteUploadedFilesOnEnd; + } + @Override public PlatformHttpConsumer createConsumer(PlatformHttpEndpoint endpoint, Processor processor) { - return new SpringBootPlatformHttpConsumer(endpoint, processor, executor); + SpringBootPlatformHttpConsumer consumer = 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..c36d32a75df --- /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.junit5.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..6e1bd6943ed --- /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.junit5.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 = 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"); + }); + } +}