断点续传实现

文章目录

  • 流程
  • 前端
  • 后端
    • 接口
    • 逻辑类
  • 问题
    • 分块文件清理问题!!!!!!

流程

上传视频的整体流程:
断点续传实现_第1张图片

1、前端对文件进行分块。
2、前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传
3、如果分块文件不存在则前端开始上传
4、前端请求媒资服务上传分块。
5、媒资服务将分块上传至MinIO。
6、前端将分块上传完毕请求媒资服务合并分块。
7、媒资服务判断分块上传完成则请求MinIO合并文件。
8、合并完成校验合并后的文件是否完整,如果完整则上传完成,否则删除文件。

前端

组件

<el-upload
  action='#'
  :auto-upload='false'
  :accept="'video/*'"
  :show-file-list='false'

el-upload>

方法


 /**
   * 文件状态改变时的钩子
   */
  // 文件分块上传 
  private handleChange(
    file: ElUploadInternalFileDetail,
    fileList: ElUploadInternalFileDetail[]
  ) {
    this.fileList = fileList
    uploadByPieces({
      file,
      pieceSize: 1, //分片大小
      success: (data) => {
        file.percentage = (data.num/data.chunkCount) * 100
        console.log('success::' + data)
      },
      error: (e) => {
        console.log(file, fileList)
        // 出错了可以从列表中删除吧
        // fileList.forEach((n,i) => { if(n.uid == file.uid){
        //   fileList.splice(i,1)
        // } })
        console.log('error::' + '视频分片上传失败')
      }
    })
  }

分块工具

export const uploadByPieces = ({ file, pieceSize = 2, success, error }: any) => {
  // 上传过程中用到的变量
  let fileMD5 = '' // md5加密文件的标识
  // 传过来  默认是1,所以 每个分块是1mb
  const chunkSize = pieceSize * 1024 * 1024  // 分片大小
  const chunkCount = Math.ceil(file.size / chunkSize) // 总片数

  //得到某一片的分片
  const getChunkInfo = (file, currentChunk, chunkSize) => {
    let start = currentChunk * chunkSize
    let end = Math.min(file.size, start + chunkSize)
    let chunk = file.raw.slice(start, end)
    return chunk
  }

  // 第一步
  const readFileMD5 = () => {
    // 得到 第一片和 最后一片
    const startChunk = getChunkInfo(file, 0, chunkSize)
    const endChunk = getChunkInfo(file, chunkCount - 1, chunkSize)
    //  对第一片进行转码然 后md5加密,网上很多是直接 对整个文件转码加密得到标识,但是我发现大文件尤其是几个G的文件会崩溃,所以我是  先分片然后取第一片加密
    let fileRederInstance = new FileReader()
    fileRederInstance.readAsBinaryString(file.raw)
    fileRederInstance.addEventListener('load', (e) => {
      let fileBolb = (e.target as any).result
      fileMD5 = CryptoJS.MD5(CryptoJS.enc.Latin1.parse(fileBolb)).toString()
      // 上传前提交注册 - 参数
      const params = {
        fileMd5: fileMD5
        // fileName:file.name,
        // fileSize:file.size,
        // mimetype:file.raw.type,
        // fileExt:file.name.split('.').at(-1)
      }
      // 上传前提交注册 - 接口调用
      upRegister(params).then(res => {
        // 文件不存在就上传
        if (res.code == 0) {
          readChunkMD5(0)
        }
      }).catch(err => error(err))

    })
  }

  // 针对每个分片文件进行上传处理
  const readChunkMD5 = async (num) => {
    if (num <= chunkCount - 1) {
      //得到当前需要上传的分片文件
      const chunk = getChunkInfo(file, num, chunkSize)
      // 上传分块前检查
      //await checkchunk({fileMd5:fileMD5,chunk:num,chunkSize:chunkCount}).then( async res => {
      await checkchunk({ fileMd5: fileMD5, chunk: num }).then(async res => {
        if (res.code == 0 && res.result == false) {
          // 分块上传
          let fetchForm = new FormData()
          fetchForm.append('file', chunk)
          fetchForm.append('fileMd5', fileMD5)
          fetchForm.append('chunk', num)
          await upChunk(fetchForm).then(async res => {
            // 上传成功
            success({ num, chunkCount, state: 'uploading' })
            if (res.code == 0) {
              readChunkMD5(num + 1)
            }
          }).catch(err => {
            error(err)
          })
        } else {
          success({ num, chunkCount, state: 'uploading' })
          // 上传成功就开始下一块
          readChunkMD5(num + 1)
        }
      })
    } else {
      // 上传结束请求合并
      // 提交合并
      mergeChunks({
        fileMd5: fileMD5,
        fileName: file.name,
        chunkTotal: chunkCount
        // mimetype:file.raw.type,
        // fileExt:file.name.split('.').at(-1)
      }).then(res => {
        // 合并成功了
        success({ num, chunkCount, state: 'success' })
      }).catch(err => {
        error(err)
      })
    }
  }

  readFileMD5() // 开始执行代码
}

