大文件分片下载

解决大文件上传超时问题,使用Range支持,对文件进行分片下载
步骤:
一、首先通过发送0-1长度去后端获取文件大小、名称等信息返回给前端
二、前端通过文件大小、分片大小计算出分片数据量,循环请求后端,分片获取文件数据,前端组合Blob数组数据,记录当前请求的索引和数据进行组合
三、全部异步请求完毕之后,对所有数据进行从大到小排序,从新生成一个新的Blob,一定要保证数组的顺序正确,不然打开文件会有异常
在这里插入图片描述

多线程异步下载

<template>
  <div style="width: 400px;margin: 0 auto;margin-top: 20px">
    <div style="display: flex;gap: 10px">
      <a-button @click="downloadBatch" id="download">多线程下载 {{ (fileSize/1073741824).toFixed(2) }} GB</a-button>
    </div>
    <div>
      {{ downloadText }}
    </div>
    <div style="margin-top: 16px;text-align: center">
      <a-progress type="circle" :percent="percentage" />
    </div>
  </div>
</template>
<script>

import axios from 'axios'
import to from '@/utils/to'

export default {
  name: 'Download',
  data () {
    return {
      percentage: 0,
      currentBlob: null,
      url: '/download/file',
      chunkSize: 1024 * 1024 * 20, // 单个分段大小 10M
      totalChunk: 1, // 总共分几段下载
      fileSize: 0, // 文件大小
      downloadText: ''
    }
  },
  methods: {
    // 初始化一个空的 Blob 对象
    initializeBlob () {
      this.currentBlob = new Blob([])
    },
    // 向 Blob 对象添加新内容
    appendContentToBlob (content) {
      if (!this.currentBlob) {
        this.initializeBlob()
      }
      const newBlob = new Blob([this.currentBlob, content])
      this.currentBlob = newBlob
    },
    // 下载 Blob 对象
    downloadBlob (fileName) {
      if (!this.currentBlob) {
        console.error('Blob 对象为空')
        return
      }
      const downloadLink = document.createElement('a')
      const fileUrl = URL.createObjectURL(this.currentBlob)
      downloadLink.href = fileUrl
      downloadLink.download = fileName // 设置下载的文件名
      downloadLink.click()
      // 释放临时URL对象
      URL.revokeObjectURL(fileUrl)
      this.percentage = 100
    },
    // 下载 Blob 对象
    downloadBlobs (fileName, blobs) {
      if (!blobs) {
        console.error('Blob 对象为空')
        return
      }
      const downloadLink = document.createElement('a')
      const fileUrl = URL.createObjectURL(new Blob(blobs))
      downloadLink.href = fileUrl
      downloadLink.download = fileName // 设置下载的文件名
      downloadLink.click()
      // 释放临时URL对象
      URL.revokeObjectURL(fileUrl)
      this.percentage = 100
    },
    // 批量下载组合数据
    downloadRange (url, start, end, i) {
      return new Promise(async (resolve, reject) => {
        const [err, res] = await to(axios({
          method: 'get',
          url: url,
          data: {},
          headers: { range: `bytes=${start}-${end}` },
          responseType: 'blob',
          timeout: 6000000 // 请求超时时间
        }))
        if (err) {
          reject(err)
        }
        // 计算下载百分比  当前下载的片数/总片数
        this.percentage = Number.parseFloat(Number((i / this.totalChunk) * 100).toFixed(2))
        this.downloadText = `共切分:${this.totalChunk}片,第${i + 1}片上传完成`
        resolve({
          i,
          buffer: res.data
        })
      })
    },
    async downloadBatch () {
      // 发送第一次请求,从后端获取下载文件的大小等信息
      const [err, res] = await to(axios({
        method: 'get',
        url: this.url,
        async: true,
        data: {},
        headers: { range: `bytes=0-1` },
        responseType: 'blob',
        timeout: 6000000 // 请求超时时间
      }))
      if (err) return false
      if (res.status === 200 || res.status === 206) {
        // 文件名称
        const fileName = decodeURIComponent(res.headers['fname'])
        // 截取文件总长度和最后偏移量
        const result = res.headers['content-range'].split('/')
        // 获取文件总大小,方便计算下载百分比
        this.fileSize = parseInt(result[1], 10) // 转为整数
        // 计算总共分片数,向上取整
        this.totalChunk = Math.ceil(this.fileSize / this.chunkSize)
        const arr = []
        for (let i = 0; i < this.totalChunk; i++) {
          const start = i * this.chunkSize
          let end
          if (i === this.totalChunk - 1) {
            end = this.fileSize - 1 // 最后一片的结束位置
          } else {
            end = (i + 1) * this.chunkSize - 1
          }
          console.log(`Chunk ${i + 1}: Start: ${start}, End: ${end}, FileSize:${this.fileSize}`)
          arr.push(this.downloadRange(this.url, start, end, i))
        }
        Promise.all(arr).then(res => {
          const blobs = res.sort(item => item.i - item.i).map(item => (item.buffer))
          this.downloadText = `结果集长度为:${blobs.length}`
          this.downloadBlobs(fileName, blobs)
        })
      }
    }
  }
}
</script>

