Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions vertx-core/src/main/java/io/vertx/core/http/HttpServerResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,18 @@ default Future<Void> sendFile(String filename) {
return sendFile(filename, 0);
}

/**
* Same as {@link #sendFile(String, long, long, SendFileOptions)} with offset {@code 0} and length
* {@code Long.MAX_VALUE} which means until the end of the file.
*
* @param filename path to the file to serve
* @param options send-file options
* @return a future completed with the body result
*/
default Future<Void> sendFile(String filename, SendFileOptions options) {
return sendFile(filename, 0, options);
}

/**
* Same as {@link #sendFile(String, long, long)} using length @code{Long.MAX_VALUE} which means until the end of the
* file.
Expand All @@ -373,6 +385,19 @@ default Future<Void> sendFile(String filename, long offset) {
return sendFile(filename, offset, Long.MAX_VALUE);
}

/**
* Same as {@link #sendFile(String, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
* which means until the end of the file.
*
* @param filename path to the file to serve
* @param offset offset to start serving from
* @param options send-file options
* @return a future completed with the body result
*/
default Future<Void> sendFile(String filename, long offset, SendFileOptions options) {
return sendFile(filename, offset, Long.MAX_VALUE, options);
}

/**
* Ask the OS to stream a file as specified by {@code filename} directly
* from disk to the outgoing connection, bypassing userspace altogether
Expand All @@ -391,6 +416,22 @@ default Future<Void> sendFile(String filename, long offset) {
*/
Future<Void> sendFile(String filename, long offset, long length);

/**
* Same as {@link #sendFile(String, long, long)} with send-file options.
* <p>
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
* It is ignored when the file-region/sendfile path is used.
*
* @param filename path to the file to serve
* @param offset offset to start serving from
* @param length the number of bytes to send
* @param options send-file options
* @return a future completed with the body result
*/
default Future<Void> sendFile(String filename, long offset, long length, SendFileOptions options) {
return sendFile(filename, offset, length);
}

