Skip to content

Commit 30af01f

Browse files
committed
Use DataBufferUtils.write in DefaultFilePart.transferTo
This commit makes sure that in DefaultMultipartMessageReader's DefaultFilePart, the file is not closed before all bytes are written, by using DataBufferUtils.write (see c1b6885191d6a50347aeaa14da994f0db88f26fe). The commit also improves on the logging of the DefaultMultipartMessageReader. Closes gh-23130
1 parent f08656c commit 30af01f

File tree

2 files changed

+59
-43
lines changed

2 files changed

+59
-43
lines changed

spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultMultipartMessageReader.java

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,9 @@
1616

1717
package org.springframework.http.codec.multipart;
1818

19-
import java.io.IOException;
20-
import java.nio.channels.AsynchronousFileChannel;
21-
import java.nio.channels.Channel;
2219
import java.nio.charset.Charset;
2320
import java.nio.charset.StandardCharsets;
24-
import java.nio.file.OpenOption;
2521
import java.nio.file.Path;
26-
import java.nio.file.StandardOpenOption;
2722
import java.util.Collections;
2823
import java.util.List;
2924
import java.util.Map;
@@ -100,10 +95,6 @@ public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage mess
10095
return Flux.error(new CodecException("No multipart boundary found in Content-Type: \"" +
10196
message.getHeaders().getContentType() + "\""));
10297
}
103-
if (logger.isTraceEnabled()) {
104-
logger.trace("Boundary: " + toString(boundary));
105-
}
106-
10798
byte[] boundaryNeedle = concat(BOUNDARY_PREFIX, boundary);
10899
Flux<DataBuffer> body = skipUntilFirstBoundary(message.getBody(), boundary);
109100

@@ -148,8 +139,10 @@ private static Flux<DataBuffer> skipUntilFirstBoundary(Flux<DataBuffer> dataBuff
148139
DataBuffer slice = dataBuffer.retainedSlice(endIdx + 1, length);
149140
DataBufferUtils.release(dataBuffer);
150141
if (logger.isTraceEnabled()) {
151-
logger.trace("Found first boundary at " + endIdx + " in " + toString(dataBuffer));
152-
}
142+
logger.trace(
143+
"Found last byte of first boundary (" + toString(boundary)
144+
+ ") at " + endIdx);
145+
}
153146
return Mono.just(slice);
154147
}
155148
else {
@@ -188,14 +181,14 @@ private static Part toPart(DataBuffer dataBuffer) {
188181
}
189182
}
190183

191-
if (logger.isTraceEnabled()) {
192-
logger.trace("Part data: " + toString(dataBuffer));
193-
}
194184
int endIdx = HEADER_MATCHER.match(dataBuffer);
195185

