#1612 support download large file. (#1642)

This commit is contained in:
lylylyly2 2021-01-05 01:31:37 +08:00 committed by GitHub
parent d4e54cd2f2
commit 285b73d624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 35 deletions

View File

@ -3,17 +3,40 @@ package com.taobao.arthas.core.shell.term.impl.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.taobao.arthas.common.IOUtils;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.DefaultFileRegion;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
*
@ -22,6 +45,10 @@ import io.netty.handler.codec.http.HttpVersion;
*/
public class DirectoryBrowser {
public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
public static final long MIN_NETTY_DIRECT_SEND_SIZE = 10 * 1024 * 1024;
private static final Logger logger = LoggerFactory.getLogger(DirectoryBrowser.class);
//@formatter:off
private static String pageHeader = "<!DOCTYPE html>\n" +
"<html>\n" +
@ -108,14 +135,24 @@ public class DirectoryBrowser {
return sb.toString();
}
public static DefaultFullHttpResponse view(File dir, String path, HttpVersion version) throws IOException {
/**
* write data here,still return not null just to know succeeded.
* @param dir
* @param path
* @param request
* @param ctx
* @return
* @throws IOException
*/
public static DefaultFullHttpResponse directView(File dir, String path, FullHttpRequest request, ChannelHandlerContext ctx) throws IOException {
if (path.startsWith("/")) {
path = path.substring(1, path.length());
}
// path maybe: arthas-output/20201225-203454.svg
// 需要取 dir的parent来去掉前缀
File file = new File(dir.getParent(), path);
HttpVersion version = request.protocolVersion();
if (isSubFile(dir, file)) {
DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(version, HttpResponseStatus.OK);
@ -127,22 +164,112 @@ public class DirectoryBrowser {
String renderResult = renderDir(file);
fullResp.content().writeBytes(renderResult.getBytes("utf-8"));
fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8");
ctx.write(fullResp);
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
return fullResp;
} else {
FileInputStream fileInputStream = new FileInputStream(file);
try {
byte[] content = IOUtils.getBytes(fileInputStream);
fullResp.content().writeBytes(content);
HttpUtil.setContentLength(fullResp, fullResp.content().readableBytes());
} finally {
IOUtils.close(fileInputStream);
logger.info("get file now. file:" + file.getPath());
if (file.isHidden() || !file.exists() || file.isDirectory() || !file.isFile()) {
return null;
}
RandomAccessFile raf;
try {
raf = new RandomAccessFile(file, "r");
} catch (Exception ignore) {
return null;
}
long fileLength = raf.length();
if (fileLength < MIN_NETTY_DIRECT_SEND_SIZE){
FileInputStream fileInputStream = new FileInputStream(file);
try {
byte[] content = IOUtils.getBytes(fileInputStream);
fullResp.content().writeBytes(content);
HttpUtil.setContentLength(fullResp, fullResp.content().readableBytes());
} finally {
IOUtils.close(fileInputStream);
}
ctx.write(fullResp);
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
return fullResp;
}
logger.info("file {} size bigger than 10MB, send by future.",file.getName());
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
HttpUtil.setContentLength(response, fileLength);
setContentTypeHeader(response, file);
setDateAndCacheHeaders(response, file);
if (HttpUtil.isKeepAlive(request)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
// Write the initial line and the header.
ctx.write(response);
// Write the content.
ChannelFuture sendFileFuture;
ChannelFuture lastContentFuture;
if (ctx.pipeline().get(SslHandler.class) == null) {
sendFileFuture =
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
// Write the end marker.
lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
} else {
sendFileFuture =
ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),
ctx.newProgressivePromise());
// HttpChunkedInput will write the end marker (LastHttpContent) for us.
lastContentFuture = sendFileFuture;
}
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) { // total unknown
logger.info(future.channel() + " Transfer progress: " + progress);
} else {
logger.info(future.channel() + " Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) {
logger.info(future.channel() + " Transfer complete.");
}
});
// Decide whether to close the connection or not.
if (!HttpUtil.isKeepAlive(request)) {
// Close the connection when the whole content is written out.
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
return fullResp;
}
return fullResp;
}
return null;
}
private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) {
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
// Date header
Calendar time = new GregorianCalendar();
response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
// Add cache headers
time.add(Calendar.SECOND, 3600);
response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + 3600);
response.headers().set(
HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
}
private static void setContentTypeHeader(HttpResponse response, File file) {
String contentType = "application/octet-stream";
// 暂时hardcode 大文件的content-type
response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
}
public static boolean isSubFile(File parent, File child) throws IOException {
String parentPath = parent.getCanonicalPath();
String childPath = child.getCanonicalPath();

View File

@ -1,18 +1,11 @@
package com.taobao.arthas.core.shell.term.impl.http;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.taobao.arthas.common.IOUtils;
import com.taobao.arthas.core.server.ArthasBootstrap;
import com.taobao.arthas.core.shell.term.impl.http.api.HttpApiHandler;
import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@ -24,13 +17,19 @@ import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.termd.core.http.HttpTtyConnection;
import io.termd.core.util.Logging;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import static com.taobao.arthas.core.util.HttpUtils.createRedirectResponse;
import static com.taobao.arthas.core.util.HttpUtils.createResponse;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
@ -46,6 +45,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
private HttpApiHandler httpApiHandler;
public HttpRequestHandler(String wsUri) {
this(wsUri, ArthasBootstrap.getInstance().getOutputPath());
}
@ -73,6 +73,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
}
boolean isHttpApiResponse = false;
boolean isFileResponseFinished = false;
try {
//handle http restful api
if ("/api".equals(path)) {
@ -95,7 +96,8 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
//try output dir later, avoid overlay classpath resources files
if (response == null){
response = DirectoryBrowser.view(dir, path, request.protocolVersion());
response = DirectoryBrowser.directView(dir, path, request,ctx);
isFileResponseFinished = (response == null) ? false : true;
}
//not found
@ -109,25 +111,26 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
if (response == null){
response = createResponse(request, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error");
}
ctx.write(response);
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
//reuse http api response buf
if (isHttpApiResponse && response instanceof DefaultFullHttpResponse) {
final HttpResponse finalResponse = response;
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
httpApiHandler.onCompleted((DefaultFullHttpResponse) finalResponse);
}
});
if(!isFileResponseFinished) {
ctx.write(response);
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
//reuse http api response buf
if (isHttpApiResponse && response instanceof DefaultFullHttpResponse) {
final HttpResponse finalResponse = response;
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
httpApiHandler.onCompleted((DefaultFullHttpResponse) finalResponse);
}
});
}
}
}
}
}
private FullHttpResponse readFileFromResource(FullHttpRequest request, String path) throws IOException {
private HttpResponse readFileFromResource(FullHttpRequest request, String path) throws IOException {
DefaultFullHttpResponse fullResp = null;
InputStream in = null;
try {
@ -166,7 +169,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}