/**
* Same as {@link #sendFile(FileChannel, long)} using length @code{Long.MAX_VALUE} which means until the end of the
* file.
Expand All @@ -408,6 +449,20 @@ default Future<Void> sendFile(FileChannel channel) {
return sendFile(channel, 0);
}

/**
* Same as {@link #sendFile(FileChannel, long, long, SendFileOptions)} with offset {@code 0} and length
* {@code Long.MAX_VALUE} which means until the end of the file.
*
* @param channel the file channel to the file to serve
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(FileChannel channel, SendFileOptions options) {
return sendFile(channel, 0, options);
}

/**
* Same as {@link #sendFile(FileChannel, long, long)} using length @code{Long.MAX_VALUE} which means until the end of the
* file.
Expand All @@ -426,6 +481,21 @@ default Future<Void> sendFile(FileChannel channel, long offset) {
return sendFile(channel, offset, Long.MAX_VALUE);
}

/**
* Same as {@link #sendFile(FileChannel, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
* which means until the end of the file.
*
* @param channel the file channel to the file to serve
* @param offset offset to start serving from
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(FileChannel channel, long offset, SendFileOptions options) {
return sendFile(channel, offset, Long.MAX_VALUE, options);
}

/**
* Ask the OS to stream a file as specified by {@code channel} directly
* from disk to the outgoing connection, bypassing userspace altogether
Expand All @@ -449,6 +519,24 @@ default Future<Void> sendFile(FileChannel channel, long offset) {
@Unstable
Future<Void> sendFile(FileChannel channel, long offset, long length);

/**
* Same as {@link #sendFile(FileChannel, long, long)} with send-file options.
* <p>
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
* It is ignored when the file-region/sendfile path is used.
*
* @param channel the file channel to the file to serve
* @param offset offset to start serving from
* @param length the number of bytes to send
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(FileChannel channel, long offset, long length, SendFileOptions options) {
return sendFile(channel, offset, length);
}

/**
* Same as {@link #sendFile(FileChannel)} with {@link RandomAccessFile}
* <p>
Expand All @@ -465,6 +553,20 @@ default Future<Void> sendFile(RandomAccessFile file) {
return sendFile(file, 0);
}

/**
* Same as {@link #sendFile(RandomAccessFile, long, long, SendFileOptions)} with offset {@code 0} and length
* {@code Long.MAX_VALUE} which means until the end of the file.
*
* @param file the file to serve
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(RandomAccessFile file, SendFileOptions options) {
return sendFile(file, 0, options);
}

/**
*
* Same as {@link #sendFile(FileChannel, long)} with {@link RandomAccessFile}
Expand All @@ -483,6 +585,21 @@ default Future<Void> sendFile(RandomAccessFile file, long offset) {
return sendFile(file, offset, Long.MAX_VALUE);
}

/**
* Same as {@link #sendFile(RandomAccessFile, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
* which means until the end of the file.
*
* @param file the file to serve
* @param offset offset to start serving from
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(RandomAccessFile file, long offset, SendFileOptions options) {
return sendFile(file, offset, Long.MAX_VALUE, options);
}

/**
* Same as {@link #sendFile(FileChannel, long, long)} with {@link RandomAccessFile}
* <p>
Expand All @@ -499,6 +616,24 @@ default Future<Void> sendFile(RandomAccessFile file, long offset) {
@Unstable
Future<Void> sendFile(RandomAccessFile file, long offset, long length);

/**
* Same as {@link #sendFile(RandomAccessFile, long, long)} with send-file options.
* <p>
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
* It is ignored when the file-region/sendfile path is used.
*
* @param file the file to serve
* @param offset offset to start serving from
* @param length the number of bytes to send
* @param options send-file options
* @return a future completed with the body result
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
@Unstable
default Future<Void> sendFile(RandomAccessFile file, long offset, long length, SendFileOptions options) {
return sendFile(file, offset, length);
}

/**
* @return has the response already ended?
*/
Expand Down
90 changes: 90 additions & 0 deletions vertx-core/src/main/java/io/vertx/core/http/SendFileOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
package io.vertx.core.http;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.annotations.Unstable;
import io.vertx.codegen.json.annotations.JsonGen;
import io.vertx.core.impl.Arguments;
import io.vertx.core.json.JsonObject;

