springboot + vue 分片上传,分片下载

分片上传

前端代码

 
 {{fileObj.progressText ? fileObj.progressText : '正在解析文件'}}
 

 methods: {
    handleFileInputChange(e) {
          let _this = this;
          if (e.target.files) {
             let file = e.target.files[0];
             let sha1 = hex_sha1(file.name + '-' + file.lastModified);//用于前端判断该文件是否已上传
             let item = {
                  progressText: '等待上传', //上传提示
                  progress: 0, //进度
                  file: file, // 文件本身
                  retry: 0,// 重试次数
                  del: false, // 删除标志
                  sha1: sha1, // 前端判断标识
                  uuid: UUID.generate().replace(/-/g, ''), //作为该文件的唯一标识
                  idx: 0, //当前分片
                  chunkNum: Math.ceil(file.size / bufferLength), // 计算分片数量, 向上取整
                  finishIdx: 0//完成上传的分片数
              }
              _this.upload(item);
             e.target.value = '';
         }
    },
    /*上传*/
    async upload(item) {
        let _this = this;
        item.progressText = '正在上传(' + Math.round(item.finishIdx / item.chunkNum * 100) + '%)';
        _this.uploadSlice(item);
    },
    /*上传*/
    uploadSlice(item) {
        if (item.del) return;
        let _this = this;
        let file = item.file;
        // 每片分片大小
        let bufferLength = 1024 * 1024;
        //开始切割位置
        let start = item.idx * bufferLength;
        //全部上传完毕或重试次数用完则退出
        if (start >= file.size) return;
        //计算分割的位置
        let end = start + bufferLength;
        //如果分割点超出文件大小,回退分割点
        if (end > file.size) {
            end = file.size;
        }
        //切割文件
        let chunk = file.slice(start, end);
        let fileReader = new FileReader();
        //该方法用于将File对象转化为二进制文件
        fileReader.readAsBinaryString(chunk);
        fileReader.onload = function (e) {
            //e.target.result为读取到的分片的二进制
            //创建 formData 对象并添加数据
            let formData = new FormData();
            formData.set("uuid", item.uuid);
            formData.set('sha1', hex_sha1(e.target.result));//每个分片的sha1
            formData.set("file", chunk, file.name);
            formData.set("idx", item.idx);
            formData.set("totalIdx", item.chunkNum);
            formData.set("start", start);
            formData.set("end", end);
            formData.set("bufferLength", bufferLength);
            formData.set("totalSize", file.size);
            _this.$http.post(_this.src, formData).then((res) => {
                item.finishIdx += 1;
                let p = item.finishIdx / item.chunkNum;
                item.progress = p;
                if (p === 1) {
                    item.progressText = '上传完成(100%)';
                    if (res.code === 1 && res.data) {
                        _this.$emit('complete', res.data)
                    }
                } else {
                    item.progressText = '正在上传(' + Math.round(p * 100) + '%)';
                }
                if (item.idx === item.chunkNum) return;
                item.idx += 1;
                _this.uploadSlice(item);
            }).catch(err => {
                console.log(err);
                //失败后可以重试上传对应分片
           })
        }
    }
 }

后端代码

重点:只要使用了RandomAccessFile对文件进行处理(好像还有FileChannel可以实现)

//FileSliceVo 实体类
public class FileSliceVo implements Serializable {
    private static final long SerialVersionUID = 1L;
    private Integer idx;
    private Long start;
    private Long end;
    private Long bufferLength;
    private Integer totalSize;
    private String sha1;
    private Integer totalIdx;
    private long idxSize;
    private String uuid;
    private String path;
    private String filename;
}