196186
HttpHeaders headers;
197187
DataBuffer body;
198188
if (endIdx > 0) {
189+
if (logger.isTraceEnabled()) {
190+
logger.trace("Found last byte of part header at " + endIdx );
191+
}
199192
readPosition = dataBuffer.readPosition();
200193
int headersLength = endIdx + 1 - (readPosition + HEADER_BODY_SEPARATOR.length);
201194
DataBuffer headersBuffer = dataBuffer.retainedSlice(readPosition, headersLength);
@@ -204,6 +197,9 @@ private static Part toPart(DataBuffer dataBuffer) {
204197
headers = toHeaders(headersBuffer);
205198
}
206199
else {
200+
if (logger.isTraceEnabled()) {
201+
logger.trace("No header found");
202+
}
207203
headers = new HttpHeaders();
208204
body = DataBufferUtils.retain(dataBuffer);
209205
}
@@ -252,16 +248,6 @@ private static HttpHeaders toHeaders(DataBuffer dataBuffer) {
252248
return result;
253249
}
254250

255-
256-
private static String toString(DataBuffer dataBuffer) {
257-
byte[] bytes = new byte[dataBuffer.readableByteCount()];
258-
int j = 0;
259-
for (int i = dataBuffer.readPosition(); i < dataBuffer.writePosition(); i++) {
260-
bytes[j++] = dataBuffer.getByte(i);
261-
}
262-
return toString(bytes);
263-
}
264-
265251
private static String toString(byte[] bytes) {
266252
StringBuilder builder = new StringBuilder();
267253
for (byte b : bytes) {
@@ -368,10 +354,6 @@ public String value() {
368354

369355
private static class DefaultFilePart extends DefaultPart implements FilePart {
370356

371-
private static final OpenOption[] FILE_CHANNEL_OPTIONS =
372-
{StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE};
373-
374-
375357
public DefaultFilePart(HttpHeaders headers, DataBuffer body) {
376358
super(headers, body);
377359
}
@@ -385,23 +367,9 @@ public String filename() {
385367

386368
@Override
387369
public Mono<Void> transferTo(Path dest) {
388-
return Mono.using(() -> AsynchronousFileChannel.open(dest, FILE_CHANNEL_OPTIONS),
389-
this::writeBody, this::close);
390-
}
391-
392-
private Mono<Void> writeBody(AsynchronousFileChannel channel) {
393-
return DataBufferUtils.write(content(), channel)
394-
.map(DataBufferUtils::release)
395-
.then();
370+
return DataBufferUtils.write(content(), dest);
396371
}
397372

398-
private void close(Channel channel) {
399-
try {
400-
channel.close();
401-
}
402-
catch (IOException ignore) {
403-
}
404-
}
405373
}
406374

407375
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.web.reactive.result.method.annotation;
1818

19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
1923
import java.util.List;
2024
import java.util.stream.Collectors;
2125

@@ -31,6 +35,7 @@
3135
import org.springframework.context.annotation.Bean;
3236
import org.springframework.context.annotation.Configuration;
3337
import org.springframework.core.io.ClassPathResource;
38+
import org.springframework.core.io.Resource;
3439
import org.springframework.http.HttpEntity;
3540
import org.springframework.http.HttpStatus;
3641
import org.springframework.http.client.MultipartBodyBuilder;
@@ -145,6 +150,34 @@ public void filePartsMono() {
145150
.verifyComplete();
146151
}
147152

153+
@Test
154+
public void transferTo() {
155+
Flux<String> result = webClient
156+
.post()
157+
.uri("/transferTo")
158+
.syncBody(generateBody())
159+
.retrieve()
160+
.bodyToFlux(String.class);
161+
162+
StepVerifier.create(result)
163+
.consumeNextWith(filename -> verifyContents(Paths.get(filename), new ClassPathResource("foo.txt", MultipartHttpMessageReader.class)))
164+
.consumeNextWith(filename -> verifyContents(Paths.get(filename), new ClassPathResource("logo.png", getClass())))
165+
.verifyComplete();
166+
167+
}
168+
169+
private static void verifyContents(Path tempFile, Resource resource) {
170+
try {
171+
byte[] tempBytes = Files.readAllBytes(tempFile);
172+
byte[] resourceBytes = Files.readAllBytes(resource.getFile().toPath());
173+
assertThat(tempBytes).isEqualTo(resourceBytes);
174+
}
175+
catch (IOException ex) {
176+
throw new AssertionError(ex);
177+
}
178+
}
179+
180+
148181
@Test
149182
public void modelAttribute() {
150183
Mono<String> result = webClient
@@ -217,6 +250,21 @@ Mono<String> filePartsFlux(@RequestPart("fileParts") Mono<FilePart> parts) {
217250
return partFluxDescription(Flux.from(parts));
218251
}
219252

253+
@PostMapping("/transferTo")
254+
Flux<String> transferTo(@RequestPart("fileParts") Flux<FilePart> parts) {
255+
return parts.flatMap(filePart -> {
256+
try {
257+
Path tempFile = Files.createTempFile("MultipartIntegrationTests", filePart.filename());
258+
return filePart.transferTo(tempFile)
259+
.then(Mono.just(tempFile.toString() + "\n"));
260+
261+
}
262+
catch (IOException e) {
263+
return Mono.error(e);
264+
}
265+
});
266+
}
267+
220268
@PostMapping("/modelAttribute")
221269
String modelAttribute(@ModelAttribute FormBean formBean) {
222270
return formBean.toString();

0 commit comments

Comments
 (0)