/**
* Options controlling how {@link HttpServerResponse#sendFile(String)} streams a file when
* the file-region/sendfile path cannot be used.
*/
@DataObject
@JsonGen(publicConverter = false)
@Unstable
public class SendFileOptions {

/**
* The default chunk size used by the send-file fallback path.
*/
public static final int DEFAULT_CHUNK_SIZE = 8192;

private int chunkSize;

/**
* Create default send-file options.
*/
public SendFileOptions() {
chunkSize = DEFAULT_CHUNK_SIZE;
}

/**
* Create send-file options from JSON.
*
* @param json the JSON object
*/
public SendFileOptions(JsonObject json) {
this();
SendFileOptionsConverter.fromJson(json, this);
}

/**
* Copy constructor.
*
* @param other the options to copy
*/
public SendFileOptions(SendFileOptions other) {
chunkSize = other.chunkSize;
}

/**
* @return the chunk size, in bytes, used when the file is streamed through the fallback path
*/
public int getChunkSize() {
return chunkSize;
}

/**
* Set the chunk size, in bytes, used when the file is streamed through the fallback path.
* <p>
* This option is ignored when the file can be transferred using the file-region/sendfile path.
*
* @param chunkSize the chunk size in bytes
* @return a reference to this, so the API can be used fluently
*/
public SendFileOptions setChunkSize(int chunkSize) {
Arguments.require(chunkSize > 0, "chunkSize must be > 0");
this.chunkSize = chunkSize;
return this;
}

/**
* @return a JSON representation of these options
*/
public JsonObject toJson() {
JsonObject json = new JsonObject();
SendFileOptionsConverter.toJson(this, json);
return json;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,11 @@ public HttpServerResponse drainHandler(Handler<Void> handler) {

@Override
public Future<Void> sendFile(String filename, long offset, long length) {
return sendFile(filename, offset, length, new SendFileOptions());
}

@Override
public Future<Void> sendFile(String filename, long offset, long length, SendFileOptions options) {
if (offset < 0) {
return context.failedFuture("offset : " + offset + " (expected: >= 0)");
}
Expand All @@ -521,14 +526,19 @@ public Future<Void> sendFile(String filename, long offset, long length) {
checkValid();
}
if (conn.supportsSendFile()) {
return sendFileInternal(filename, offset, length);
return sendFileInternal(filename, offset, length, options);
} else {
return sendAsyncFile(filename, offset, length);
return sendAsyncFile(filename, offset, length, options);
}
}

@Override
public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
return sendFile(file, offset, length, new SendFileOptions());
}

@Override
public Future<Void> sendFile(RandomAccessFile file, long offset, long length, SendFileOptions options) {
if (!headersMap.contains(io.vertx.core.http.HttpHeaders.CONTENT_TYPE)) {
headersMap.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
}
Expand All @@ -538,11 +548,16 @@ public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
} catch (IOException e) {
return context.failedFuture(e);
}
return sendFileInternal(offset, length, size, file, null, false);
return sendFileInternal(offset, length, size, file, null, false, options);
}

@Override
public Future<Void> sendFile(FileChannel channel, long offset, long length) {
return sendFile(channel, offset, length, new SendFileOptions());
}

@Override
public Future<Void> sendFile(FileChannel channel, long offset, long length, SendFileOptions options) {
if (!headersMap.contains(io.vertx.core.http.HttpHeaders.CONTENT_TYPE)) {
headersMap.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
}
Expand All @@ -552,13 +567,14 @@ public Future<Void> sendFile(FileChannel channel, long offset, long length) {
} catch (IOException e) {
return context.failedFuture(e);
}
return sendFileInternal(offset, length, size, null, channel, false);
return sendFileInternal(offset, length, size, null, channel, false, options);
}

private Future<Void> sendAsyncFile(String filename, long offset, long length) {
private Future<Void> sendAsyncFile(String filename, long offset, long length, SendFileOptions options) {
return HttpUtils
.resolveFile(context, filename, offset, length)
.compose(file -> {
file.setReadBufferSize(options.getChunkSize());
long fileLength = file.getReadLength();
long contentLength = Math.min(length, fileLength);
// fail early before status code/headers are written to the response
Expand All @@ -578,7 +594,7 @@ private Future<Void> sendAsyncFile(String filename, long offset, long length) {
});
}

private Future<Void> sendFileInternal(String filename, long offset, long length) {
private Future<Void> sendFileInternal(String filename, long offset, long length, SendFileOptions options) {
File file = context.owner().fileResolver().resolve(filename);
long size;
RandomAccessFile raf;
Expand All @@ -595,10 +611,10 @@ private Future<Void> sendFileInternal(String filename, long offset, long length)
}
headersMap.set(CONTENT_TYPE, mimeType);
}
return sendFileInternal(offset, length, size, raf, null, true);
return sendFileInternal(offset, length, size, raf, null, true, options);
}

private Future<Void> sendFileInternal(long offset, long length, long size, RandomAccessFile file, FileChannel channel, boolean close) {
private Future<Void> sendFileInternal(long offset, long length, long size, RandomAccessFile file, FileChannel channel, boolean close, SendFileOptions options) {
Future<Void> fut = null;
try {
long actualLength = Math.min(length, size - offset);
Expand All @@ -612,9 +628,9 @@ private Future<Void> sendFileInternal(long offset, long length, long size, Rando
channel = file.getChannel();
}
if (close) {
chunkedFile = new ChunkedNioFile(channel, actualOffset, actualLength, 8192);
chunkedFile = new ChunkedNioFile(channel, actualOffset, actualLength, options.getChunkSize());
} else {
chunkedFile = new UncloseableChunkedNioFile(channel, actualOffset, actualLength);
chunkedFile = new UncloseableChunkedNioFile(channel, actualOffset, actualLength, options.getChunkSize());
}
} catch (IOException e) {
return context.failedFuture(e);
Expand Down
Loading