后端

接口

	 @Autowired
    private MediaFileService mediaFileService;

    @ApiOperation(value = "文件上传前检查文件")
    @PostMapping("/upload/checkfile")
    public RestResponse<Boolean> checkfile(
            @RequestParam("fileMd5") String fileMd5
    ) throws Exception {
        return mediaFileService.checkFile (fileMd5);
    }


    @ApiOperation(value = "分块文件上传前的检测")
    @PostMapping("/upload/checkchunk")
    public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5,
                                            @RequestParam("chunk") int chunk) throws Exception {
        return mediaFileService.checkChunk (fileMd5, chunk);
    }

    @ApiOperation(value = "上传分块文件")
    @PostMapping("/upload/uploadchunk")
    public RestResponse uploadchunk(@RequestParam("file") MultipartFile file,
                                    @RequestParam("fileMd5") String fileMd5,
                                    @RequestParam("chunk") int chunk) throws Exception {

        //创建临时文件
        File tempFile = File.createTempFile ("minio", "temp");
        //上传的文件拷贝到临时文件
        file.transferTo (tempFile);
        //文件路径
        String absolutePath = tempFile.getAbsolutePath ();
        return mediaFileService.uploadChunk (fileMd5, chunk, absolutePath);
    }


    @ApiOperation(value = "合并文件")
    @PostMapping("/upload/mergechunks")
    public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5,
                                    @RequestParam("fileName") String fileName,
                                    @RequestParam("chunkTotal") int chunkTotal) throws Exception {
        Long companyId = 1232141425L;

        UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto ();
        uploadFileParamsDto.setFileType ("001002");
        uploadFileParamsDto.setTags ("课程视频");
        uploadFileParamsDto.setRemark ("");
        uploadFileParamsDto.setFilename (fileName);

        return mediaFileService.mergeChunks (companyId, fileMd5, chunkTotal, uploadFileParamsDto);

    }

逻辑类

检查 整个文件可在 minio

  public RestResponse<Boolean> checkFile(String fileMd5) throws Exception {
        MediaFiles mediaFiles = getById (fileMd5);
        // 数据库存在,查 minio
        if (mediaFiles != null) {
            String bucket = mediaFiles.getBucket ();
            String filePath = mediaFiles.getFilePath ();
            FilterInputStream filterInputStream = minioClient.getObject (GetObjectArgs.builder ()
                    .bucket (bucket).object (filePath).build ());

            // minio存在
            if (filterInputStream != null) return RestResponse.success (true);

        }
        // 文件不存在
        return RestResponse.success (false);
    }


//得到分块文件的目录
 private String getChunkFileFolderPath(String fileMd5) {
     return fileMd5.substring (0, 1) + "/" + fileMd5.substring (1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
 }

上传分块前 检查分块是否存在 minio

public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
       String chunkFilePath = getChunkFileFolderPath (fileMd5) + chunkIndex;
       GetObjectResponse getRes = null;
       try {
           getRes = minioClient.getObject (GetObjectArgs.builder ().object (chunkFilePath).bucket (videofiles).build ());
       } catch (Exception e) {
           // 分块不存在
           return RestResponse.success (false);
       }
       // 分块存在
       if (getRes != null) return RestResponse.success (true);
       // 分块不存在
       return RestResponse.success (false);
   }

上传分块文件到minio

// 获取mimeType 
private String getMimeType(String extension) {
    if (extension == null)
        extension = "";
    //根据扩展名取出mimeType
    ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch (extension);
    //通用mimeType,字节流
    String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    if (extensionMatch != null) {
        mimeType = extensionMatch.getMimeType ();
    }
    return mimeType;
}
  public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        //得到 分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath (fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;
        //mimeType
        String mimeType = getMimeType (null);
        //将文件存储至minIO
        boolean b = addFileToMinIo (localChunkFilePath, mimeType, videofiles, chunkFilePath);
        if (!b) {
            log.debug ("上传分块文件失败:{}", chunkFilePath);
            return RestResponse.validfail (false, "上传分块失败");
        }
        log.debug ("上传分块文件成功:{}", chunkFilePath);
        return RestResponse.success (true);

    }
