Skip to content

Commit 1e5f8cc

Browse files
committed
FilePart and MultipartFile provide transferTo(Path) variant
Also, ZeroCopyHttpOutputMessage provides writeWith(Path, int, int), enforcing that variant as the implementation target in 5.1 (analogous to FilePart). Issue: SPR-16925
1 parent c38cb43 commit 1e5f8cc

File tree

10 files changed

+93
-47
lines changed

10 files changed

+93
-47
lines changed

spring-web/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.http;
1818

1919
import java.io.File;
20+
import java.nio.file.Path;
2021

2122
import reactor.core.publisher.Mono;
2223

@@ -25,6 +26,7 @@
2526
* file transfers.
2627
*
2728
* @author Arjen Poutsma
29+
* @author Juergen Hoeller
2830
* @since 5.0
2931
* @see <a href="https://en.wikipedia.org/wiki/Zero-copy">Zero-copy</a>
3032
*/
@@ -38,6 +40,19 @@ public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage {
3840
* @param count the number of bytes to be transferred
3941
* @return a publisher that indicates completion or error.
4042
*/
41-
Mono<Void> writeWith(File file, long position, long count);
43+
default Mono<Void> writeWith(File file, long position, long count) {
44+
return writeWith(file.toPath(), position, count);
45+
}
46+
47+
/**
48+
* Use the given {@link Path} to write the body of the message to the underlying
49+
* HTTP layer.
50+
* @param file the file to transfer
51+
* @param position the position within the file from which the transfer is to begin
52+
* @param count the number of bytes to be transferred
53+
* @return a publisher that indicates completion or error.
54+
* @since 5.1
55+
*/
56+
Mono<Void> writeWith(Path file, long position, long count);
4257

4358
}

spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.http.client.reactive;
1818

19-
import java.io.File;
2019
import java.net.URI;
20+
import java.nio.file.Path;
2121
import java.util.Collection;
2222

2323
import io.netty.buffer.ByteBuf;
@@ -97,8 +97,8 @@ private static Publisher<ByteBuf> toByteBufs(Publisher<? extends DataBuffer> dat
9797
}
9898

9999
@Override
100-
public Mono<Void> writeWith(File file, long position, long count) {
101-
return doCommit(() -> this.outbound.sendFile(file.toPath(), position, count).then());
100+
public Mono<Void> writeWith(Path file, long position, long count) {
101+
return doCommit(() -> this.outbound.sendFile(file, position, count).then());
102102
}
103103

104104
@Override

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.http.codec.multipart;
1818

1919
import java.io.File;
20+
import java.nio.file.Path;
2021

2122
import reactor.core.publisher.Mono;
2223

@@ -25,6 +26,7 @@
2526
* a multipart request.
2627
*
2728
* @author Rossen Stoyanchev
29+
* @author Juergen Hoeller
2830
* @since 5.0
2931
*/
3032
public interface FilePart extends Part {
@@ -38,10 +40,26 @@ public interface FilePart extends Part {
3840
* Convenience method to copy the content of the file in this part to the
3941
* given destination file. If the destination file already exists, it will
4042
* be truncated first.
43+
* <p>The default implementation delegates to {@link #transferTo(Path)}.
4144
* @param dest the target file
4245
* @return completion {@code Mono} with the result of the file transfer,
4346
* possibly {@link IllegalStateException} if the part isn't a file
47+
* @see #transferTo(Path)
4448
*/
45-
Mono<Void> transferTo(File dest);
49+
default Mono<Void> transferTo(File dest) {
50+
return transferTo(dest.toPath());
51+
}
52+
53+
/**
54+
* Convenience method to copy the content of the file in this part to the
55+
* given destination file. If the destination file already exists, it will
56+
* be truncated first.
57+
* @param dest the target file
58+
* @return completion {@code Mono} with the result of the file transfer,
59+
* possibly {@link IllegalStateException} if the part isn't a file
60+
* @since 5.1
61+
* @see #transferTo(File)
62+
*/
63+
Mono<Void> transferTo(Path dest);
4664

4765
}

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

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

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

19-
import java.io.File;
2019
import java.io.IOException;
2120
import java.nio.channels.Channels;
2221
import java.nio.channels.FileChannel;
2322
import java.nio.channels.ReadableByteChannel;
2423
import java.nio.charset.Charset;
2524
import java.nio.charset.StandardCharsets;
2625
import java.nio.file.OpenOption;
26+
import java.nio.file.Path;
2727
import java.nio.file.StandardOpenOption;
2828
import java.util.Collections;
2929
import java.util.List;
@@ -90,19 +90,14 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType
9090

9191

9292
@Override
93-
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message,
94-
Map<String, Object> hints) {
95-
93+
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
9694
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory));
9795
}
9896