<style scoped lang="less">

</style>

to 工具类

/**
 * 捕获异常是使用try/catch的方式来处理,因为await后面跟着的是Promise对象,当有异常的情况下会被Promise对象的内部
 * catch捕获,而await就是一个then的语法糖,并不会捕获异常, 那就需要使用try/catch来捕获异常,并进行相应的逻辑处理。
 * @param promise
 * @returns {Promise<T | *[]>}
 */
export default function to (promise) {
  if (!promise || !Promise.prototype.isPrototypeOf(promise)) {
    return new Promise((resolve, reject) => {
      reject(new Error(`${promise}\r\n requires promises as the param"`))
    }).catch((err) => {
      return [err, null]
    })
  }
  return promise.then(data => {
    return [null, data]
  }).catch(err => [err])
}

Java后端代码

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Optional;

/**
 * @version V1.0
 * @Title:
 * @ClassName: DownLoadController
 * @Description:
 */
@RequestMapping("/download")
@RestController
@Api(tags = "文件下载")
@Slf4j
public class DownLoadController {

    private final static String utf8 = "utf-8";

    @GetMapping("/file")
    public void downLoadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 设置编码格式
        response.setCharacterEncoding(utf8);
        //获取文件路径
        String drive = "F";
        String fileName = "AdobeAcrobatProDC_setup.zip";
        //参数校验


        log.info(fileName, drive);
        //完整路径(路径拼接待优化-前端传输优化-后端从新格式化  )
        String pathAll = drive + ":\\" + fileName;
        log.info("pathAll{}", pathAll);
        Optional<String> pathFlag = Optional.ofNullable(pathAll);
        File file = null;
        if (pathFlag.isPresent()) {
            //根据文件名,读取file流
            file = new File(pathAll);
            log.info("文件路径是{}", pathAll);
            if (!file.exists()) {
                log.warn("文件不存在");
                return;
            }
        } else {
            //请输入文件名
            log.warn("请输入文件名!");
            return;
        }

        InputStream is = null;
        OutputStream os = null;
        try {
            //获取长度
            long fSize = file.length();
            response.setContentType("application/x-download");
            String file_Name = URLEncoder.encode(file.getName(), "UTF-8");
            response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
            //根据前端传来的Range  判断支不支持分片下载
            response.setHeader("Accept-Range", "bytes");
            // response.setHeader("fSize",String.valueOf(fSize))
            response.setHeader("fName", file_Name);
            //定义断点
            long pos = 0, last = fSize - 1, sum = 0;
            //判断前端需不需要分片下载
            if (null != request.getHeader("Range")) {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                String numRange = request.getHeader("Range").replaceAll("bytes=", "");
                String[] strRange = numRange.split("-");
                if (strRange.length == 2) {
                    pos = Long.parseLong(strRange[0].trim());
                    last = Long.parseLong(strRange[1].trim());
                    //若结束字节超出文件大小 取文件大小
                    if (last > fSize - 1) {
                        last = fSize - 1;
                    }
                } else {
                    //若只给一个长度  开始位置一直到结束
                    pos = Long.parseLong(numRange.replaceAll("-", "").trim());
                }
            }
            long rangeLenght = last - pos + 1;
            String contentRange = new StringBuffer("bytes").append(pos).append("-").append(last).append("/").append(fSize).toString();
            response.setHeader("Content-Range", contentRange);
            os = new BufferedOutputStream(response.getOutputStream());
            is = new BufferedInputStream(new FileInputStream(file));
            //跳过已读的文件(重点,跳过之前已经读过的文件)
            is.skip(pos);
            byte[] buffer = new byte[1024];
            int lenght = 0;
            //相等证明读完
            while (sum < rangeLenght) {
                lenght = is.read(buffer, 0, (rangeLenght - sum) <= buffer.length ? (int) (rangeLenght - sum) : buffer.length);
                sum = sum + lenght;
                os.write(buffer, 0, lenght);
            }
            log.info("下载完成");
        } finally {
            if (is != null) {
                is.close();
            }
            if (os != null) {
                os.close();
            }
        }
    }
}

GitHub 加速计划 / vu / vue
207.52 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:1 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 3 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