美文网首页
springboot + vue 分片上传,分片下载

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

作者: 三没产品 | 来源:发表于2022-05-24 11:08 被阅读0次

    分片上传

    前端代码

     <progress class="progress" :value="fileObj.progress" ></progress>
     <span class="desc">{{fileObj.progressText ? fileObj.progressText : '正在解析文件'}}</span>
     <input type="file" @change="handleFileInputChange"/>
    
     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<FileSliceVo> 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 自动播放
    */
     <video :src="avSrc" controls="controls" width="100%" crossorigin="anonymous" autoplay></video>
    

    后端代码

    /**
    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 分片上传,分片下载

          本文链接:https://www.haomeiwen.com/subject/xwcjprtx.html