Skip to content

Commit 018f879

Browse files
committed
Configurable chunk size for chunked send file
Signed-off-by: doxlik <doxlikx@gmail.com>
1 parent 3bb9021 commit 018f879

8 files changed

Lines changed: 384 additions & 18 deletions

File tree

vertx-core/src/main/java/io/vertx/core/http/HttpServerResponse.java

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,18 @@ default Future<Void> sendFile(String filename) {
357357
return sendFile(filename, 0);
358358
}
359359

360+
/**
361+
* Same as {@link #sendFile(String, long, long, SendFileOptions)} with offset {@code 0} and length
362+
* {@code Long.MAX_VALUE} which means until the end of the file.
363+
*
364+
* @param filename path to the file to serve
365+
* @param options send-file options
366+
* @return a future completed with the body result
367+
*/
368+
default Future<Void> sendFile(String filename, SendFileOptions options) {
369+
return sendFile(filename, 0, options);
370+
}
371+
360372
/**
361373
* Same as {@link #sendFile(String, long, long)} using length @code{Long.MAX_VALUE} which means until the end of the
362374
* file.
@@ -373,6 +385,19 @@ default Future<Void> sendFile(String filename, long offset) {
373385
return sendFile(filename, offset, Long.MAX_VALUE);
374386
}
375387

388+
/**
389+
* Same as {@link #sendFile(String, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
390+
* which means until the end of the file.
391+
*
392+
* @param filename path to the file to serve
393+
* @param offset offset to start serving from
394+
* @param options send-file options
395+
* @return a future completed with the body result
396+
*/
397+
default Future<Void> sendFile(String filename, long offset, SendFileOptions options) {
398+
return sendFile(filename, offset, Long.MAX_VALUE, options);
399+
}
400+
376401
/**
377402
* Ask the OS to stream a file as specified by {@code filename} directly
378403
* from disk to the outgoing connection, bypassing userspace altogether
@@ -391,6 +416,22 @@ default Future<Void> sendFile(String filename, long offset) {
391416
*/
392417
Future<Void> sendFile(String filename, long offset, long length);
393418

