springboot 大文件分片下载

针对大文件下载比较慢得问题,或者超出ie浏览器限制得2G大小,需要考虑使用分片下载,废话不多说直接上代码

一、controller层接口定义,方法设置参数Range,用于接收前端请求头中得Range文件范围信息

@ApiOperation(value = "文件下载", notes = "downloadFile")
@GetMapping(value = "material/folder/download")
public void downloadFile(@ApiParam("文件夹节点id") @RequestParam("folderId") String folderId,
                         @ApiParam("文件夹根节点名称") @RequestParam("folderName") String folderName,
                         HttpServletResponse response, HttpServletRequest request,
                         @RequestHeader(name = "Range", required = false) String range) {

    File file = this.projectFacade.packageFolderV3(folderId);
    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(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()) {
        //初始化response.
        response.reset();
        response.setHeader("Content-type", "application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Disposition", disposition + ";filename=" +
                URLEncoder.encode(folderName+".zip", StandardCharsets.UTF_8.name()));
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.setHeader("ETag", URLEncoder.encode(folderName+".zip", StandardCharsets.UTF_8.name()));
        response.setDateHeader("Expires", 0);
        response.setHeader("Pragma", "no-cache");
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
        //输出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,每个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() || full.equals(ranges.get(0))) {
        // 返回整个文件.
        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 + "/" + length);
        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--");
    }
}

四、内部实体类Range

private static class Range {
    long start;
    long end;
    long length;
    long total;

    /**
     * Range段构造方法.
     *
     * @param start range起始位置.
     * @param end   range结束位置.
     * @param total range段的长度.
     */
    public Range(long start, long end, long total) {
        this.start = start;
        this.end = end;
        this.length = end - start + 1;
        this.total = total;
    }

    public static long sublong(String value, int beginIndex, int endIndex) {
        String substring = value.substring(beginIndex, endIndex);
        return (substring.length() > 0&& !StringUtils.isEmpty(substring)) ? Long.parseLong(substring) : -1;
    }

    private static void copy(RandomAccessFile randomAccessFile, OutputStream output, long fileSize, long start, long length) throws IOException {
        byte[] buffer = new byte[4096];
        int read = 0;
        long transmitted = 0;
        if (fileSize == length) {
            randomAccessFile.seek(start);
            //需要下载的文件长度与文件长度相同,下载整个文件.
            while ((transmitted + read) <= length && (read = randomAccessFile.read(buffer)) != -1){
                output.write(buffer, 0, read);
                transmitted += read;
            }
            //处理最后不足buff大小的部分
            if(transmitted < length){
                log.info("最后不足buff大小的部分大小为:" + (length - transmitted));
                read = randomAccessFile.read(buffer, 0, (int)(length - transmitted));
                output.write(buffer, 0, read);
            }
        } else {
            randomAccessFile.seek(start);
            long toRead = length;

            //如果需要读取的片段,比单次读取的4096小,则使用读取片段大小读取
            if(toRead < buffer.length){
                output.write(buffer, 0, randomAccessFile.read(new byte[(int) toRead]));
                return;
            }
            while ((read = randomAccessFile.read(buffer)) > 0) {
                toRead -= read;
                if (toRead > 0) {
                    output.write(buffer, 0, read);
                } else {
                    output.write(buffer, 0, (int) toRead + read);
                    break;
                }
            }

        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Range range = (Range) o;
        return start == range.start &&
                end == range.end &&
                length == range.length &&
                total == range.total;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        return Objects.hash(prime, start, end, length, total);
    }
}

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
文件分片下载是一种常见的下载方式,可以通过将大文件分成多个小文件下载,从而提高下载速度和稳定性。在 Vue 和 SpringBoot 中,可以通过以下步骤实现大文件分片下载: 1. 将大文件分成多个小文件,每个小文件大小为固定的值,比如1MB或2MB。 2. 在前端 Vue 中,使用 axios 发送请求,并设置请求头 Range,表示请求文件的某个片段。 3. 在后端 SpringBoot 中,接收前端请求,并根据请求头 Range,返回对应的文件片段。 4. 前端 Vue 接收到多个文件片段后,将它们合并成一个完整的文件。 5. 下载完成后,将多个小文件删除,以释放存储空间。 具体实现细节可以参考以下代码: 前端 Vue: ``` downloadFile() { const url = '/api/download'; const file_name = 'large_file.mp4'; const chunk_size = 2 * 1024 * 1024; // 2MB per chunk const total_size = 1024 * 1024 * 1024; // 1GB const total_chunks = Math.ceil(total_size / chunk_size); const headers = { 'Content-Type': 'application/json' }; axios.post(url, { file_name, chunk_size, total_size }, { headers }) .then(res => { const { data } = res; const blobs = data.map((chunk, index) => { return new Blob([chunk], { type: 'application/octet-stream' }); }); const blob = new Blob(blobs, { type: 'application/octet-stream' }); const object_url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = object_url; link.download = file_name; link.click(); window.URL.revokeObjectURL(object_url); }); } ``` 后端 SpringBoot: ``` @PostMapping("/download") public List<byte[]> downloadFile(@RequestBody Map<String, Object> params, HttpServletRequest request, HttpServletResponse response) throws IOException { String file_name = (String) params.get("file_name"); int chunk_size = (int) params.get("chunk_size"); int total_size = (int) params.get("total_size"); int total_chunks = (int) Math.ceil((double) total_size / chunk_size); String range_header = request.getHeader("Range"); if (range_header == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return null; } String[] range = range_header.split("=")[1].split("-"); int start = Integer.parseInt(range[0]); int end = range.length > 1 ? Integer.parseInt(range[1]) : start + chunk_size - 1; if (end >= total_size) { end = total_size - 1; } response.setHeader("Content-Type", "application/octet-stream"); response.setHeader("Content-Length", String.valueOf(end - start + 1)); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + total_size); response.setHeader("Accept-Ranges", "bytes"); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); RandomAccessFile file = new RandomAccessFile(file_name, "r"); file.seek(start); byte[] bytes = new byte[chunk_size]; List<byte[]> chunks = new ArrayList<>(); int read = 0; while (read < chunk_size && start + read <= end) { int n = file.read(bytes, read, chunk_size - read); if (n <= 0) { break; } read += n; } chunks.add(Arrays.copyOf(bytes, read)); file.close(); return chunks; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值