diff --git a/spring-web/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java b/spring-web/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java index 100411004d..863a6e86a2 100644 --- a/spring-web/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java +++ b/spring-web/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.http; import java.io.File; +import java.nio.file.Path; import reactor.core.publisher.Mono; @@ -25,6 +26,7 @@ import reactor.core.publisher.Mono; * file transfers. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.0 * @see Zero-copy */ @@ -38,6 +40,19 @@ public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage { * @param count the number of bytes to be transferred * @return a publisher that indicates completion or error. */ - Mono writeWith(File file, long position, long count); + default Mono writeWith(File file, long position, long count) { + return writeWith(file.toPath(), position, count); + } + + /** + * Use the given {@link Path} to write the body of the message to the underlying + * HTTP layer. + * @param file the file to transfer + * @param position the position within the file from which the transfer is to begin + * @param count the number of bytes to be transferred + * @return a publisher that indicates completion or error. + * @since 5.1 + */ + Mono writeWith(Path file, long position, long count); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index a74a1033d5..0cb25e249d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -16,8 +16,8 @@ package org.springframework.http.client.reactive; -import java.io.File; import java.net.URI; +import java.nio.file.Path; import java.util.Collection; import io.netty.buffer.ByteBuf; @@ -97,8 +97,8 @@ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements Zero } @Override - public Mono writeWith(File file, long position, long count) { - return doCommit(() -> this.outbound.sendFile(file.toPath(), position, count).then()); + public Mono writeWith(Path file, long position, long count) { + return doCommit(() -> this.outbound.sendFile(file, position, count).then()); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FilePart.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FilePart.java index e1237ce76e..df05a5b2fa 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/FilePart.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FilePart.java @@ -17,6 +17,7 @@ package org.springframework.http.codec.multipart; import java.io.File; +import java.nio.file.Path; import reactor.core.publisher.Mono; @@ -25,6 +26,7 @@ import reactor.core.publisher.Mono; * a multipart request. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ public interface FilePart extends Part { @@ -38,10 +40,26 @@ public interface FilePart extends Part { * Convenience method to copy the content of the file in this part to the * given destination file. If the destination file already exists, it will * be truncated first. + *

The default implementation delegates to {@link #transferTo(Path)}. * @param dest the target file * @return completion {@code Mono} with the result of the file transfer, * possibly {@link IllegalStateException} if the part isn't a file + * @see #transferTo(Path) */ - Mono transferTo(File dest); + default Mono transferTo(File dest) { + return transferTo(dest.toPath()); + } + + /** + * Convenience method to copy the content of the file in this part to the + * given destination file. If the destination file already exists, it will + * be truncated first. + * @param dest the target file + * @return completion {@code Mono} with the result of the file transfer, + * possibly {@link IllegalStateException} if the part isn't a file + * @since 5.1 + * @see #transferTo(File) + */ + Mono transferTo(Path dest); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index 0a362dfc3e..4e4974b194 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -16,7 +16,6 @@ package org.springframework.http.codec.multipart; -import java.io.File; import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; @@ -24,6 +23,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.OpenOption; +import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; @@ -90,19 +90,14 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader @Override - public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, - Map hints) { - + public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory)); } @Override - public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage message, - Map hints) { - - return Mono.error(new UnsupportedOperationException( - "Can't read a multipart request body into a single Part.")); + public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { + return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } @@ -118,15 +113,14 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private final PartBodyStreamStorageFactory streamStorageFactory; - SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory, PartBodyStreamStorageFactory streamStorageFactory) { + this.inputMessage = inputMessage; this.bufferFactory = bufferFactory; this.streamStorageFactory = streamStorageFactory; } - @Override public void accept(FluxSink emitter) { HttpHeaders headers = this.inputMessage.getHeaders(); @@ -189,14 +183,12 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private final AtomicInteger terminated = new AtomicInteger(0); - FluxSinkAdapterListener(FluxSink sink, DataBufferFactory factory, MultipartContext context) { this.sink = sink; this.bufferFactory = factory; this.context = context; } - @Override public void onPartFinished(StreamStorage storage, Map> headers) { HttpHeaders httpHeaders = new HttpHeaders(); @@ -250,16 +242,14 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private final DataBufferFactory bufferFactory; - AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) { Assert.notNull(headers, "HttpHeaders is required"); - Assert.notNull(bufferFactory, "'bufferFactory' is required"); + Assert.notNull(bufferFactory, "DataBufferFactory is required"); this.name = MultipartUtils.getFieldName(headers); this.headers = headers; this.bufferFactory = bufferFactory; } - @Override public String name() { return this.name; @@ -280,14 +270,12 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private final StreamStorage storage; - SynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) { super(headers, factory); - Assert.notNull(storage, "'storage' is required"); + Assert.notNull(storage, "StreamStorage is required"); this.storage = storage; } - @Override public Flux content() { return DataBufferUtils.readInputStream(getStorage()::getInputStream, getBufferFactory(), 4096); @@ -301,33 +289,28 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private static class SynchronossFilePart extends SynchronossPart implements FilePart { - private static final OpenOption[] FILE_CHANNEL_OPTIONS = { - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE }; - + private static final OpenOption[] FILE_CHANNEL_OPTIONS = + {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE}; private final String filename; - - SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, - DataBufferFactory factory) { - + SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, DataBufferFactory factory) { super(headers, storage, factory); this.filename = filename; } - @Override public String filename() { return this.filename; } @Override - public Mono transferTo(File destination) { + public Mono transferTo(Path dest) { ReadableByteChannel input = null; FileChannel output = null; try { input = Channels.newChannel(getStorage().getInputStream()); - output = FileChannel.open(destination.toPath(), FILE_CHANNEL_OPTIONS); + output = FileChannel.open(dest, FILE_CHANNEL_OPTIONS); long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE); long totalWritten = 0; while (totalWritten < size) { @@ -366,13 +349,11 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader private final String content; - SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) { super(headers, bufferFactory); this.content = content; } - @Override public String value() { return this.content; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index c46908cd4d..591924dce1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive; -import java.io.File; +import java.nio.file.Path; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.HttpResponseStatus; @@ -112,8 +112,8 @@ class ReactorServerHttpResponse extends AbstractServerHttpResponse implements Ze } @Override - public Mono writeWith(File file, long position, long count) { - return doCommit(() -> this.response.sendFile(file.toPath(), position, count).then()); + public Mono writeWith(Path file, long position, long count) { + return doCommit(() -> this.response.sendFile(file, position, count).then()); } private static Publisher toByteBufs(Publisher dataBuffers) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 58aed42f29..b7aa467ab8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -16,10 +16,10 @@ package org.springframework.http.server.reactive; -import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.file.Path; import java.nio.file.StandardOpenOption; import io.undertow.server.HttpServerExchange; @@ -106,10 +106,10 @@ class UndertowServerHttpResponse extends AbstractListenerServerHttpResponse impl } @Override - public Mono writeWith(File file, long position, long count) { + public Mono writeWith(Path file, long position, long count) { return doCommit(() -> Mono.defer(() -> { - try (FileChannel source = FileChannel.open(file.toPath(), StandardOpenOption.READ)) { + try (FileChannel source = FileChannel.open(file, StandardOpenOption.READ)) { StreamSinkChannel destination = this.exchange.getResponseChannel(); Channels.transferBlocking(destination, source, position, count); return Mono.empty(); diff --git a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java index fdfdec68fd..09ee1372e8 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java @@ -19,10 +19,13 @@ package org.springframework.web.multipart; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import org.springframework.core.io.InputStreamSource; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; /** * A representation of an uploaded file received in a multipart request. @@ -127,4 +130,15 @@ public interface MultipartFile extends InputStreamSource { */ void transferTo(File dest) throws IOException, IllegalStateException; + /** + * Transfer the received file to the given destination file. + *

The default implementation simply copies the file input stream. + * @since 5.1 + * @see #getInputStream() + * @see #transferTo(File) + */ + default void transferTo(Path dest) throws IOException, IllegalStateException { + FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest)); + } + } diff --git a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFileResource.java b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFileResource.java index def7861d7c..73e0e021a4 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFileResource.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFileResource.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.multipart; import java.io.IOException; @@ -35,7 +36,7 @@ class MultipartFileResource extends AbstractResource { public MultipartFileResource(MultipartFile multipartFile) { - Assert.notNull(multipartFile, "MultipartFile must not be null."); + Assert.notNull(multipartFile, "MultipartFile must not be null"); this.multipartFile = multipartFile; } @@ -86,9 +87,8 @@ class MultipartFileResource extends AbstractResource { @Override public boolean equals(Object obj) { - return (obj == this || - (obj instanceof MultipartFileResource && - ((MultipartFileResource) obj).multipartFile.equals(this.multipartFile))); + return (obj == this || (obj instanceof MultipartFileResource && + ((MultipartFileResource) obj).multipartFile.equals(this.multipartFile))); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java index a483fe8507..e1c9585e7e 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java @@ -20,6 +20,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; @@ -27,6 +29,7 @@ import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; import org.springframework.web.multipart.MultipartFile; @@ -185,6 +188,15 @@ public class CommonsMultipartFile implements MultipartFile, Serializable { } } + @Override + public void transferTo(Path dest) throws IOException, IllegalStateException { + if (!isAvailable()) { + throw new IllegalStateException("File has already been moved - cannot be transferred again"); + } + + FileCopyUtils.copy(this.fileItem.getInputStream(), Files.newOutputStream(dest)); + } + /** * Determine whether the multipart content is still available. * If a temporary file has been moved, the content is no longer available. diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java index fa6ae7dbda..c0273ff82c 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -263,6 +264,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath())); } } + + @Override + public void transferTo(Path dest) throws IOException, IllegalStateException { + FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest)); + } }