From 73198d7852c48201da93369d8270e5e0722f96c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E4=BF=8A?= <873019219@qq.com> Date: Sat, 10 Apr 2021 18:06:10 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20:zap:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD,=20=E6=94=AF=E6=8C=81=E6=96=AD=E7=82=B9=E7=BB=AD?= =?UTF-8?q?=E4=BC=A0=E5=92=8C=E5=A4=9A=E7=BA=BF=E7=A8=8B=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/home/LocalController.java | 7 +- .../java/im/zhaojun/zfile/util/FileUtil.java | 137 +++++++++++++++--- 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/src/main/java/im/zhaojun/zfile/controller/home/LocalController.java b/src/main/java/im/zhaojun/zfile/controller/home/LocalController.java index 3768dba..64a5593 100644 --- a/src/main/java/im/zhaojun/zfile/controller/home/LocalController.java +++ b/src/main/java/im/zhaojun/zfile/controller/home/LocalController.java @@ -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 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); } } \ No newline at end of file diff --git a/src/main/java/im/zhaojun/zfile/util/FileUtil.java b/src/main/java/im/zhaojun/zfile/util/FileUtil.java index 9a8686d..eada1fb 100644 --- a/src/main/java/im/zhaojun/zfile/util/FileUtil.java +++ b/src/main/java/im/zhaojun/zfile/util/FileUtil.java @@ -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 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 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(); + } + } } }