赞
踩
简介:
大家应该都听说过分片上传(断点上传),那么断点下载又是什么呢?其实完全可以按照上传的理解
来理解断点续传、分片下载。下载文件的时候将一个大文件分成N个部分进行下载,然后前端再进行组合。
最终得到一个完整的文件。
但是呢,下载跟上传,后端的实现方式还是有区别的,上传需要把接口分成4个接口;但是下载不需要,
一个接口搞定;主要依赖http的Range(关于range,网上资料应该不少)头来进行处理(其实个人还考虑过
另外一种方式,未验证不知道是否可行;方式就是后端将文件进行切割,然后提供一个接口告诉前端某个文
件有多少个分片,前端分别调用接口获取各个分片,然后将分片文件进行合并,此方式是参考到分片上传的
假想)。此方法同样支持普通下载,不传入Range头就可进行普通下载;也可一次只下载一段(传入一个
range:bytes=0-10240);也可下载多段(传入多个range:bytes=0-10240,10241-20480);也可一次下载完文件
(range范围为整个文件即可:bytes=0-102400);前端怎样配合实现完全不知道,如果有哪位大佬知道的话,
真心求教!下面开始进行代码的编码
实现:
/** * 文件下载 * @author kevin * @param response : * @param range : * @param filePath : * @date 2021/1/17 */ @ApiOperation(value = "文件下载", notes = "downloadFile") @GetMapping(value = "/downloadFile") public void downloadFile(@RequestParam("fileId") String fileId, @RequestParam(name = "filePath", required = false) String filePath, HttpServletResponse response, @RequestHeader(name = "Range", required = false) String range) { List<FileInfo> fileInfo= fileMapper.getFileById(fileId); if(null == fileInfo){ throw new RuntimeException("下载失败,未找到需要下载的文件"); } filePath = StringUtils.isNotBlank(filePath) ? filePath : fileInfo.getFilePath(); File file = new File(filePath); String filename = file.getName(); long length = file.length(); Range full = new Range(0, length - 1, length); List<Range> ranges = new ArrayList<>(); //处理Range try { if (!file.exists()) { String msg = "需要下载的文件不存在:" + file.getAbsolutePath(); log.error(msg); throw new RuntimeException(msg); } if (file.isDirectory()) { String msg = "需要下载的文件的路径对应的是一个文件夹:" + file.getAbsolutePath(); log.error(msg); throw new RuntimeException(ResponseState.REQUEST_ERROR.getCode(), msg); } dealRanges(full, range, ranges, response, length); }catch (IOException e){ e.printStackTrace(); throw new RuntimeException("文件下载异常:" + e.getMessage()); } // 如果浏览器支持内容类型,则设置为“内联”,否则将弹出“另存为”对话框. attachment inline String disposition = "attachment"; // 将需要下载的文件段发送到客服端,准备流. try (RandomAccessFile input = new RandomAccessFile(file, "r"); ServletOutputStream output = response.getOutputStream()) { //最后修改时间 FileTime lastModifiedObj = Files.getLastModifiedTime(file.toPath()); long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneId.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC); //初始化response. response.reset(); response.setBufferSize(20480); response.setHeader("Content-type", "application/octet-stream;charset=UTF-8"); response.setHeader("Content-Disposition", disposition + ";filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setDateHeader("Last-Modified", lastModified); response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L); //输出Range到response outputRange(response, ranges, input, output, full, length); output.flush(); response.flushBuffer(); }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("文件下载异常:" + e.getMessage()); } } /** * 处理请求中的Range(多个range或者一个range,每个range范围) * @author kevin * @param range : * @param ranges : * @param response : * @param length : * @date 2021/1/17 */ private void dealRanges(Range full, String range, List<Range> ranges, HttpServletResponse response, long length) throws IOException { if (range != null) { // Range 头的格式必须为 "bytes=n-n,n-n,n-n...". 如果不是此格式, 返回 416. if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { response.setHeader("Content-Range", "bytes */" + length); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } // 处理传入的range的每一段. for (String part : range.substring(6).split(",")) { part = part.split("/")[0]; // 对于长度为100的文件,以下示例返回: // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100). int delimiterIndex = part.indexOf("-"); long start = Range.sublong(part, 0, delimiterIndex); long end = Range.sublong(part, delimiterIndex + 1, part.length()); //如果未设置起始点,则计算的是最后的 end 个字节;设置起始点为 length-end,结束点为length-1 //如果未设置结束点,或者结束点设置的比总长度大,则设置结束点为length-1 if (start == -1) { start = length - end; end = length - 1; } else if (end == -1 || end > length - 1) { end = length - 1; } // 检查Range范围是否有效。如果无效,则返回416. if (start > end) { response.setHeader("Content-Range", "bytes */" + length); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } // 添加Range范围. ranges.add(new Range(start, end, end - start + 1)); } }else{ //如果未传入Range,默认下载整个文件 ranges.add(full); } } /** * output写流输出到response * @author kevin * @param response : * @param ranges : * @param input : * @param output : * @param full : * @param length : * @date 2021/1/17 */ private void outputRange(HttpServletResponse response, List<Range> ranges, RandomAccessFile input, ServletOutputStream output, Range full, long length) throws IOException { if (ranges.isEmpty() || ranges.get(0) == full) { // 返回整个文件. response.setContentType("application/octet-stream;charset=UTF-8"); response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total); response.setHeader("Content-length", String.valueOf(full.length)); response.setStatus(HttpServletResponse.SC_OK); // 200. Range.copy(input, output, length, full.start, full.length); } else if (ranges.size() == 1) { // 返回文件的一个分段. Range r = ranges.get(0); response.setContentType("application/octet-stream;charset=UTF-8"); response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total); response.setHeader("Content-length", String.valueOf(r.length)); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // 复制单个文件分段. Range.copy(input, output, length, r.start, r.length); } else { // 返回文件的多个分段. response.setContentType("multipart/byteranges; boundary=MULTIPART_BYTERANGES"); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206. // 复制多个文件分段. for (Range r : ranges) { //为每个Range添加MULTIPART边界和标题字段 output.println(); output.println("--MULTIPART_BYTERANGES"); output.println("Content-Type: application/octet-stream;charset=UTF-8"); output.println("Content-length: " + r.length); output.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total); // 复制多个需要复制的文件分段当中的一个分段. Range.copy(input, output, length, r.start, r.length); } // 以MULTIPART文件的边界结束. output.println(); output.println("--MULTIPART_BYTERANGES--"); } }
参考文章:http://blog.ncmem.com/wordpress/2023/11/16/springboot%e5%90%8e%e7%ab%af%e5%ae%9e%e7%8e%b0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e5%88%86%e7%89%87%e4%b8%8b%e8%bd%bd/
欢迎入群一起讨论
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。