private boolean addFileToMinIo(String localFilePath, String mimeType, String bucket, String objectName) {
    try {
        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder ()
                // 哪个桶
                .bucket (bucket)
                // 给文件取个名,可以有多层目录
                .object (objectName)
                // 源文件路径
                .filename (localFilePath)
                .contentType (mimeType)
                .build ();

        minioClient.uploadObject (uploadObjectArgs);
        log.debug ("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
        System.out.println ("上传成功");
        return true;
    } catch (Exception e) {
        log.error ("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage (), e);
        XueChengPlusException.cast ("上传文件到文件系统失败");

    }
    return false;


}

合并分块文件

  public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) throws Exception {
        String filename = uploadFileParamsDto.getFilename ();
        // 合并
        // 得到 分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath (fileMd5);
        List<ComposeSource> sources = new ArrayList<> ();
        for (int i = 0; i < chunkTotal; i++) {
            String chunkFilePath = chunkFileFolderPath + i;
            ComposeSource composeSource = ComposeSource.builder ()
                    .bucket (videofiles)
                    .object (chunkFilePath)
                    .build ();
            sources.add (composeSource);

        }
        String extName = filename.substring (filename.lastIndexOf ("."));
        // 存储合并文件的地址
        String filePath = getFilePathByMd5 (fileMd5, extName);

        try {
            //合并文件
            ObjectWriteResponse response = minioClient.composeObject (
                    ComposeObjectArgs.builder ()
                            .bucket (videofiles)
                            .object (filePath)
                            .sources (sources)
                            .build ());
            log.debug ("合并文件成功:{}", filePath);
        } catch (Exception e) {
            log.debug ("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage (), e);
            return RestResponse.validfail (false, "合并文件失败。");
        }


        // 合并完成后 校验
        File file = null;
        try {
            file = downloadFile (videofiles, filePath);
        } catch (IOException e) {
            log.error ("下载合并后文件失败!!");
            return RestResponse.validfail (false, "下载合并后文件失败。");
        }


        String downloadMd5 = null;

        try (FileInputStream fileInputStream = new FileInputStream (file)) {
            downloadMd5 = DigestUtils.md5Hex (fileInputStream);
        } catch (Exception e) {
            throw new RuntimeException ("获取文件md5失败");
        } finally {
            if (file != null) {
                file.delete ();
            }
        }

        if (!fileMd5.equals (downloadMd5)) {
            log.error ("校验失败,删除该文件:{}", filePath);
            // 删除文件
            minioClient.removeObject (RemoveObjectArgs.builder ().bucket (videofiles).object (filePath).build ());

            return RestResponse.validfail (false, "文件合并校验失败,最终上传失败。");
        }
        // 文件入库
        currentProxy.addMediaFilesToDb (companyId, fileMd5, uploadFileParamsDto, videofiles, filePath);


        // 校验结束,删除分块
        removeChunks (videofiles, chunkFileFolderPath, chunkTotal);


        // 完成合并
        return RestResponse.success (true);
    }

    /**
     * 清除分块
     *
     * @param bucket
     * @param chunkFileFolderPath
     * @param chunkTotal
     */
    private void removeChunks(String bucket, String chunkFileFolderPath, int chunkTotal) {

        // 存储删除分块的集合
        ArrayList<DeleteObject> list = new ArrayList<> ();


        for (int i = 0; i < chunkTotal; i++) {
            DeleteObject deleteObject = new DeleteObject (chunkFileFolderPath + i);
            list.add (deleteObject);
        }
        RemoveObjectsArgs build = RemoveObjectsArgs.builder ().bucket (bucket).objects (list).build ();
        minioClient.removeObjects (build);


    }


    private File downloadFile(String bucket, String filename) throws IOException {

        FileOutputStream fos = null;
        // 下载的文件
        File minioFile = null;
        try {

            InputStream inputStream = minioClient.getObject (GetObjectArgs.builder ()
                    .bucket (bucket)
                    .object (filename).build ());

            minioFile = File.createTempFile ("minio", ".merge");
            fos = new FileOutputStream (minioFile);
            IoUtils.copy (inputStream, fos);
            return minioFile;
        } catch (Exception e) {
            log.error ("下载文件失败:{},文件名:{}", bucket, filename);
            throw new RuntimeException (e);
        } finally {
            if (fos != null) {
                fos.close ();
            }
        }


    }

    /**
     * 得到合并后的文件的地址
     *
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.substring (0, 1) + "/" + fileMd5.substring (1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

问题

分块文件清理问题!!!!!!

上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?
1、在数据库中有一张文件表记录minio中存储的文件信息。
2、文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。
3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。

你可能感兴趣的:(学习记录,前端,javascript,开发语言)