Skip to content

Commit

Permalink
Add basic Range header support
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Nov 20, 2023
1 parent 668874e commit b84fcde
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Range> ranges;

public ByteRange(List<Range> 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<Range> 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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}

0 comments on commit b84fcde

Please sign in to comment.