From 192f2becf616642de23b43e03ffb3cdf5c5f4493 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 29 Mar 2022 17:01:34 +0200 Subject: [PATCH] Automatically clean up multipart temp files This commit ensures that any resources created for multipart handling, obtained via ServerWebExchange.getMultipartData(), are automatically deleted after handling the completing the response. Resource for parts obtained via BodyExtractors::toMultipartData and BodyExtractors::toParts are not cleaned automatically, and should be cleaned via Part::delete. Closes gh-27633 --- .../server/adapter/HttpWebHandlerAdapter.java | 23 +++++++++++++++++++ .../web/reactive/function/BodyExtractors.java | 6 +++++ .../function/MultipartIntegrationTests.java | 9 ++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index 72e9793910..7dddb1f26c 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -18,7 +18,9 @@ package org.springframework.web.server.adapter; import java.util.Arrays; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,6 +34,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.codec.LoggingCodecSupport; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.http.codec.multipart.Part; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -249,6 +252,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa return getDelegate().handle(exchange) .doOnSuccess(aVoid -> logResponse(exchange)) .onErrorResume(ex -> handleUnresolvedError(exchange, ex)) + .then(cleanupMultipart(exchange)) .then(Mono.defer(response::setComplete)); } @@ -323,4 +327,23 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()); } + private Mono cleanupMultipart(ServerWebExchange exchange) { + return exchange.getMultipartData() + .onErrorResume(t -> Mono.empty()) // ignore errors reading multipart data + .flatMapIterable(Map::values) + .flatMapIterable(Function.identity()) + .flatMap(this::deletePart) + .then(); + } + + private Mono deletePart(Part part) { + return part.delete().onErrorResume(ex -> { + if (logger.isWarnEnabled()) { + logger.warn("Failed to perform cleanup of multipart items", ex); + } + return Mono.empty(); + }); + } + + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java index f433ccdfb4..419ae2ceb4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java @@ -137,6 +137,9 @@ public abstract class BodyExtractors { /** * Extractor to read multipart data into a {@code MultiValueMap}. + *

Note: that resources used for part handling, + * like storage for the uploaded files, is not deleted automatically, but + * should be done via {@link Part#delete()}. * @return {@code BodyExtractor} for multipart data */ // Parameterized for server-side use @@ -151,6 +154,9 @@ public abstract class BodyExtractors { /** * Extractor to read multipart data into {@code Flux}. + *

Note: that resources used for part handling, + * like storage for the uploaded files, is not deleted automatically, but + * should be done via {@link Part#delete()}. * @return {@code BodyExtractor} for multipart request parts */ // Parameterized for server-side use diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java index c73aa7dc3a..1677ea9a04 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java @@ -25,6 +25,7 @@ import java.util.Map; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -168,7 +169,9 @@ class MultipartIntegrationTests extends AbstractRouterFunctionIntegrationTests { assertThat(parts.size()).isEqualTo(2); assertThat(((FilePart) parts.get("fooPart")).filename()).isEqualTo("foo.txt"); assertThat(((FormFieldPart) parts.get("barPart")).value()).isEqualTo("bar"); - return ServerResponse.ok().build(); + return Flux.fromIterable(parts.values()) + .concatMap(Part::delete) + .then(ServerResponse.ok().build()); } catch(Exception e) { return Mono.error(e); @@ -183,7 +186,9 @@ class MultipartIntegrationTests extends AbstractRouterFunctionIntegrationTests { assertThat(parts.size()).isEqualTo(2); assertThat(((FilePart) parts.get(0)).filename()).isEqualTo("foo.txt"); assertThat(((FormFieldPart) parts.get(1)).value()).isEqualTo("bar"); - return ServerResponse.ok().build(); + return Flux.fromIterable(parts) + .concatMap(Part::delete) + .then(ServerResponse.ok().build()); } catch(Exception e) { return Mono.error(e);