9997

10098
@Override
101-
public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
102-
Map<String, Object> hints) {
103-
104-
return Mono.error(new UnsupportedOperationException(
105-
"Can't read a multipart request body into a single Part."));
99+
public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
100+
return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part"));
106101
}
107102

108103

@@ -118,15 +113,14 @@ private static class SynchronossPartGenerator implements Consumer<FluxSink<Part>
118113

119114
private final PartBodyStreamStorageFactory streamStorageFactory;
120115

121-
122116
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory,
123117
PartBodyStreamStorageFactory streamStorageFactory) {
118+
124119
this.inputMessage = inputMessage;
125120
this.bufferFactory = bufferFactory;
126121
this.streamStorageFactory = streamStorageFactory;
127122
}
128123

129-
130124
@Override
131125
public void accept(FluxSink<Part> emitter) {
132126
HttpHeaders headers = this.inputMessage.getHeaders();
@@ -189,14 +183,12 @@ private static class FluxSinkAdapterListener implements NioMultipartParserListen
189183

190184
private final AtomicInteger terminated = new AtomicInteger(0);
191185

192-
193186
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory factory, MultipartContext context) {
194187
this.sink = sink;
195188
this.bufferFactory = factory;
196189
this.context = context;
197190
}
198191

199-
200192
@Override
201193
public void onPartFinished(StreamStorage storage, Map<String, List<String>> headers) {
202194
HttpHeaders httpHeaders = new HttpHeaders();
@@ -250,16 +242,14 @@ private abstract static class AbstractSynchronossPart implements Part {
250242

251243
private final DataBufferFactory bufferFactory;
252244

253-
254245
AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) {
255246
Assert.notNull(headers, "HttpHeaders is required");
256-
Assert.notNull(bufferFactory, "'bufferFactory' is required");
247+
Assert.notNull(bufferFactory, "DataBufferFactory is required");
257248
this.name = MultipartUtils.getFieldName(headers);
258249
this.headers = headers;
259250
this.bufferFactory = bufferFactory;
260251
}
261252

262-
263253
@Override
264254
public String name() {
265255
return this.name;
@@ -280,14 +270,12 @@ private static class SynchronossPart extends AbstractSynchronossPart {
280270

281271
private final StreamStorage storage;
282272

283-
284273
SynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
285274
super(headers, factory);
286-
Assert.notNull(storage, "'storage' is required");
275+
Assert.notNull(storage, "StreamStorage is required");
287276
this.storage = storage;
288277
}
289278

290-
291279
@Override
292280
public Flux<DataBuffer> content() {
293281
return DataBufferUtils.readInputStream(getStorage()::getInputStream, getBufferFactory(), 4096);
@@ -301,33 +289,28 @@ protected StreamStorage getStorage() {
301289

302290
private static class SynchronossFilePart extends SynchronossPart implements FilePart {
303291

304-
private static final OpenOption[] FILE_CHANNEL_OPTIONS = {
305-
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE };
306-
292+
private static final OpenOption[] FILE_CHANNEL_OPTIONS =
293+
{StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE};
307294

308295
private final String filename;
309296

310-
311-
SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage,
312-
DataBufferFactory factory) {
313-
297+
SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, DataBufferFactory factory) {
314298
super(headers, storage, factory);
315299
this.filename = filename;
316300
}
317301

318-
319302
@Override
320303
public String filename() {
321304
return this.filename;
322305
}
323306

324307
@Override
325-
public Mono<Void> transferTo(File destination) {
308+
public Mono<Void> transferTo(Path dest) {
326309
ReadableByteChannel input = null;
327310
FileChannel output = null;
328311
try {
329312
input = Channels.newChannel(getStorage().getInputStream());
330-
output = FileChannel.open(destination.toPath(), FILE_CHANNEL_OPTIONS);
313+
output = FileChannel.open(dest, FILE_CHANNEL_OPTIONS);
331314
long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE);
332315
long totalWritten = 0;
333316
while (totalWritten < size) {
@@ -366,13 +349,11 @@ private static class SynchronossFormFieldPart extends AbstractSynchronossPart im
366349

367350
private final String content;
368351

369-
370352
SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) {
371353
super(headers, bufferFactory);
372354
this.content = content;
373355
}
374356

375-
376357
@Override
377358
public String value() {
378359
return this.content;

spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.http.server.reactive;
1818

19-
import java.io.File;
19+
import java.nio.file.Path;
2020

2121
import io.netty.buffer.ByteBuf;
2222
import io.netty.handler.codec.http.HttpResponseStatus;
@@ -112,8 +112,8 @@ protected void applyCookies() {
112112
}
113113

114114
@Override
115-
public Mono<Void> writeWith(File file, long position, long count) {
116-
return doCommit(() -> this.response.sendFile(file.toPath(), position, count).then());
115+
public Mono<Void> writeWith(Path file, long position, long count) {
116+
return doCommit(() -> this.response.sendFile(file, position, count).then());
117117
}
118118

119119
private static Publisher<ByteBuf> toByteBufs(Publisher<? extends DataBuffer> dataBuffers) {

spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java

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

1717
package org.springframework.http.server.reactive;
1818

19-
import java.io.File;
2019
import java.io.IOException;
2120
import java.nio.ByteBuffer;
2221
import java.nio.channels.FileChannel;
22+
import java.nio.file.Path;
2323
import java.nio.file.StandardOpenOption;
2424

2525
import io.undertow.server.HttpServerExchange;
@@ -106,10 +106,10 @@ protected void applyCookies() {
106106
}
107107

108108
@Override
109-
public Mono<Void> writeWith(File file, long position, long count) {
109+
public Mono<Void> writeWith(Path file, long position, long count) {
110110
return doCommit(() ->
111111
Mono.defer(() -> {
112-
try (FileChannel source = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
112+
try (FileChannel source = FileChannel.open(file, StandardOpenOption.READ)) {
113113
StreamSinkChannel destination = this.exchange.getResponseChannel();
114114
Channels.transferBlocking(destination, source, position, count);
115115
return Mono.empty();

spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
2224

2325
import org.springframework.core.io.InputStreamSource;
2426
import org.springframework.core.io.Resource;
2527
import org.springframework.lang.Nullable;
28+
import org.springframework.util.FileCopyUtils;
2629

2730
/**
2831
* A representation of an uploaded file received in a multipart request.
@@ -127,4 +130,15 @@ default Resource getResource() {
127130
*/
128131
void transferTo(File dest) throws IOException, IllegalStateException;
129132

133+
/**
134+
* Transfer the received file to the given destination file.
135+
* <p>The default implementation simply copies the file input stream.
136+
* @since 5.1
137+
* @see #getInputStream()
138+
* @see #transferTo(File)
139+
*/
140+
default void transferTo(Path dest) throws IOException, IllegalStateException {
141+
FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
142+
}
143+
130144
}

spring-web/src/main/java/org/springframework/web/multipart/MultipartFileResource.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package org.springframework.web.multipart;
1718

1819
import java.io.IOException;
@@ -35,7 +36,7 @@ class MultipartFileResource extends AbstractResource {
3536

3637

3738
public MultipartFileResource(MultipartFile multipartFile) {
38-
Assert.notNull(multipartFile, "MultipartFile must not be null.");
39+
Assert.notNull(multipartFile, "MultipartFile must not be null");
3940
this.multipartFile = multipartFile;
4041
}
4142

@@ -86,9 +87,8 @@ public String getDescription() {
8687

8788
@Override
8889
public boolean equals(Object obj) {
89-
return (obj == this ||
90-
(obj instanceof MultipartFileResource &&
91-
((MultipartFileResource) obj).multipartFile.equals(this.multipartFile)));
90+
return (obj == this || (obj instanceof MultipartFileResource &&
91+
((MultipartFileResource) obj).multipartFile.equals(this.multipartFile)));
9292
}
9393

9494
@Override

0 commit comments

Comments
 (0)