package com.firefly.server.http2.router.handler.file;
import com.firefly.codec.http2.model.*;
import com.firefly.net.buffer.FileRegion;
import com.firefly.server.http2.router.Handler;
import com.firefly.server.http2.router.RoutingContext;
import com.firefly.server.http2.router.handler.error.AbstractErrorResponseHandler;
import com.firefly.server.http2.router.handler.error.DefaultErrorResponseHandlerLoader;
import com.firefly.utils.StringUtils;
import com.firefly.utils.concurrent.Callback;
import com.firefly.utils.io.BufferUtils;
import com.firefly.utils.io.IO;
import com.firefly.utils.lang.URIUtils;
import java.io.*;
import java.util.List;
/**
* @author Pengtao Qiu
*/
public class StaticFileHandler implements Handler {
private StaticFileConfiguration configuration;
private AbstractErrorResponseHandler errorResponseHandler;
public StaticFileHandler(StaticFileConfiguration configuration) {
this.configuration = configuration;
errorResponseHandler = DefaultErrorResponseHandlerLoader.getInstance().getHandler();
}
public StaticFileHandler(String rootPath) {
this(new StaticFileConfiguration());
configuration.setRootPath(rootPath);
errorResponseHandler = DefaultErrorResponseHandlerLoader.getInstance().getHandler();
}
@Override
public void handle(RoutingContext ctx) {
File file = new File(configuration.getRootPath(), URIUtils.canonicalPath(ctx.getURI().getPath()));
if (file.exists()) {
long contentLength = file.length();
String mimetype = MimeTypes.getDefaultMimeByExtension(file.getName());
List<String> reqRanges = ctx.getFields().getValuesList(HttpHeader.RANGE.asString());
if (reqRanges == null || reqRanges.isEmpty()) {
ctx.setStatus(HttpStatus.OK_200);
ctx.put(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength));
if (StringUtils.hasText(mimetype)) {
ctx.put(HttpHeader.CONTENT_TYPE, mimetype);
}
try (OutputStream out = ctx.getResponse().getOutputStream();
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
IO.copy(in, out, contentLength);
} catch (FileNotFoundException e) {
errorResponseHandler.render(ctx, HttpStatus.NOT_FOUND_404, null);
} catch (IOException e) {
if (ctx.getResponse().isCommitted()) {
errorResponseHandler.render(ctx, HttpStatus.INTERNAL_SERVER_ERROR_500, e);
}
}
} else {
// Parse the satisfiable ranges
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, contentLength);
if (ranges == null || ranges.size() == 0) {
// if there are no satisfiable ranges, send 416 response
ctx.put(HttpHeader.CONTENT_RANGE, InclusiveByteRange.to416HeaderRangeString(contentLength));
errorResponseHandler.render(ctx, HttpStatus.RANGE_NOT_SATISFIABLE_416, null);
} else {
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 206 response
if (ranges.size() == 1) {
InclusiveByteRange singleSatisfiableRange = ranges.get(0);
long singleLength = singleSatisfiableRange.getSize(contentLength);
ctx.setStatus(HttpStatus.PARTIAL_CONTENT_206);
ctx.put(HttpHeader.CONTENT_LENGTH, String.valueOf(singleLength));
ctx.put(HttpHeader.CONTENT_RANGE, singleSatisfiableRange.toHeaderRangeString(contentLength));
if (StringUtils.hasText(mimetype)) {
ctx.put(HttpHeader.CONTENT_TYPE, mimetype);
}
long position = singleSatisfiableRange.getFirst(contentLength);
try (FileRegion fileRegion = new FileRegion(file, position, singleLength);
OutputStream out = ctx.getResponse().getOutputStream()) {
fileRegion.transferTo(Callback.NOOP, (buf, callback, count) -> out.write(BufferUtils.toArray(buf)));
} catch (FileNotFoundException e) {
errorResponseHandler.render(ctx, HttpStatus.NOT_FOUND_404, null);
} catch (IOException e) {
if (ctx.getResponse().isCommitted()) {
errorResponseHandler.render(ctx, HttpStatus.INTERNAL_SERVER_ERROR_500, e);
}
}
} else {
// multiple non-overlapping valid ranges cause a multipart
// 206 response which does not require an overall content-length header
ctx.setStatus(HttpStatus.PARTIAL_CONTENT_206);
InputStream in = null;
try (MultiPartOutputStream multi = new MultiPartOutputStream(ctx.getResponse().getOutputStream())) {
String ctp;
if (ctx.getFields().get(HttpHeader.REQUEST_RANGE) != null) {
ctp = "multipart/x-byteranges; boundary=";
} else {
ctp = "multipart/byteranges; boundary=";
}
ctx.put(HttpHeader.CONTENT_TYPE, ctp + multi.getBoundary());
in = new BufferedInputStream(new FileInputStream(file));
long pos = 0;
// calculate the content-length
int length = 0;
String[] header = new String[ranges.size()];
for (int i = 0; i < ranges.size(); i++) {
InclusiveByteRange ibr = ranges.get(i);
header[i] = ibr.toHeaderRangeString(contentLength);
length += ((i > 0) ? 2 : 0) +
2 + multi.getBoundary().length() + 2 +
(mimetype == null ? 0 : HttpHeader.CONTENT_TYPE.asString().length() + 2 + mimetype.length()) + 2 +
HttpHeader.CONTENT_RANGE.asString().length() + 2 + header[i].length() + 2 +
2 +
(ibr.getLast(contentLength) - ibr.getFirst(contentLength)) + 1;
}
length += 2 + 2 + multi.getBoundary().length() + 2 + 2;
ctx.put(HttpHeader.CONTENT_LENGTH, String.valueOf(length));
for (int i = 0; i < ranges.size(); i++) {
InclusiveByteRange ibr = ranges.get(i);
multi.startPart(mimetype, new String[]{HttpHeader.CONTENT_RANGE + ": " + header[i]});
long start = ibr.getFirst(contentLength);
long size = ibr.getSize(contentLength);
// Handle non cached resource
if (start < pos) {
in.close();
in = new BufferedInputStream(new FileInputStream(file));
pos = 0;
}
if (pos < start) {
in.skip(start - pos);
pos = start;
}
IO.copy(in, multi, size);
pos += size;
}
in.close();
} catch (IOException e) {
if (in != null) {
IO.close(in);
}
if (ctx.getResponse().isCommitted()) {
errorResponseHandler.render(ctx, HttpStatus.INTERNAL_SERVER_ERROR_500, e);
}
}
}
}
}
} else {
errorResponseHandler.render(ctx, HttpStatus.NOT_FOUND_404, null);
}
}
}