419+
/**
420+
* Same as {@link #sendFile(String, long, long)} with send-file options.
421+
* <p>
422+
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
423+
* It is ignored when the file-region/sendfile path is used.
424+
*
425+
* @param filename path to the file to serve
426+
* @param offset offset to start serving from
427+
* @param length the number of bytes to send
428+
* @param options send-file options
429+
* @return a future completed with the body result
430+
*/
431+
default Future<Void> sendFile(String filename, long offset, long length, SendFileOptions options) {
432+
return sendFile(filename, offset, length);
433+
}
434+
394435
/**
395436
* Same as {@link #sendFile(FileChannel, long)} using length @code{Long.MAX_VALUE} which means until the end of the
396437
* file.
@@ -408,6 +449,20 @@ default Future<Void> sendFile(FileChannel channel) {
408449
return sendFile(channel, 0);
409450
}
410451

452+
/**
453+
* Same as {@link #sendFile(FileChannel, long, long, SendFileOptions)} with offset {@code 0} and length
454+
* {@code Long.MAX_VALUE} which means until the end of the file.
455+
*
456+
* @param channel the file channel to the file to serve
457+
* @param options send-file options
458+
* @return a future completed with the body result
459+
*/
460+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
461+
@Unstable
462+
default Future<Void> sendFile(FileChannel channel, SendFileOptions options) {
463+
return sendFile(channel, 0, options);
464+
}
465+
411466
/**
412467
* Same as {@link #sendFile(FileChannel, long, long)} using length @code{Long.MAX_VALUE} which means until the end of the
413468
* file.
@@ -426,6 +481,21 @@ default Future<Void> sendFile(FileChannel channel, long offset) {
426481
return sendFile(channel, offset, Long.MAX_VALUE);
427482
}
428483

484+
/**
485+
* Same as {@link #sendFile(FileChannel, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
486+
* which means until the end of the file.
487+
*
488+
* @param channel the file channel to the file to serve
489+
* @param offset offset to start serving from
490+
* @param options send-file options
491+
* @return a future completed with the body result
492+
*/
493+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
494+
@Unstable
495+
default Future<Void> sendFile(FileChannel channel, long offset, SendFileOptions options) {
496+
return sendFile(channel, offset, Long.MAX_VALUE, options);
497+
}
498+
429499
/**
430500
* Ask the OS to stream a file as specified by {@code channel} directly
431501
* from disk to the outgoing connection, bypassing userspace altogether
@@ -449,6 +519,24 @@ default Future<Void> sendFile(FileChannel channel, long offset) {
449519
@Unstable
450520
Future<Void> sendFile(FileChannel channel, long offset, long length);
451521

522+
/**
523+
* Same as {@link #sendFile(FileChannel, long, long)} with send-file options.
524+
* <p>
525+
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
526+
* It is ignored when the file-region/sendfile path is used.
527+
*
528+
* @param channel the file channel to the file to serve
529+
* @param offset offset to start serving from
530+
* @param length the number of bytes to send
531+
* @param options send-file options
532+
* @return a future completed with the body result
533+
*/
534+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
535+
@Unstable
536+
default Future<Void> sendFile(FileChannel channel, long offset, long length, SendFileOptions options) {
537+
return sendFile(channel, offset, length);
538+
}
539+
452540
/**
453541
* Same as {@link #sendFile(FileChannel)} with {@link RandomAccessFile}
454542
* <p>
@@ -465,6 +553,20 @@ default Future<Void> sendFile(RandomAccessFile file) {
465553
return sendFile(file, 0);
466554
}
467555

556+
/**
557+
* Same as {@link #sendFile(RandomAccessFile, long, long, SendFileOptions)} with offset {@code 0} and length
558+
* {@code Long.MAX_VALUE} which means until the end of the file.
559+
*
560+
* @param file the file to serve
561+
* @param options send-file options
562+
* @return a future completed with the body result
563+
*/
564+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
565+
@Unstable
566+
default Future<Void> sendFile(RandomAccessFile file, SendFileOptions options) {
567+
return sendFile(file, 0, options);
568+
}
569+
468570
/**
469571
*
470572
* Same as {@link #sendFile(FileChannel, long)} with {@link RandomAccessFile}
@@ -483,6 +585,21 @@ default Future<Void> sendFile(RandomAccessFile file, long offset) {
483585
return sendFile(file, offset, Long.MAX_VALUE);
484586
}
485587

588+
/**
589+
* Same as {@link #sendFile(RandomAccessFile, long, long, SendFileOptions)} using length {@code Long.MAX_VALUE}
590+
* which means until the end of the file.
591+
*
592+
* @param file the file to serve
593+
* @param offset offset to start serving from
594+
* @param options send-file options
595+
* @return a future completed with the body result
596+
*/
597+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
598+
@Unstable
599+
default Future<Void> sendFile(RandomAccessFile file, long offset, SendFileOptions options) {
600+
return sendFile(file, offset, Long.MAX_VALUE, options);
601+
}
602+
486603
/**
487604
* Same as {@link #sendFile(FileChannel, long, long)} with {@link RandomAccessFile}
488605
* <p>
@@ -499,6 +616,24 @@ default Future<Void> sendFile(RandomAccessFile file, long offset) {
499616
@Unstable
500617
Future<Void> sendFile(RandomAccessFile file, long offset, long length);
501618

619+
/**
620+
* Same as {@link #sendFile(RandomAccessFile, long, long)} with send-file options.
621+
* <p>
622+
* {@link SendFileOptions#getChunkSize()} configures the chunk size used by the fallback streaming path.
623+
* It is ignored when the file-region/sendfile path is used.
624+
*
625+
* @param file the file to serve
626+
* @param offset offset to start serving from
627+
* @param length the number of bytes to send
628+
* @param options send-file options
629+
* @return a future completed with the body result
630+
*/
631+
@GenIgnore(GenIgnore.PERMITTED_TYPE)
632+
@Unstable
633+
default Future<Void> sendFile(RandomAccessFile file, long offset, long length, SendFileOptions options) {
634+
return sendFile(file, offset, length);
635+
}
636+
502637
/**
503638
* @return has the response already ended?
504639
*/
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.core.http;
12+
13+
import io.vertx.codegen.annotations.DataObject;
14+
import io.vertx.codegen.annotations.Unstable;
15+
import io.vertx.codegen.json.annotations.JsonGen;
16+
import io.vertx.core.impl.Arguments;
17+
import io.vertx.core.json.JsonObject;
18+
19+
/**
20+
* Options controlling how {@link HttpServerResponse#sendFile(String)} streams a file when
21+
* the file-region/sendfile path cannot be used.
22+
*/
23+
@DataObject
24+
@JsonGen(publicConverter = false)
25+
@Unstable
26+
public class SendFileOptions {
27+
28+
/**
29+
* The default chunk size used by the send-file fallback path.
30+
*/
31+
public static final int DEFAULT_CHUNK_SIZE = 8192;
32+
33+
private int chunkSize;
34+
35+
/**
36+
* Create default send-file options.
37+
*/
38+
public SendFileOptions() {
39+
chunkSize = DEFAULT_CHUNK_SIZE;
40+
}
41+
42+
/**
43+
* Create send-file options from JSON.
44+
*
45+
* @param json the JSON object
46+
*/
47+
public SendFileOptions(JsonObject json) {
48+
this();
49+
SendFileOptionsConverter.fromJson(json, this);
50+
}
51+
52+
/**
53+
* Copy constructor.
54+
*
55+
* @param other the options to copy
56+
*/
57+
public SendFileOptions(SendFileOptions other) {
58+
chunkSize = other.chunkSize;
59+
}
60+
61+
/**
62+
* @return the chunk size, in bytes, used when the file is streamed through the fallback path
63+
*/
64+
public int getChunkSize() {
65+
return chunkSize;
66+
}
67+
68+
/**
69+
* Set the chunk size, in bytes, used when the file is streamed through the fallback path.
70+
* <p>
71+
* This option is ignored when the file can be transferred using the file-region/sendfile path.
72+
*
73+
* @param chunkSize the chunk size in bytes
74+
* @return a reference to this, so the API can be used fluently
75+
*/
76+
public SendFileOptions setChunkSize(int chunkSize) {
77+
Arguments.require(chunkSize > 0, "chunkSize must be > 0");
78+
this.chunkSize = chunkSize;
79+
return this;
80+
}
81+
82+
/**
83+
* @return a JSON representation of these options
84+
*/
85+
public JsonObject toJson() {
86+
JsonObject json = new JsonObject();
87+
SendFileOptionsConverter.toJson(this, json);
88+
return json;
89+
}
90+
}

vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerResponseImpl.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ public HttpServerResponse drainHandler(Handler<Void> handler) {
511511

512512
@Override
513513
public Future<Void> sendFile(String filename, long offset, long length) {
514+
return sendFile(filename, offset, length, new SendFileOptions());
515+
}
516+
517+
@Override
518+
public Future<Void> sendFile(String filename, long offset, long length, SendFileOptions options) {
514519
if (offset < 0) {
515520
return context.failedFuture("offset : " + offset + " (expected: >= 0)");
516521
}
@@ -521,14 +526,19 @@ public Future<Void> sendFile(String filename, long offset, long length) {
521526
checkValid();
522527
}
523528
if (conn.supportsSendFile()) {
524-
return sendFileInternal(filename, offset, length);
529+
return sendFileInternal(filename, offset, length, options);
525530
} else {
526-
return sendAsyncFile(filename, offset, length);
531+
return sendAsyncFile(filename, offset, length, options);
527532
}
528533
}
529534

530535
@Override
531536
public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
537+
return sendFile(file, offset, length, new SendFileOptions());
538+
}
539+
540+
@Override
541+
public Future<Void> sendFile(RandomAccessFile file, long offset, long length, SendFileOptions options) {
532542
if (!headersMap.contains(io.vertx.core.http.HttpHeaders.CONTENT_TYPE)) {
533543
headersMap.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
534544
}
@@ -538,11 +548,16 @@ public Future<Void> sendFile(RandomAccessFile file, long offset, long length) {
538548
} catch (IOException e) {
539549
return context.failedFuture(e);
540550
}
541-
return sendFileInternal(offset, length, size, file, null, false);
551+
return sendFileInternal(offset, length, size, file, null, false, options);
542552
}
543553

