Skip to content

Commit

Permalink
UNDERTOW-324 Support range requests in the default servlet and the re…
Browse files Browse the repository at this point in the history
…source handler
  • Loading branch information
stuartwdouglas committed Jan 21, 2015
1 parent 4c7915d commit c6357ee
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 112 deletions.
Expand Up @@ -46,7 +46,7 @@
*
* @author Stuart Douglas
*/
public class FileResource implements Resource {
public class FileResource implements RangeAwareResource {

private final File file;
private final String path;
Expand Down Expand Up @@ -113,12 +113,25 @@ public String getContentType(final MimeMappings mimeMappings) {

@Override
public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback callback) {
serveImpl(sender, exchange, -1, -1, callback, false);
}
@Override
public void serveRange(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final IoCallback callback) {
serveImpl(sender, exchange, start, end, callback, true);

}
private void serveImpl(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final IoCallback callback, final boolean range) {


abstract class BaseFileTask implements Runnable {
protected volatile FileChannel fileChannel;

protected boolean openFile() {
try {
fileChannel = exchange.getConnection().getWorker().getXnio().openFile(file, FileAccess.READ_ONLY);
if(range) {
fileChannel.position(start);
}
} catch (FileNotFoundException e) {
exchange.setResponseCode(StatusCodes.NOT_FOUND);
callback.onException(exchange, sender, e);
Expand All @@ -136,8 +149,18 @@ class ServerTask extends BaseFileTask implements IoCallback {

private Pooled<ByteBuffer> pooled;

long remaining = end - start + 1;

@Override
public void run() {
if(range && remaining == 0) {
//we are done
pooled.free();
pooled = null;
IoUtils.safeClose(fileChannel);
callback.onComplete(exchange, sender);
return;
}
if (fileChannel == null) {
if (!openFile()) {
return;
Expand All @@ -157,6 +180,12 @@ public void run() {
return;
}
buffer.flip();
if(range) {
if(buffer.remaining() > remaining) {
buffer.limit((int) (buffer.position() + remaining));
}
remaining -= buffer.remaining();
}
sender.send(buffer, this);
} catch (IOException e) {
onException(exchange, sender, e);
Expand Down Expand Up @@ -190,12 +219,12 @@ public void onException(final HttpServerExchange exchange, final Sender sender,
}

class TransferTask extends BaseFileTask {

@Override
public void run() {
if (!openFile()) {
return;
}

sender.transferFrom(fileChannel, new IoCallback() {
@Override
public void onComplete(HttpServerExchange exchange, Sender sender) {
Expand All @@ -218,7 +247,7 @@ public void onException(HttpServerExchange exchange, Sender sender, IOException
}
}

BaseFileTask task = manager.getTransferMinSize() > file.length() ? new ServerTask() : new TransferTask();
BaseFileTask task = manager.getTransferMinSize() > file.length() || range ? new ServerTask() : new TransferTask();
if (exchange.isInIoThread()) {
exchange.dispatch(task);
} else {
Expand Down Expand Up @@ -255,4 +284,8 @@ public URL getUrl() {
}
}

@Override
public boolean isRangeSupported() {
return true;
}
}
@@ -0,0 +1,47 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.undertow.server.handlers.resource;

import io.undertow.io.IoCallback;
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;

/**
* A resource implementation that
*
* @author Stuart Douglas
*/
public interface RangeAwareResource extends Resource {


/**
* Serve the resource, and call the provided callback when complete.
*
* @param sender The sender to use.
* @param exchange The exchange
*/
void serveRange(final Sender sender, final HttpServerExchange exchange, long start, long end, final IoCallback completionCallback);

/**
* It is possible that some resources managers may only support range requests on a subset of their resources,
*
* @return <core>true</core> if this resource supports range requests
*/
boolean isRangeSupported();
}
Expand Up @@ -41,6 +41,7 @@
import io.undertow.server.handlers.cache.ResponseCache;
import io.undertow.server.handlers.encoding.ContentEncodedResource;
import io.undertow.server.handlers.encoding.ContentEncodedResourceManager;
import io.undertow.util.ByteRange;
import io.undertow.util.CanonicalPathUtils;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
Expand Down Expand Up @@ -164,7 +165,6 @@ private void serveResource(final HttpServerExchange exchange, final boolean send
}
}


//we now dispatch to a worker thread
//as resource manager methods are potentially blocking
HttpHandler dispatchTask = new HttpHandler() {
Expand Down Expand Up @@ -231,7 +231,51 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.endExchange();
return;
}
//todo: handle range requests
final ContentEncodedResourceManager contentEncodedResourceManager = ResourceHandler.this.contentEncodedResourceManager;
Long contentLength = resource.getContentLength();

if (contentLength != null) {
exchange.setResponseContentLength(contentLength);
}
ByteRange range = null;
long start = -1, end = -1;
if(resource instanceof RangeAwareResource && ((RangeAwareResource)resource).isRangeSupported() && contentLength != null && contentEncodedResourceManager == null) {
//TODO: figure out what to do with the content encoded resource manager
range = ByteRange.parse(exchange.getRequestHeaders().getFirst(Headers.RANGE));
if(range != null && range.getRanges() == 1) {
start = range.getStart(0);
end = range.getEnd(0);
if(start == -1 ) {
//suffix range
long toWrite = end;
if(toWrite >= 0) {
exchange.setResponseContentLength(toWrite);
} else {
//ignore the range request
range = null;
}
start = contentLength - end;
end = contentLength;
} else if(end == -1) {
//prefix range
long toWrite = contentLength - start;
if(toWrite >= 0) {
exchange.setResponseContentLength(toWrite);
} else {
//ignore the range request
range = null;
}
end = contentLength;
} else {
long toWrite = end - start + 1;
exchange.setResponseContentLength(toWrite);
}
if(range != null) {
exchange.setResponseCode(StatusCodes.PARTIAL_CONTENT);
exchange.getResponseHeaders().put(Headers.CONTENT_RANGE, range.getStart(0) + "-" + range.getEnd(0) + "/" + contentLength);
}
}
}
//we are going to proceed. Set the appropriate headers
final String contentType = resource.getContentType(mimeMappings);

Expand All @@ -248,12 +292,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
if (etag != null) {
exchange.getResponseHeaders().put(Headers.ETAG, etag.toString());
}
Long contentLength = resource.getContentLength();
if (contentLength != null) {
exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, contentLength.toString());
}

final ContentEncodedResourceManager contentEncodedResourceManager = ResourceHandler.this.contentEncodedResourceManager;
if (contentEncodedResourceManager != null) {
try {
ContentEncodedResource encoded = contentEncodedResourceManager.getResource(resource, exchange);
Expand All @@ -275,6 +314,8 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {

if (!sendContent) {
exchange.endExchange();
} else if(range != null) {
((RangeAwareResource)resource).serveRange(exchange.getResponseSender(), exchange, start, end, IoCallback.END_EXCHANGE);
} else {
resource.serve(exchange.getResponseSender(), exchange, IoCallback.END_EXCHANGE);
}
Expand Down
Expand Up @@ -18,6 +18,16 @@

package io.undertow.server.handlers.resource;

import io.undertow.UndertowLogger;
import io.undertow.io.IoCallback;
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
import io.undertow.util.MimeMappings;
import io.undertow.util.StatusCodes;
import org.xnio.IoUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -30,20 +40,10 @@
import java.util.LinkedList;
import java.util.List;

import io.undertow.UndertowLogger;
import io.undertow.io.IoCallback;
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
import io.undertow.util.MimeMappings;
import io.undertow.util.StatusCodes;
import org.xnio.IoUtils;

/**
* @author Stuart Douglas
*/
public class URLResource implements Resource {
public class URLResource implements Resource, RangeAwareResource {

private final URL url;
private final URLConnection connection;
Expand Down Expand Up @@ -91,9 +91,9 @@ public String getName() {
@Override
public boolean isDirectory() {
File file = getFile();
if(file != null) {
if (file != null) {
return file.isDirectory();
} else if(url.getPath().endsWith("/")) {
} else if (url.getPath().endsWith("/")) {
return true;
}
return false;
Expand Down Expand Up @@ -126,15 +126,28 @@ public String getContentType(final MimeMappings mimeMappings) {
}

@Override
public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback completionCallback) {
public void serve(Sender sender, HttpServerExchange exchange, IoCallback completionCallback) {
serveImpl(sender, exchange, -1, -1, false, completionCallback);
}

public void serveImpl(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final boolean range, final IoCallback completionCallback) {

class ServerTask implements Runnable, IoCallback {

private InputStream inputStream;
private byte[] buffer;

long toSkip = start;
long remaining = end - start + 1;

@Override
public void run() {
if (range && remaining == 0) {
//we are done, just return
IoUtils.safeClose(inputStream);
completionCallback.onComplete(exchange, sender);
return;
}
if (inputStream == null) {
try {
inputStream = url.openStream();
Expand All @@ -152,7 +165,29 @@ public void run() {
completionCallback.onComplete(exchange, sender);
return;
}
sender.send(ByteBuffer.wrap(buffer, 0, res), this);
int bufferStart = 0;
int length = res;
if (range && toSkip > 0) {
//skip to the start of the requested range
//not super efficient, but what can you do
while (toSkip > res) {
toSkip -= res;
res = inputStream.read(buffer);
if (res == -1) {
//we are done, just return
IoUtils.safeClose(inputStream);
completionCallback.onComplete(exchange, sender);
return;
}
}
bufferStart = (int) toSkip;
length -= toSkip;
toSkip = 0;
}
if (range && length > remaining) {
length = (int) remaining;
}
sender.send(ByteBuffer.wrap(buffer, bufferStart, length), this);
} catch (IOException e) {
onException(exchange, sender, e);
}
Expand Down Expand Up @@ -199,7 +234,7 @@ public String getCacheKey() {

@Override
public File getFile() {
if(url.getProtocol().equals("file")) {
if (url.getProtocol().equals("file")) {
try {
return new File(url.toURI());
} catch (URISyntaxException e) {
Expand All @@ -218,4 +253,14 @@ public File getResourceManagerRoot() {
public URL getUrl() {
return url;
}

@Override
public void serveRange(Sender sender, HttpServerExchange exchange, long start, long end, IoCallback completionCallback) {
serveImpl(sender, exchange, start, end, true, completionCallback);
}

@Override
public boolean isRangeSupported() {
return true;
}
}

0 comments on commit c6357ee

Please sign in to comment.