From b84fcde5a7d22b52d0576dc55963b9fe54103a51 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 20 Nov 2023 12:49:41 +0200 Subject: [PATCH] Add basic Range header support Closes: #37205 --- .../server/test/providers/FileTestCase.java | 15 ++ .../serialisers/ServerFileBodyHandler.java | 140 +++++++++++++++++- .../serialisers/ServerPathBodyHandler.java | 5 +- 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java index c9adc3def2c48..80f99ddf1105d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/FileTestCase.java @@ -41,6 +41,21 @@ public void testFiles() throws Exception { .statusCode(200) .header(HttpHeaders.CONTENT_LENGTH, contentLength) .body(Matchers.equalTo(content)); + RestAssured.given().header("Range", "bytes=0-9").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(0, 10))); + RestAssured.given().header("Range", "bytes=10-19").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, "10") + .body(Matchers.equalTo(content.substring(10, 20))); + RestAssured.given().header("Range", "bytes=10-").get("/providers/file/file") + .then() + .statusCode(206) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(content.length() - 10)) + .body(Matchers.equalTo(content.substring(10))); RestAssured.get("/providers/file/file-partial") .then() .statusCode(200) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java index aa974beede41f..bd33ed659fda6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerFileBodyHandler.java @@ -3,13 +3,18 @@ import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.providers.serialisers.FileBodyHandler; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -30,6 +35,139 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(File o, Type genericType, ServerRequestContext context) throws WebApplicationException { - context.serverResponse().sendFile(o.getAbsolutePath(), 0, o.length()); + sendFile(o, context); + } + + static void sendFile(File file, ServerRequestContext context) { + ResteasyReactiveRequestContext ctx = ((ResteasyReactiveRequestContext) context); + Object rangeObj = ctx.getHeader("Range", true); + ByteRange byteRange = rangeObj == null ? null : ByteRange.parse(rangeObj.toString()); + if ((byteRange != null) && (byteRange.ranges.size() == 1)) { + ByteRange.Range range = byteRange.ranges.get(0); + long length = range.getEnd() == -1 ? Long.MAX_VALUE : range.getEnd() - range.getStart() + 1; + context.serverResponse() + .setStatusCode(Response.Status.PARTIAL_CONTENT.getStatusCode()) + .sendFile(file.getAbsolutePath(), range.getStart(), length); + + } else { + context.serverResponse().sendFile(file.getAbsolutePath(), 0, file.length()); + } + } + + /** + * Represents a byte range for a range request + * + * @author Stuart Douglas + * + * NOTE: copied from Quarkus HTTP + */ + public static class ByteRange { + + private static final Logger log = Logger.getLogger(ByteRange.class); + + private final List ranges; + + public ByteRange(List ranges) { + this.ranges = ranges; + } + + public int getRanges() { + return ranges.size(); + } + + /** + * Gets the start of the specified range segment, of -1 if this is a suffix range segment + * + * @param range The range segment to get + * @return The range start + */ + public long getStart(int range) { + return ranges.get(range).getStart(); + } + + /** + * Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment + * + * @param range The range segment to get + * @return The range end + */ + public long getEnd(int range) { + return ranges.get(range).getEnd(); + } + + /** + * Attempts to parse a range request. If the range request is invalid it will just return null so that + * it may be ignored. + * + * @param rangeHeader The range spec + * @return A range spec, or null if the range header could not be parsed + */ + public static ByteRange parse(String rangeHeader) { + if (rangeHeader == null || rangeHeader.length() < 7) { + return null; + } + if (!rangeHeader.startsWith("bytes=")) { + return null; + } + List ranges = new ArrayList<>(); + String[] parts = rangeHeader.substring(6).split(","); + for (String part : parts) { + try { + int index = part.indexOf('-'); + if (index == 0) { + //suffix range spec + //represents the last N bytes + //internally we represent this using a -1 as the start position + long val = Long.parseLong(part.substring(1)); + if (val < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + ranges.add(new Range(-1, val)); + } else { + if (index == -1) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long start = Long.parseLong(part.substring(0, index)); + if (start < 0) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + long end; + if (index + 1 < part.length()) { + end = Long.parseLong(part.substring(index + 1)); + } else { + end = -1; + } + ranges.add(new Range(start, end)); + } + } catch (NumberFormatException e) { + log.debugf("Invalid range spec %s", rangeHeader); + return null; + } + } + if (ranges.isEmpty()) { + return null; + } + return new ByteRange(ranges); + } + + public static class Range { + private final long start, end; + + public Range(long start, long end) { + this.start = start; + this.end = end; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java index 0585d84426286..252ddd98a8dff 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/providers/serialisers/ServerPathBodyHandler.java @@ -12,7 +12,6 @@ import org.jboss.resteasy.reactive.common.providers.serialisers.PathBodyHandler; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; -import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -36,8 +35,6 @@ public boolean isWriteable(Class type, Type genericType, ResteasyReactiveReso @Override public void writeResponse(java.nio.file.Path o, Type genericType, ServerRequestContext context) throws WebApplicationException { - ServerHttpResponse serverResponse = context.serverResponse(); - // sendFile implies end(), even though javadoc doesn't say, if you add end() it will throw - serverResponse.sendFile(o.toString(), 0, Long.MAX_VALUE); + ServerFileBodyHandler.sendFile(o.toFile(), context); } }