优化本地文件下载功能, 支持断点续传和多线程下载

This commit is contained in:
赵俊
2021-04-10 18:06:10 +08:00
parent fb0d9721aa
commit 73198d7852
2 changed files with 119 additions and 25 deletions

View File

@@ -5,7 +5,6 @@ import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.service.impl.LocalServiceImpl;
import im.zhaojun.zfile.util.FileUtil;
import im.zhaojun.zfile.util.StringUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.GetMapping;
@@ -15,6 +14,7 @@ import org.springframework.web.servlet.HandlerMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
@@ -37,13 +37,14 @@ public class LocalController {
*/
@GetMapping("/file/{driveId}/**")
@ResponseBody
public ResponseEntity<Object> downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
public void downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request, final HttpServletResponse response) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
AntPathMatcher apm = new AntPathMatcher();
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
LocalServiceImpl localService = (LocalServiceImpl) driveContext.get(driveId);
return FileUtil.export(new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath)));
File file = new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath));
FileUtil.export(request, response, file);
}
}

View File

@@ -1,18 +1,26 @@
package im.zhaojun.zfile.util;
import cn.hutool.core.util.URLUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Date;
/**
* @author zhaojun
*/
@Slf4j
public class FileUtil {
public static ResponseEntity<Object> export(File file, String fileName) {
@@ -23,7 +31,7 @@ public class FileUtil {
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
if (StringUtils.isNullOrEmpty(fileName)) {
fileName = file.getName();
@@ -31,10 +39,10 @@ public class FileUtil {
headers.setContentDispositionFormData("attachment", URLUtil.encode(fileName));
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Last-Modified", new Date().toString());
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
headers.add(HttpHeaders.PRAGMA, "no-cache");
headers.add(HttpHeaders.EXPIRES, "0");
headers.add(HttpHeaders.LAST_MODIFIED, new Date().toString());
headers.add(HttpHeaders.ETAG, String.valueOf(System.currentTimeMillis()));
return ResponseEntity
.ok()
.headers(headers)
@@ -43,26 +51,111 @@ public class FileUtil {
.body(new FileSystemResource(file));
}
public static ResponseEntity<Object> export(File file) {
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("404 FILE NOT FOUND");
/**
* 返回文件给 response支持断点续传和多线程下载
* @param request 请求对象
* @param response 响应对象
* @param file 下载的文件
*/
public static void export(HttpServletRequest request, HttpServletResponse response, File file) {
String range = request.getHeader(HttpHeaders.RANGE);
String rangeSeparator = "-";
// 开始下载位置
long startByte = 0;
// 结束下载位置
long endByte = file.length() - 1;
// 如果是断点续传
if (range != null && range.contains("bytes=") && range.contains(rangeSeparator)) {
// 设置响应状态码为 206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split(rangeSeparator);
try {
// 判断 range 的类型
if (ranges.length == 1) {
// 类型一bytes=-2343
if (range.startsWith(rangeSeparator)) {
endByte = Long.parseLong(ranges[0]);
}
// 类型二bytes=2343-
else if (range.endsWith(rangeSeparator)) {
startByte = Long.parseLong(ranges[0]);
}
}
// 类型三bytes=22-2343
else if (ranges.length == 2) {
startByte = Long.parseLong(ranges[0]);
endByte = Long.parseLong(ranges[1]);
}
} catch (NumberFormatException e) {
// 传参不规范,则直接返回所有内容
startByte = 0;
endByte = file.length() - 1;
}
} else {
// 没有 ranges 即全部一次性传输,需要用 200 状态码,这一行应该可以省掉,因为默认返回是 200 状态码
response.setStatus(HttpServletResponse.SC_OK);
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
//要下载的长度endByte 为总长度 -1这时候要加回去
long contentLength = endByte - startByte + 1;
//文件名
String fileName = file.getName();
//文件类型
String contentType = request.getServletContext().getMimeType(fileName);
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.setContentDispositionFormData("attachment", URLUtil.encode(file.getName()));
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
// 这里文件名换你想要的inline 表示浏览器可以直接使用
// 参考资料https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + URLUtil.encode(fileName));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
// [要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + rangeSeparator + endByte + "/" + file.length());
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Last-Modified", new Date().toString());
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
return ResponseEntity
.ok()
.headers(headers)
.contentLength(file.length())
.contentType(mediaType)
.body(new FileSystemResource(file));
BufferedOutputStream outputStream;
RandomAccessFile randomAccessFile = null;
//已传送数据大小
long transmitted = 0;
try {
randomAccessFile = new RandomAccessFile(file, "r");
outputStream = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[4096];
int len = 0;
randomAccessFile.seek(startByte);
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
outputStream.write(buff, 0, len);
transmitted += len;
// 本地测试, 防止下载速度过快
// Thread.sleep(1);
}
// 处理不足 buff.length 部分
if (transmitted < contentLength) {
len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
outputStream.write(buff, 0, len);
transmitted += len;
}
outputStream.flush();
response.flushBuffer();
randomAccessFile.close();
// log.trace("下载完毕: {}-{}, 已传输 {}", startByte, endByte, transmitted);
} catch (ClientAbortException e) {
// ignore 用户停止下载
// log.trace("用户停止下载: {}-{}, 已传输 {}", startByte, endByte, transmitted);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}