public AjaxResult upload(MultipartFile file, FileSliceVo sliceVo) {
   if (file == null || file.isEmpty()) {
       return AjaxResult.fail("请选择要上传的文件");
   }
   log.info("sha1:{} - totalSize:{} - bufferLength: {} - totalIdx: {} - idx:{} - idxSize: {} - start: {} - end: {}", sliceVo.getSha1(), sliceVo.getTotalSize(), sliceVo.getBufferLength(), sliceVo.getTotalIdx(), sliceVo.getIdx(), file.getSize(), sliceVo.getStart(), sliceVo.getEnd());
   //检测对应分片是否已上传
   List sliceVos = RedisStorage.get(RedisKeys.FILE_KEY + sliceVo.getUuid(), () -> ListUtil.list(false), 3600L);
        if (CollectionUtils.isNotEmpty(sliceVos.stream().filter(f -> f.getSha1().equals(sliceVo.getSha1())).collect(Collectors.toList()))) {
            return AjaxResult.ok();
        }
   }
   // 用uuid的后两位作为文件夹的名称
   String format = sliceVo.getUuid().substring(sliceVo.getUuid().length() - 2);
   // 文件夹路径
   String baseUrl = appConfiguration.getFilePath() + File.separator + format;
   // 用前端传过来的uuid作为文件名称
   String filename = sliceVo.getUuid() + "." + FileUtils.getExtension(file.getOriginalFilename());
   // 文件夹路径 文件对象
   File serviceFile = new File(baseUrl);
   // 文件路劲 文件对象
   File tmpFile = new File(baseUrl, filename);
   // 检测文件夹是否已存在,不存在就创建
   if (!serviceFile.exists()) {
       serviceFile.mkdirs();
   }
   sliceVo.setPath(format + File.separator + filename);
   sliceVo.setFilename(file.getOriginalFilename());
   sliceVo.setIdxSize(file.getSize()); // 该分片的文件大小
   try (RandomAccessFile accessFile = new RandomAccessFile(tmpFile, "rw")) {
         accessFile.seek(sliceVo.getStart());
         accessFile.write(file.getBytes());
         sliceVos.add(sliceVo);
         JedisUtil.setObjectValue(RedisKeys.FILE_KEY + sliceVo.getUuid(), sliceVos, 3600L);
         long sum = sliceVos.stream().mapToLong(FileSliceVo::getIdxSize).sum();
         // 检测是否文件上传完成
         if (sliceVos.size() == sliceVo.getTotalIdx() && (sum + "").equals(sliceVo.getTotalSize().toString())) {
              //文件上传完成后,保存文件信息
             String sha1 = SecureUtil.sha1(tmpFile);
             ItFiles itFiles = filesService.selectBySha1(sha1);
             if (itFiles == null) {
                 itFiles = new ItFiles();
                 itFiles.setFilename(file.getOriginalFilename());
                 itFiles.setCreateAt(LocalDateTime.now());
                 itFiles.setPath(format + File.separator + filename);
                 itFiles.setFilesize(sliceVo.getTotalSize());
                 itFiles.setSha1(sha1);
                 filesService.save(itFiles);
             }
             //  删除对应的redis数据
            JedisUtil.del(RedisKeys.FILE_KEY + sliceVo.getUuid());
        }
   }

分片上传参考的是tus协议

视频文件分片下载(其他大文件亦可参考)

前端代码(vue)

/*
avSrc: 文件获取地址
crossorigin="anonymous"  允许跨域
autoplay 自动播放
*/
 

后端代码

/**
range: video 会在请求头中自动添加Range字段,Range: bytes=1048576-
使用RandomAccessFile 读取对应的文件分片
*/
public void fileSlice(HttpServletResponse response, @RequestHeader String range, @RequestParam String params, @RequestParam String sign) throws IOException {
    log.info("range -> {}", range);
    if (MD5Utils.md5(params + FileUtils.KEY).equals(sign)) {
        File file = new File(appConfiguration.getFilePath() + File.separator + params);
        if (file.exists()) {
            try (OutputStream out = response.getOutputStream();RandomAccessFile accessFile = new RandomAccessFile(file, "r")) {
                long fileLength = file.length();
                long start = Long.parseLong(range.substring(range.indexOf("=") + 1, range.indexOf("-")));
                long end = Math.min(start + 1024 * 1024 - 1, fileLength - 1);

                //设定文件读取开始位置(以字节为单位)
                accessFile.seek(start);
                int contentLength = (int) (end - start + 1);

                //返回码需要为206,代表只处理了部分请求,响应了部分数据
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                response.setHeader("Content-Type", "video/mp4");
                //设置此次相应返回的数据长度
                response.setContentLength(contentLength);
                //设置此次相应返回的数据范围,  range的end最大为fileLen-1,最后一段需设置为 xxx-fileLen-1/fileLen
                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
                response.setHeader("Accept-Ranges", "bytes");

                byte[] cache = new byte[1024];
                int len;
                while ((len = accessFile.read(cache)) != -1) {
                    out.write(cache, 0, len);
                }
            } catch (Exception e) {
                log.error("file down", e);
            }
        } else {
            response.setContentType(ContentType.TEXT_PLAIN.getValue());
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("文件不存在");
            response.getWriter().close();
        }
    }
}

分片播放参考地址:Spring boot实现视频播放断点续传 - BuptWade - 博客园 (cnblogs.com)

你可能感兴趣的:(springboot + vue 分片上传,分片下载)