package org.fenixedu.bennu.io.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.fenixedu.bennu.core.util.CoreConfiguration;
import org.fenixedu.bennu.io.domain.FileStorage;
import org.fenixedu.bennu.io.domain.GenericFile;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
/**
* Utility methods to handle the download of {@link GenericFile} instances. The methods provided by this class provide raw access
* to the file, performing no validation of access control or URL matching.
*
* The main purpose of this class is to allow all the existing features in file download (such as byte range serving, cache
* controls, ETags, sendfile support, etc.) to be reused, independently of the way the user actually asks for the file.
*
*/
public class DownloadUtil {
private static final Pattern RANGE_PATTERN = Pattern.compile("bytes=(?<start>\\d*)-(?<end>\\d*)");
/**
* Downloads the given file, based on the given request, into the provided response instance.
*
* This method provides the same semantics as
* {@link #downloadFile(GenericFile, HttpServletRequest, HttpServletResponse, String)}, using the default value for the Cache
* Control header.
*
* @param file
* The file to be downloaded
* @param request
* The request which originated the download request
* @param response
* The response into which the file should be written
* @throws IOException
* If an IO error occurred while accessing or writing the file
* @throws NullPointerException
* If either of the arguments is {@code null}
* @see #downloadFile(GenericFile, HttpServletRequest, HttpServletResponse, String)
*/
public static void downloadFile(GenericFile file, HttpServletRequest request, HttpServletResponse response)
throws IOException {
downloadFile(file, request, response, CoreConfiguration.getConfiguration().staticCacheControl());
}
/**
* Downloads the given file, based on the given request, into the provided response instance, sending the provided cache
* control header.
*
* This methods does not provide any access control validation, and does not impose any limitation on the endpoint which is
* exposing the file.
*
* @param file
* The file to be downloaded
* @param request
* The request which originated the download request
* @param response
* The response into which the file should be written
* @param cacheControl
* The value of the Cache-Control header, may be {@code null}
* @throws IOException
* If an IO error occurred while accessing or writing the file
* @throws NullPointerException
* If either the file, request or response is {@code null}
*/
public static void downloadFile(GenericFile file, HttpServletRequest request, HttpServletResponse response,
String cacheControl) throws IOException {
String etag = "W/\"" + file.getExternalId() + "\"";
response.setHeader("ETag", etag);
if (cacheControl != null) {
response.setHeader("Cache-Control", cacheControl);
}
response.setHeader("Accept-Ranges", "bytes");
response.setContentType(file.getContentType());
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
long length = file.getSize();
long start = 0;
long end = length - 1;
String range = request.getHeader("Range");
if (range != null) {
Matcher matcher = RANGE_PATTERN.matcher(range);
boolean match = matcher.matches();
if (match) {
try {
String startGroup = matcher.group("start");
start = Strings.isNullOrEmpty(startGroup) ? start : Long.valueOf(startGroup);
start = start < 0 ? 0 : start;
String endGroup = matcher.group("end");
end = Strings.isNullOrEmpty(endGroup) ? end : Long.valueOf(endGroup);
end = end > length - 1 ? length - 1 : end;
} catch (NumberFormatException e) {
match = false;
}
}
if (!match) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
}
long contentLength = end - start + 1;
response.setHeader("Content-Length", Long.toString(contentLength));
if (range != null) {
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + length);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
} else {
response.setStatus(HttpServletResponse.SC_OK);
}
if (FileStorage.tryDownloadFile(file, request, response, start, end)) {
return;
}
try (InputStream stream = file.getStream()) {
if (stream != null) {
try (OutputStream out = response.getOutputStream()) {
copyStream(stream, out, start, contentLength);
out.flush();
}
} else {
response.sendError(HttpServletResponse.SC_NO_CONTENT, "File empty");
}
}
}
private static final int BUF_SIZE = 0x1000; // 4K
private static void copyStream(InputStream in, OutputStream out, long start, long bytesToRead) throws IOException {
ByteStreams.skipFully(in, start);
byte buffer[] = new byte[BUF_SIZE];
while (bytesToRead > 0) {
int len = in.read(buffer);
if (len == -1) {
break;
}
if (bytesToRead >= len) {
out.write(buffer, 0, len);
bytesToRead -= len;
} else {
out.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
}
}
}