544554
@Override
545555
public Future<Void> sendFile(FileChannel channel, long offset, long length) {
556+
return sendFile(channel, offset, length, new SendFileOptions());
557+
}
558+
559+
@Override
560+
public Future<Void> sendFile(FileChannel channel, long offset, long length, SendFileOptions options) {
546561
if (!headersMap.contains(io.vertx.core.http.HttpHeaders.CONTENT_TYPE)) {
547562
headersMap.set(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
548563
}
@@ -552,13 +567,14 @@ public Future<Void> sendFile(FileChannel channel, long offset, long length) {
552567
} catch (IOException e) {
553568
return context.failedFuture(e);
554569
}
555-
return sendFileInternal(offset, length, size, null, channel, false);
570+
return sendFileInternal(offset, length, size, null, channel, false, options);
556571
}
557572

558-
private Future<Void> sendAsyncFile(String filename, long offset, long length) {
573+
private Future<Void> sendAsyncFile(String filename, long offset, long length, SendFileOptions options) {
559574
return HttpUtils
560575
.resolveFile(context, filename, offset, length)
561576
.compose(file -> {
577+
file.setReadBufferSize(options.getChunkSize());
562578
long fileLength = file.getReadLength();
563579
long contentLength = Math.min(length, fileLength);
564580
// fail early before status code/headers are written to the response
@@ -578,7 +594,7 @@ private Future<Void> sendAsyncFile(String filename, long offset, long length) {
578594
});
579595
}
580596

581-
private Future<Void> sendFileInternal(String filename, long offset, long length) {
597+
private Future<Void> sendFileInternal(String filename, long offset, long length, SendFileOptions options) {
582598
File file = context.owner().fileResolver().resolve(filename);
583599
long size;
584600
RandomAccessFile raf;
@@ -595,10 +611,10 @@ private Future<Void> sendFileInternal(String filename, long offset, long length)
595611
}
596612
headersMap.set(CONTENT_TYPE, mimeType);
597613
}
598-
return sendFileInternal(offset, length, size, raf, null, true);
614+
return sendFileInternal(offset, length, size, raf, null, true, options);
599615
}
600616

601-
private Future<Void> sendFileInternal(long offset, long length, long size, RandomAccessFile file, FileChannel channel, boolean close) {
617+
private Future<Void> sendFileInternal(long offset, long length, long size, RandomAccessFile file, FileChannel channel, boolean close, SendFileOptions options) {
602618
Future<Void> fut = null;
603619
try {
604620
long actualLength = Math.min(length, size - offset);
@@ -612,9 +628,9 @@ private Future<Void> sendFileInternal(long offset, long length, long size, Rando
612628
channel = file.getChannel();
613629
}
614630
if (close) {
615-
chunkedFile = new ChunkedNioFile(channel, actualOffset, actualLength, 8192);
631+
chunkedFile = new ChunkedNioFile(channel, actualOffset, actualLength, options.getChunkSize());
616632
} else {
617-
chunkedFile = new UncloseableChunkedNioFile(channel, actualOffset, actualLength);
633+
chunkedFile = new UncloseableChunkedNioFile(channel, actualOffset, actualLength, options.getChunkSize());
618634
}
619635
} catch (IOException e) {
620636
return context.failedFuture(e);

0 commit comments

Comments
 (0)