From 31a9de94c1141195692f8dcd6302d86699bf1ff5 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Thu, 6 Aug 2015 10:57:16 +0200 Subject: [PATCH] Implement range support for serving files, this will allow us for example on web servers to support range requests, resumable downloads, etc... Signed-off-by: Paulo Lopes (cherry picked from commit bcb6cb0) --- .../vertx/core/http/HttpServerResponse.java | 68 ++++++++++++++++++- .../http/impl/HttpServerResponseImpl.java | 18 ++--- .../core/http/impl/ServerConnection.java | 4 +- .../java/io/vertx/core/net/NetSocket.java | 60 +++++++++++++++- .../vertx/core/net/impl/ConnectionBase.java | 6 +- .../io/vertx/core/net/impl/NetSocketImpl.java | 8 +-- .../vertx/test/core/FileResolverTestBase.java | 30 ++++++++ 7 files changed, 171 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/vertx/core/http/HttpServerResponse.java b/src/main/java/io/vertx/core/http/HttpServerResponse.java index d34e13cff8d..ccf78e02bf0 100644 --- a/src/main/java/io/vertx/core/http/HttpServerResponse.java +++ b/src/main/java/io/vertx/core/http/HttpServerResponse.java @@ -262,7 +262,40 @@ public interface HttpServerResponse extends WriteStream { * @return a reference to this, so the API can be used fluently */ @Fluent - HttpServerResponse sendFile(String filename); + default HttpServerResponse sendFile(String filename) { + return sendFile(filename, 0); + } + + /** + * Ask the OS to stream a file as specified by {@code filename} directly + * from disk to the outgoing connection, bypassing userspace altogether + * (where supported by the underlying operating system. + * This is a very efficient way to serve files.

+ * The actual serve is asynchronous and may not complete until some time after this method has returned. + * + * @param filename path to the file to serve + * @param offset offset to start serving from + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default HttpServerResponse sendFile(String filename, long offset) { + return sendFile(filename, offset, Long.MAX_VALUE); + } + + /** + * Ask the OS to stream a file as specified by {@code filename} directly + * from disk to the outgoing connection, bypassing userspace altogether + * (where supported by the underlying operating system. + * This is a very efficient way to serve files.

+ * The actual serve is asynchronous and may not complete until some time after this method has returned. + * + * @param filename path to the file to serve + * @param offset offset to start serving from + * @param length length to serve to + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpServerResponse sendFile(String filename, long offset, long length); /** * Like {@link #sendFile(String)} but providing a handler which will be notified once the file has been completely @@ -273,7 +306,36 @@ public interface HttpServerResponse extends WriteStream { * @return a reference to this, so the API can be used fluently */ @Fluent - HttpServerResponse sendFile(String filename, Handler> resultHandler); + default HttpServerResponse sendFile(String filename, Handler> resultHandler) { + return sendFile(filename, 0, resultHandler); + } + + /** + * Like {@link #sendFile(String, long)} but providing a handler which will be notified once the file has been completely + * written to the wire. + * + * @param filename path to the file to serve + * @param offset the offset to serve from + * @param resultHandler handler that will be called on completion + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default HttpServerResponse sendFile(String filename, long offset, Handler> resultHandler) { + return sendFile(filename, offset, Long.MAX_VALUE, resultHandler); + } + + /** + * Like {@link #sendFile(String, long, long)} but providing a handler which will be notified once the file has been + * completely written to the wire. + * + * @param filename path to the file to serve + * @param offset the offset to serve from + * @param length the length to serve to + * @param resultHandler handler that will be called on completion + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpServerResponse sendFile(String filename, long offset, long length, Handler> resultHandler); /** * Close the underlying TCP connection corresponding to the request. @@ -300,7 +362,7 @@ public interface HttpServerResponse extends WriteStream { * @return a reference to this, so the API can be used fluently */ @Fluent - HttpServerResponse headersEndHandler(Handler> handler); + HttpServerResponse headersEndHandler(Handler> handler); /** * Provide a handler that will be called just before the last part of the body is written to the wire diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java b/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java index 746cdfb769d..aaf8b0eab1c 100644 --- a/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java +++ b/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java @@ -63,7 +63,7 @@ public class HttpServerResponseImpl implements HttpServerResponse { private Handler drainHandler; private Handler exceptionHandler; private Handler closeHandler; - private Handler> headersEndHandler; + private Handler> headersEndHandler; private Handler bodyEndHandler; private boolean chunked; private boolean closed; @@ -328,14 +328,14 @@ public void end() { } @Override - public HttpServerResponseImpl sendFile(String filename) { - doSendFile(filename, null); + public HttpServerResponseImpl sendFile(String filename, long offset, long length) { + doSendFile(filename, offset, length, null); return this; } @Override - public HttpServerResponse sendFile(String filename, Handler> resultHandler) { - doSendFile(filename, resultHandler); + public HttpServerResponse sendFile(String filename, long start, long end, Handler> resultHandler) { + doSendFile(filename, start, end, resultHandler); return this; } @@ -354,7 +354,7 @@ public boolean headWritten() { } @Override - public HttpServerResponse headersEndHandler(Handler> handler) { + public HttpServerResponse headersEndHandler(Handler> handler) { synchronized (conn) { this.headersEndHandler = handler; return this; @@ -412,7 +412,7 @@ private void end0(ByteBuf data) { } } - private void doSendFile(String filename, Handler> resultHandler) { + private void doSendFile(String filename, long offset, long length, Handler> resultHandler) { synchronized (conn) { if (headWritten) { throw new IllegalStateException("Head already written"); @@ -421,7 +421,7 @@ private void doSendFile(String filename, Handler> resultHandle File file = vertx.resolveFile(filename); long fileLength = file.length(); if (!contentLengthSet()) { - putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength)); + putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(Math.min(length, fileLength - offset))); } if (!contentTypeSet()) { int li = filename.lastIndexOf('.'); @@ -439,7 +439,7 @@ private void doSendFile(String filename, Handler> resultHandle try { raf = new RandomAccessFile(file, "r"); conn.queueForWrite(response); - conn.sendFile(raf, fileLength); + conn.sendFile(raf, Math.min(offset, fileLength), Math.min(length, fileLength - offset)); } catch (IOException e) { if (resultHandler != null) { ContextImpl ctx = vertx.getOrCreateContext(); diff --git a/src/main/java/io/vertx/core/http/impl/ServerConnection.java b/src/main/java/io/vertx/core/http/impl/ServerConnection.java index 4ad8f581669..d086d44ab9f 100644 --- a/src/main/java/io/vertx/core/http/impl/ServerConnection.java +++ b/src/main/java/io/vertx/core/http/impl/ServerConnection.java @@ -369,8 +369,8 @@ protected boolean supportsFileRegion() { return super.supportsFileRegion() && channel.pipeline().get(HttpChunkContentCompressor.class) == null; } - protected ChannelFuture sendFile(RandomAccessFile file, long fileLength) throws IOException { - return super.sendFile(file, fileLength); + protected ChannelFuture sendFile(RandomAccessFile file, long offset, long length) throws IOException { + return super.sendFile(file, offset, length); } private void processMessage(Object msg) { diff --git a/src/main/java/io/vertx/core/net/NetSocket.java b/src/main/java/io/vertx/core/net/NetSocket.java index fd211850612..fec0afcb6a8 100644 --- a/src/main/java/io/vertx/core/net/NetSocket.java +++ b/src/main/java/io/vertx/core/net/NetSocket.java @@ -108,7 +108,34 @@ public interface NetSocket extends ReadStream, WriteStream { * @return a reference to this, so the API can be used fluently */ @Fluent - NetSocket sendFile(String filename); + default NetSocket sendFile(String filename) { + return sendFile(filename, 0, Long.MAX_VALUE); + } + + /** + * Tell the operating system to stream a file as specified by {@code filename} directly from disk to the outgoing connection, + * bypassing userspace altogether (where supported by the underlying operating system. This is a very efficient way to stream files. + * + * @param filename file name of the file to send + * @param offset offset + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default NetSocket sendFile(String filename, long offset) { + return sendFile(filename, offset, Long.MAX_VALUE); + } + + /** + * Tell the operating system to stream a file as specified by {@code filename} directly from disk to the outgoing connection, + * bypassing userspace altogether (where supported by the underlying operating system. This is a very efficient way to stream files. + * + * @param filename file name of the file to send + * @param offset offset + * @param length length + * @return a reference to this, so the API can be used fluently + */ + @Fluent + NetSocket sendFile(String filename, long offset, long length); /** * Same as {@link #sendFile(String)} but also takes a handler that will be called when the send has completed or @@ -119,7 +146,36 @@ public interface NetSocket extends ReadStream, WriteStream { * @return a reference to this, so the API can be used fluently */ @Fluent - NetSocket sendFile(String filename, Handler> resultHandler); + default NetSocket sendFile(String filename, Handler> resultHandler) { + return sendFile(filename, 0, Long.MAX_VALUE, resultHandler); + } + + /** + * Same as {@link #sendFile(String, long)} but also takes a handler that will be called when the send has completed or + * a failure has occurred + * + * @param filename file name of the file to send + * @param offset offset + * @param resultHandler handler + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default NetSocket sendFile(String filename, long offset, Handler> resultHandler) { + return sendFile(filename, offset, Long.MAX_VALUE, resultHandler); + } + + /** + * Same as {@link #sendFile(String, long, long)} but also takes a handler that will be called when the send has completed or + * a failure has occurred + * + * @param filename file name of the file to send + * @param offset offset + * @param length length + * @param resultHandler handler + * @return a reference to this, so the API can be used fluently + */ + @Fluent + NetSocket sendFile(String filename, long offset, long length, Handler> resultHandler); /** * @return the remote address for this socket diff --git a/src/main/java/io/vertx/core/net/impl/ConnectionBase.java b/src/main/java/io/vertx/core/net/impl/ConnectionBase.java index 4af16317c6e..05014b7ce51 100644 --- a/src/main/java/io/vertx/core/net/impl/ConnectionBase.java +++ b/src/main/java/io/vertx/core/net/impl/ConnectionBase.java @@ -197,15 +197,15 @@ private boolean isSSL() { return channel.pipeline().get(SslHandler.class) != null; } - protected ChannelFuture sendFile(RandomAccessFile raf, long fileLength) throws IOException { + protected ChannelFuture sendFile(RandomAccessFile raf, long offset, long length) throws IOException { // Write the content. ChannelFuture writeFuture; if (!supportsFileRegion()) { // Cannot use zero-copy - writeFuture = writeToChannel(new ChunkedFile(raf, 0, fileLength, 8192)); + writeFuture = writeToChannel(new ChunkedFile(raf, offset, length, 8192)); } else { // No encryption - use zero-copy. - FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, fileLength); + FileRegion region = new DefaultFileRegion(raf.getChannel(), offset, length); writeFuture = writeToChannel(region); } if (writeFuture != null) { diff --git a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java index 5488f53ae1d..8c80a0bd605 100644 --- a/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java +++ b/src/main/java/io/vertx/core/net/impl/NetSocketImpl.java @@ -183,19 +183,19 @@ public synchronized NetSocket drainHandler(Handler drainHandler) { } @Override - public NetSocket sendFile(String filename) { - return sendFile(filename, null); + public NetSocket sendFile(String filename, long offset, long length) { + return sendFile(filename, offset, length, null); } @Override - public NetSocket sendFile(String filename, final Handler> resultHandler) { + public NetSocket sendFile(String filename, long offset, long length, final Handler> resultHandler) { File f = vertx.resolveFile(filename); if (f.isDirectory()) { throw new IllegalArgumentException("filename must point to a file and not to a directory"); } try { RandomAccessFile raf = new RandomAccessFile(f, "r"); - ChannelFuture future = super.sendFile(raf, f.length()); + ChannelFuture future = super.sendFile(raf, Math.min(offset, f.length()), Math.min(length, f.length() - offset)); if (resultHandler != null) { future.addListener(fut -> { final AsyncResult res; diff --git a/src/test/java/io/vertx/test/core/FileResolverTestBase.java b/src/test/java/io/vertx/test/core/FileResolverTestBase.java index 89654933989..06d61b6e0c8 100644 --- a/src/test/java/io/vertx/test/core/FileResolverTestBase.java +++ b/src/test/java/io/vertx/test/core/FileResolverTestBase.java @@ -230,6 +230,36 @@ public void testSendFileFromClasspath() { await(); } + @Test + public void testSendOpenRangeFileFromClasspath() { + vertx.createHttpServer(new HttpServerOptions().setPort(8080)).requestHandler(res -> { + res.response().sendFile(webRoot + "/somefile.html", 6); + }).listen(onSuccess(res -> { + vertx.createHttpClient(new HttpClientOptions()).request(HttpMethod.GET, 8080, "localhost", "/", resp -> { + resp.bodyHandler(buff -> { + assertTrue(buff.toString().startsWith("blah")); + testComplete(); + }); + }).end(); + })); + await(); + } + + @Test + public void testSendRangeFileFromClasspath() { + vertx.createHttpServer(new HttpServerOptions().setPort(8080)).requestHandler(res -> { + res.response().sendFile(webRoot + "/somefile.html", 6, 6); + }).listen(onSuccess(res -> { + vertx.createHttpClient(new HttpClientOptions()).request(HttpMethod.GET, 8080, "localhost", "/", resp -> { + resp.bodyHandler(buff -> { + assertEquals("", buff.toString()); + testComplete(); + }); + }).end(); + })); + await(); + } + private String readFile(File file) { return vertx.fileSystem().readFileBlocking(file.getAbsolutePath()).toString(); }