Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.firefly.mvc.web.view.StaticFileView Maven / Gradle / Ivy
package com.firefly.mvc.web.view;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.firefly.mvc.web.Constants;
import com.firefly.mvc.web.FileAccessFilter;
import com.firefly.mvc.web.View;
import com.firefly.mvc.web.servlet.SystemHtmlPage;
import com.firefly.server.exception.HttpServerException;
import com.firefly.utils.RandomUtils;
import com.firefly.utils.StringUtils;
import com.firefly.utils.VerifyUtils;
import com.firefly.utils.concurrent.Callback;
import com.firefly.utils.concurrent.CountingCallback;
import com.firefly.utils.io.BufferReaderHandler;
import com.firefly.utils.io.BufferUtils;
import com.firefly.utils.io.FileUtils;
import com.firefly.utils.log.Log;
import com.firefly.utils.log.LogFactory;
public class StaticFileView implements View {
private static Log log = LogFactory.getInstance().getLog("firefly-system");
public static final String CRLF = "\r\n";
private static Set ALLOW_METHODS = new HashSet(Arrays.asList("GET", "POST", "HEAD"));
private static String RANGE_ERROR_HTML = SystemHtmlPage.systemPageTemplate(
HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
"None of the range-specifier values in the Range request-header field overlap the current extent of the selected resource.");
private static int MAX_RANGE_NUM;
private static String SERVER_HOME;
private static Path SERVER_HOME_PATH;
private static String CHARACTER_ENCODING = "UTF-8";
private static String TEMPLATE_PATH;
private static FileAccessFilter FILE_ACCESS_FILTER = new FileAccessFilter() {
@Override
public String doFilter(HttpServletRequest request, HttpServletResponse response, String path) {
return path;
}
};
private final String inputPath;
public StaticFileView(String path) {
this.inputPath = path;
}
public static void init(String characterEncoding, FileAccessFilter fileAccessFilter, String serverHome,
int maxRangeNum, String tempPath) {
if (VerifyUtils.isNotEmpty(characterEncoding)) {
CHARACTER_ENCODING = characterEncoding;
}
if (fileAccessFilter != null) {
FILE_ACCESS_FILTER = fileAccessFilter;
}
SERVER_HOME = serverHome;
SERVER_HOME_PATH = Paths.get(serverHome).normalize();
MAX_RANGE_NUM = maxRangeNum;
if (TEMPLATE_PATH == null && tempPath != null)
TEMPLATE_PATH = tempPath;
}
@Override
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if (inputPath.startsWith(TEMPLATE_PATH)) {
SystemHtmlPage.responseSystemPage(request, response, CHARACTER_ENCODING, HttpServletResponse.SC_NOT_FOUND,
request.getRequestURI() + " not found");
return;
}
if (!ALLOW_METHODS.contains(request.getMethod())) {
response.setHeader("Allow", "GET,POST,HEAD");
SystemHtmlPage.responseSystemPage(request, response, CHARACTER_ENCODING,
HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Only support GET, POST or HEAD method");
return;
}
String path = FILE_ACCESS_FILTER.doFilter(request, response, inputPath);
if (VerifyUtils.isEmpty(path)) {
return;
}
// check the current path starts with server home
Path currentPath = Paths.get(SERVER_HOME, path).normalize();
if(!currentPath.startsWith(SERVER_HOME_PATH)) {
if(log.isDebugEnabled()) {
log.debug("the current path [{}] is not start with server home [{}]", currentPath, SERVER_HOME_PATH);
}
SystemHtmlPage.responseSystemPage(request, response, CHARACTER_ENCODING, HttpServletResponse.SC_NOT_FOUND,
request.getRequestURI() + " not found");
return;
}
File file = currentPath.toFile();
if (!file.exists() || file.isDirectory()) {
SystemHtmlPage.responseSystemPage(request, response, CHARACTER_ENCODING, HttpServletResponse.SC_NOT_FOUND,
request.getRequestURI() + " not found");
return;
}
// get content type of file
String fileSuffix = getFileSuffix(file.getName()).toLowerCase();
String contentType = Constants.MIME.get(fileSuffix);
if (contentType == null) {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
} else {
String[] type = StringUtils.split(contentType, '/');
if ("application".equals(type[0])) {
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
} else if ("text".equals(type[0])) {
contentType += "; charset=" + CHARACTER_ENCODING;
}
response.setContentType(contentType);
}
// output file
try (FileServletOutputStream out = new FileServletOutputStream(request, response, response.getOutputStream())) {
String range = request.getHeader("Range");
if (range == null) {
out.write(file);
} else {
String[] rangesSpecifier = StringUtils.split(range, '=');
if (rangesSpecifier.length != 2) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
long fileLen = file.length();
String byteRangeSet = rangesSpecifier[1].trim();
String[] byteRangeSets = StringUtils.split(byteRangeSet, ',');
if (byteRangeSets.length > 1) { // multipart/byteranges
String boundary = "ff10" + RandomUtils.randomString(13);
if (byteRangeSets.length > MAX_RANGE_NUM) {
log.error("multipart range more than {}", MAX_RANGE_NUM);
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
// multipart output
List tmpByteRangeSets = new ArrayList(MAX_RANGE_NUM);
// long otherLen = 0;
for (String t : byteRangeSets) {
String tmp = t.trim();
String[] byteRange = StringUtils.split(tmp, '-');
if (byteRange.length == 1) {
long pos = Long.parseLong(byteRange[0].trim());
if (pos == 0)
continue;
if (tmp.charAt(0) == '-') {
long lastBytePos = fileLen - 1;
long firstBytePos = lastBytePos - pos + 1;
if (firstBytePos > lastBytePos)
continue;
MultipartByteranges multipartByteranges = getMultipartByteranges(contentType,
firstBytePos, lastBytePos, fileLen, boundary);
tmpByteRangeSets.add(multipartByteranges);
} else if (tmp.charAt(tmp.length() - 1) == '-') {
long firstBytePos = pos;
long lastBytePos = fileLen - 1;
if (firstBytePos > lastBytePos)
continue;
MultipartByteranges multipartByteranges = getMultipartByteranges(contentType,
firstBytePos, lastBytePos, fileLen, boundary);
tmpByteRangeSets.add(multipartByteranges);
}
} else {
long firstBytePos = Long.parseLong(byteRange[0].trim());
long lastBytePos = Long.parseLong(byteRange[1].trim());
if (firstBytePos > fileLen || firstBytePos >= lastBytePos)
continue;
MultipartByteranges multipartByteranges = getMultipartByteranges(contentType, firstBytePos,
lastBytePos, fileLen, boundary);
tmpByteRangeSets.add(multipartByteranges);
}
}
if (tmpByteRangeSets.size() > 0) {
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Type", "multipart/byteranges; boundary=" + boundary);
for (MultipartByteranges m : tmpByteRangeSets) {
long length = m.lastBytePos - m.firstBytePos + 1;
out.write(m.head.getBytes(CHARACTER_ENCODING));
out.write(file, m.firstBytePos, length);
}
out.write((CRLF + "--" + boundary + "--" + CRLF).getBytes(CHARACTER_ENCODING));
log.debug("multipart download|{}", range);
} else {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
} else {
String tmp = byteRangeSets[0].trim();
String[] byteRange = StringUtils.split(tmp, '-');
if (byteRange.length == 1) {
long pos = Long.parseLong(byteRange[0].trim());
if (pos == 0) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
if (tmp.charAt(0) == '-') {
long lastBytePos = fileLen - 1;
long firstBytePos = lastBytePos - pos + 1;
writePartialFile(request, response, out, file, firstBytePos, lastBytePos, fileLen);
} else if (tmp.charAt(tmp.length() - 1) == '-') {
writePartialFile(request, response, out, file, pos, fileLen - 1, fileLen);
} else {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
} else {
long firstBytePos = Long.parseLong(byteRange[0].trim());
long lastBytePos = Long.parseLong(byteRange[1].trim());
if (firstBytePos > fileLen || firstBytePos >= lastBytePos) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
if (lastBytePos >= fileLen)
lastBytePos = fileLen - 1;
writePartialFile(request, response, out, file, firstBytePos, lastBytePos, fileLen);
}
log.debug("single range download|{}", range);
}
}
} catch (Throwable e) {
log.error("static file output exception", e);
throw new HttpServerException("get static file output stream error");
}
}
private void writePartialFile(HttpServletRequest request, HttpServletResponse response, FileServletOutputStream out,
File file, long firstBytePos, long lastBytePos, long fileLen) throws Throwable {
long length = lastBytePos - firstBytePos + 1;
if (length <= 0) {
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
out.write(RANGE_ERROR_HTML.getBytes(CHARACTER_ENCODING));
return;
}
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLen);
out.write(file, firstBytePos, length);
}
public static String getFileSuffix(String name) {
if (name.charAt(name.length() - 1) == '.')
return "*";
for (int i = name.length() - 2; i >= 0; i--) {
if (name.charAt(i) == '.') {
return name.substring(i + 1, name.length());
}
}
return "*";
}
private class MultipartByteranges {
public String head;
public long firstBytePos, lastBytePos;
}
private MultipartByteranges getMultipartByteranges(String contentType, long firstBytePos, long lastBytePos,
long fileLen, String boundary) {
MultipartByteranges ret = new MultipartByteranges();
ret.firstBytePos = firstBytePos;
ret.lastBytePos = lastBytePos;
ret.head = CRLF + "--" + boundary + CRLF + "Content-Type: " + contentType + CRLF + "Content-range: bytes "
+ firstBytePos + "-" + lastBytePos + "/" + fileLen + CRLF + CRLF;
return ret;
}
private class FileServletOutputStream extends ServletOutputStream {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final ServletOutputStream out;
private final Queue queue = new LinkedList();
private long size;
public FileServletOutputStream(HttpServletRequest request, HttpServletResponse response,
ServletOutputStream out) {
this.request = request;
this.response = response;
this.out = out;
}
@Override
public boolean isReady() {
return out.isReady();
}
@Override
public void setWriteListener(WriteListener writeListener) {
out.setWriteListener(writeListener);
}
@Override
public void write(int b) throws IOException {
queue.offer(new ByteChunkedData((byte) b));
size++;
}
@Override
public void write(byte[] array, int offset, int length) throws IOException {
ChunkedData c = new ByteArrayChunkedData(array, offset, length);
queue.offer(c);
size += length;
}
public void write(File file) throws IOException {
long len = file.length();
SequenceAccessFileChunkedData data = new SequenceAccessFileChunkedData(file, len);
queue.offer(data);
size += data.getLength();
}
public void write(File file, long off, long len) throws IOException {
queue.offer(new RandomAccessFileChunkedData(file, off, len));
size += len;
}
@Override
public void print(String string) throws IOException {
write(string.getBytes(response.getCharacterEncoding()));
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
if (!response.isCommitted()) {
response.setHeader("Content-Length", String.valueOf(size));
}
if (size > 0) {
if (request.getMethod().equals("HEAD"))
queue.clear();
else {
for (ChunkedData d = null; (d = queue.poll()) != null;)
d.write();
}
size = 0;
}
out.close();
}
private class FileBufferReaderHandler implements BufferReaderHandler {
private final long len;
public FileBufferReaderHandler(long len) {
this.len = len;
}
@Override
public void readBuffer(ByteBuffer buf, CountingCallback countingCallback, long count) throws IOException {
log.debug("write file, count: {} , lenth: {}", count, len);
out.write(BufferUtils.toArray(buf));
}
}
private class SequenceAccessFileChunkedData extends ChunkedData {
private final File file;
private final long len;
public SequenceAccessFileChunkedData(File file, long len) {
this.file = file;
this.len = len;
}
public long getLength() {
return len;
}
@Override
public void write() throws IOException {
try (FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ)) {
FileUtils.transferTo(fc, len, Callback.NOOP, new FileBufferReaderHandler(len));
}
}
}
private class RandomAccessFileChunkedData extends ChunkedData {
private final long len;
private final long off;
private final File file;
public RandomAccessFileChunkedData(File file, long off, long len) {
this.off = off;
this.len = len;
this.file = file;
}
@Override
public void write() throws IOException {
try (FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ)) {
FileUtils.transferTo(fc, off, len, Callback.NOOP, new FileBufferReaderHandler(len));
}
}
}
private class ByteChunkedData extends ChunkedData {
private final byte b;
public ByteChunkedData(byte b) {
this.b = b;
}
@Override
public void write() throws IOException {
out.write(b);
}
}
private class ByteArrayChunkedData extends ChunkedData {
private final byte[] b;
private final int len;
private final int off;
public ByteArrayChunkedData(byte[] b, int off, int len) {
this.b = b;
this.off = off;
this.len = len;
}
@Override
public void write() throws IOException {
out.write(b, off, len);
}
}
abstract private class ChunkedData {
abstract public void write() throws IOException;
}
}
}