当前位置:   article > 正文

Vue + SpringBoot 实现文件的断点上传、秒传,存储到Minio_springboot整合oss vue2 文件分片上传、秒传、断点续传

springboot整合oss vue2 文件分片上传、秒传、断点续传

一、前端

1. 计算文件的md5值

  前端页面使用的elment-plus的el-upload组件。

  1. <el-upload action="#" :multiple="true" :auto-upload="false" :on-change="handleChange" :show-file-list="false">
  2. <FileButton content="上传文件" type="primary" class="file-button" />
  3. </el-upload>

当上传文件后,会调用handleChange 方法,可以在这里进行文件相关的操作。

  1. //处理文件上传
  2. const handleChange = async (uploadFile) => {
  3. //文件名字
  4. let fileName = uploadFile.name
  5. //文件的大小
  6. const fileSize = uploadFile.size || 0
  7. //当前的文件对象
  8. let fileItem = {}
  9. fileItem.fileName = fileName
  10. fileItem.fileSize = fileSize
  11. fileItem.state = 1 //解码中
  12. fileItem.progress = 0 //进度是0
  13. fileItem.filePid = 102903232
  14. fileItem.fileMd5 = ""
  15. fileItem.uploadSize = 0
  16. fileUploadList.value.addFile(fileItem)
  17. //弹框显示
  18. isVisible.value = true
  19. //获得文件的md5
  20. if (uploadFile.raw) {
  21. await generateMD5OfFile(uploadFile.raw).then(
  22. res => {
  23. fileItem.fileMd5 = res
  24. }
  25. )
  26. }
  27. fileUploadList.value.addMd5(fileItem.fileName, fileItem.fileMd5)
  28. fileUploadList.value.changeFileState(fileItem.fileName, 2)
  29. //分片上传
  30. let chunkTotals = Math.ceil(fileSize / chunkSize);
  31. //分片上传
  32. if (chunkTotals > 0) {
  33. for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) {
  34. //文件最后的end
  35. let end = Math.min(fileSize, start + chunkSize);
  36. // el-mement - plus中,上传的文件就在raw里面
  37. const files = uploadFile.raw?.slice(start, end)
  38. //上传的结果
  39. const result = await uploadFileToServer(files, chunkNumber + 1, chunkTotals, fileName , getCurrentId(), fileItem.fileMd5,userId)
  40. console.log(result.data)
  41. console.log(result.data.data)
  42. if (result.data.data.status === 1) {
  43. // console.log("上传中")
  44. //上传的进度
  45. fileUploadList.value.changeProgress(fileItem.fileName, ((end / fileSize) * 100).toFixed(1))
  46. //修改已经上传完成的文件大小
  47. fileUploadList.value.changeUploadSize(fileItem.fileName, end)
  48. } else if (result.data.data.status === 3) {
  49. // console.log("上传成功!"),这里是弹窗显示的文件上传进度,可以适当修改
  50. fileUploadList.value.changeFileState(fileItem.fileName, 3) //上传完成
  51. fileUploadList.value.changeProgress(fileItem.fileName, 100) // 进度100%
  52. //通过main,进行刷新
  53. $emit("addChangeNum")
  54. return ; //结束
  55. } else {
  56. message("上传失败", 'error')
  57. return; //结束
  58. }
  59. }
  60. }
  61. }

 计算文件的MD5值

  1. //计算文件的md5
  2. function generateMD5OfFile(file) {
  3. return new Promise((resolve, reject) => {
  4. let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, // Read in chunks of 2MB
  5. chunks = Math.ceil(file.size / chunkSize),
  6. currentChunk = 0,
  7. spark = new SparkMD5.ArrayBuffer(),
  8. fileReader = new FileReader();
  9. fileReader.onload = function (e) {
  10. console.log('read chunk nr', currentChunk + 1, 'of', chunks);
  11. spark.append(e.target.result); // Append array buffer
  12. currentChunk++;
  13. if (currentChunk < chunks) {
  14. loadNext();
  15. } else {
  16. resolve(spark.end())
  17. }
  18. };
  19. fileReader.onerror = function () {
  20. reject('MD5 calc error')
  21. };
  22. function loadNext() {
  23. let start = currentChunk * chunkSize,
  24. end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
  25. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  26. }
  27. loadNext();
  28. })
  29. }

2.计算文件切片数量

自定义文件切片大小 

  1. //默认分片大小
  2. const chunkSize = 5 * 1024 * 1024

3.分片上传文件

上传文件到服务器 

  1. // 上传文件到服务器
  2. const uploadFileToServer = async (file, chunkNumber, chunkTotal, fileName,filePid, fileMd5,userId) => {
  3. const form = new FormData();
  4. // 这里的data是文件
  5. form.append("file", file);
  6. form.append("chunkNumber", chunkNumber);
  7. form.append("chunkTotal", chunkTotal);
  8. form.append("fileName", fileName)
  9. form.append("fileMd5", fileMd5)
  10. form.append("filePid", filePid)
  11. form.append("userId", userId)
  12. var result = await axios({
  13. url: env_server_production + '/file/upload',
  14. headers: { 'Content-Type': 'multipart/form-data' },
  15. method: "post",
  16. timeout: 1000000,
  17. data: form
  18. })
  19. return result
  20. }

4.实现相关文件的预览

可以简单的实现对一些文件的预览,比如图片、视频、word、pdf等等。

pdf:

等等

这里使用的是vue-office 

  1. <template>
  2. <div class="preview-body">
  3. <!-- word -->
  4. <vue-office-docx v-if="getFileType() == 1" :src="getFileUrl()" style="height: 400px;" @rendered="renderedHandler"
  5. @error="errorHandler" />
  6. <!-- pdf -->
  7. <vue-office-pdf v-else-if="getFileType() == 2" :src="getFileUrl()" style="height: 400px;"
  8. @rendered="renderedHandler" @error="errorHandler" />
  9. <!-- iamge -->
  10. <div v-else-if="getFileType() == 3">
  11. <el-image :src="getFileUrl()" style="height: 100px; width: 100px;" :zoom-rate="1.2" :max-scale="7"
  12. :min-scale="0.2" :preview-src-list="imageList" :initial-index="4" />
  13. <br>
  14. <el-text style="margin-left: 0px;" link type="primary">点击图片查看详情</el-text>
  15. </div>
  16. <!-- 不支持显示 -->
  17. <div v-else-if="getFileType() == 4">
  18. <br>
  19. 该文件不支持在线浏览,请下载后查看!
  20. </div>
  21. <!-- 视频 -->
  22. <div v-else-if="getFileType() == 5">
  23. <video autoplay width="1200px" height="400px" controls
  24. :src="getFileUrl()"
  25. id="myVideo"
  26. >
  27. </video>
  28. </div>
  29. <!-- 文本显示 -->
  30. <div v-else>
  31. <el-scrollbar height="400px" class="document-preview">
  32. <pre>{{ documentContent }}</pre>
  33. </el-scrollbar>
  34. </div>
  35. </div>
  36. </template>
  37. <script setup>
  38. //引入相关样式
  39. import VueOfficeDocx from '@vue-office/docx'
  40. import VueOfficePdf from '@vue-office/pdf'
  41. import '@vue-office/docx/lib/index.css'
  42. import { ref } from 'vue'
  43. import axios from 'axios';
  44. const props = defineProps(['file'])
  45. const video = document.getElementById("myVideo")
  46. const getFileUrl = () => {
  47. return "http://60.205.141.200:9000/" + props.file.filePath;
  48. }
  49. const getFileType = () => {
  50. let category = props.file.fileCategory
  51. if (category == 18 || category == 19) {
  52. return 1
  53. }
  54. else if (category == 13)
  55. return 2
  56. else if (category == 9 || category == 14 || category == 5) {
  57. imageList.value.push(getFileUrl())
  58. return 3
  59. }
  60. else if (category == 20 || category == 11 || category == 15)
  61. return 4
  62. else if (category == 12) {
  63. //视频
  64. return 5
  65. } else {
  66. //文本
  67. readDocumentContent();
  68. }
  69. }
  70. const readDocumentContent = async () => {
  71. var res = await axios.get(getFileUrl(), {
  72. responseType: 'text',
  73. })
  74. documentContent.value = `\n${res.data}\n`
  75. }
  76. //文件中的内容
  77. const documentContent = ref('')
  78. //图片列表
  79. const imageList = ref([])
  80. const renderedHandler = () => {
  81. console.log("渲染成功")
  82. }
  83. const errorHandler = () => {
  84. console.log("渲染失败")
  85. }
  86. </script>
  87. <style lang="scss" scoped>
  88. .document-preview {
  89. margin-right: 100px;
  90. background-color: #ccc;
  91. width: 1164px;
  92. border: 2px solid #ccc;
  93. height: 400px;
  94. border-radius: 0 0 10px 10px;
  95. text-align: left;
  96. }
  97. pre {
  98. font-family: 'Microsoft YaHei';
  99. }
  100. </style>

二、后端

后端使用minio,minio先接收分片文件,上传完成所有的分片文件后,在合并分片文件,删除中间文件即可。

1.接收分片文件、合并文件。

  1. /**
  2. * 上传文件方法。
  3. * 该方法负责检查文件是否已存在,如果存在,则返回已存在标志;如果不存在且是完整文件,则上传文件到MinIO并保存文件信息到数据库。
  4. *
  5. * @param fileVO 文件相关信息VO,包含文件本身、MD5、文件名等。
  6. * @return 如果文件已存在,返回秒传状态码;如果文件上传完成,返回上传完成状态码;否则返回null。
  7. * @throws GeneralException 如果文件为空,抛出通用异常。
  8. */
  9. @Override
  10. @Transactional(rollbackFor = Exception.class) //所有的操作都在一个事务里面。
  11. public HashMap<Object, Object> uploadFile(FileVO fileVO) {
  12. if(fileVO.getFile().isEmpty())
  13. throw new GeneralException("文件上传异常");
  14. FileInfo insertItem = new FileInfo();
  15. Date now = new Date();
  16. HashMap<Object, Object> map = new HashMap<>();
  17. //第一片文件
  18. if(fileVO.getChunkNumber() == 1){
  19. //先去数据库看看有没有这个文件
  20. QueryWrapper<FileInfo> queryWrapper = new QueryWrapper<>();
  21. queryWrapper.eq("file_md5", fileVO.getFileMd5());
  22. //通过Md5查询,别人是不是已经传过这个文件了(文件名不影响文件的MD5值)。
  23. List<FileInfo> fileInfoList = fileInfoMapper.selectList(queryWrapper);
  24. FileInfo fileInfo = null;
  25. if(fileInfoList.size() > 0){
  26. fileInfo = fileInfoList.get(0);
  27. }
  28. //别人已经上传过这个文件了,直接秒传
  29. if(fileInfo != null){
  30. log.info("服务器中有相同的文件,直接秒传");
  31. //说明minIO中有对应的文件
  32. insertItem.setUserId(fileVO.getUserId());
  33. insertItem.setFileMd5(fileVO.getFileMd5());
  34. insertItem.setFileName(fileInfo.getFileName());
  35. insertItem.setFileCategory(fileInfo.getFileCategory());
  36. insertItem.setFileId(StringUtil.getRandomString(10));
  37. insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
  38. insertItem.setFilePid(fileVO.getFilePid());
  39. insertItem.setFilePath(fileInfo.getFilePath());
  40. insertItem.setCreateTime(now);
  41. insertItem.setFileSize(fileInfo.getFileSize());
  42. insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
  43. fileInfoMapper.insert(insertItem);
  44. System.err.println(insertItem);
  45. map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
  46. map.put("fileId",insertItem.getFileId());
  47. return map;
  48. }
  49. //插入 一个切片
  50. redisUtil.set(fileVO.getFileMd5(),0);
  51. }
  52. if(Integer.parseInt(redisUtil.get(fileVO.getFileMd5()).toString()) >= fileVO.getChunkNumber()){
  53. //说明这片文件已经上传过了。
  54. map.put("status",UploadStatus.UPLOADING.getStatus());
  55. return map;
  56. }
  57. //只有一段,直接放到服务器就行
  58. if(fileVO.getChunkTotal() == 1){
  59. int lastDotIndex = fileVO.getFileName().lastIndexOf(".");
  60. String type = fileVO.getFileName().substring(lastDotIndex + 1);
  61. String url = minioUtils.uploadFile(MessageConstant.MINIO_BUCKET,fileVO.getFileName(), fileVO.getFile());
  62. insertItem.setUserId(fileVO.getUserId());
  63. insertItem.setFileMd5(fileVO.getFileMd5());
  64. insertItem.setFileName(fileVO.getFileName());
  65. insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());
  66. insertItem.setFileId(StringUtil.getRandomString(10));
  67. insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
  68. insertItem.setFilePid(fileVO.getFilePid());
  69. insertItem.setFilePath(url);
  70. insertItem.setCreateTime(now);
  71. insertItem.setFileSize(fileVO.getFile().getSize());
  72. insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
  73. fileInfoMapper.insert(insertItem);
  74. //删除redis中的切片上传信息
  75. redisUtil.del(fileVO.getFileMd5());
  76. map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
  77. map.put("fileId",insertItem.getFileId());
  78. return map;
  79. }
  80. log.info("分片上传====> md5 :{} ,=====> index :{}",fileVO.getFileMd5(),fileVO.getChunkNumber());
  81. //不止一片,继续上传
  82. //放切片文件的目录是 文件的userId + md5值,这个是唯一的。
  83. String objectName = fileVO.getUserId() + fileVO.getFileMd5() ;
  84. try {
  85. minioUtils.putChunkObject(fileVO.getFile().getInputStream(), MessageConstant.MINIO_BUCKET, objectName + "/" + fileVO.getChunkNumber());
  86. } catch (IOException e) {
  87. throw new GeneralException("文件上传异常!");
  88. }
  89. //最后一片,进行合并
  90. if(Objects.equals(fileVO.getChunkNumber(), fileVO.getChunkTotal())){
  91. //获得文件类型
  92. int lastDotIndex = fileVO.getFileName().lastIndexOf(".");
  93. String type = fileVO.getFileName().substring(lastDotIndex + 1);
  94. //objectName : userId+md5
  95. String filePath = minioUtils.composeObject(MessageConstant.MINIO_BUCKET,MessageConstant.MINIO_BUCKET,objectName, type);
  96. insertItem.setUserId(fileVO.getUserId());
  97. insertItem.setFileMd5(fileVO.getFileMd5());
  98. insertItem.setFileName(fileVO.getFileName());
  99. insertItem.setFileCategory(FileCategoryEnums.getByCode(type).getCategory());
  100. insertItem.setFileId(StringUtil.getRandomString(10));
  101. insertItem.setDelFlag(FileDelFlagEnums.USING.getFlag());
  102. insertItem.setFilePid(fileVO.getFilePid());
  103. insertItem.setFilePath(filePath);
  104. insertItem.setCreateTime(now);
  105. Long fileSize = MessageConstant.DEFAULT_CHUNK_SIZE * (fileVO.getChunkTotal() - 1) + fileVO.getFile().getSize();
  106. insertItem.setFileSize(fileSize);
  107. insertItem.setState(UploadStatus.UPLOAD_FINISH.getStatus());
  108. //插入一条数据
  109. System.out.println(fileInfoMapper.insert(insertItem));
  110. //删除minio中的临时文件目录
  111. System.out.println(minioUtils.deleteFolder(MessageConstant.MINIO_BUCKET, objectName));
  112. //删除redis中的切片上传信息
  113. redisUtil.del(fileVO.getFileMd5());
  114. map.put("status",UploadStatus.UPLOAD_FINISH.getStatus());
  115. map.put("fileId",insertItem.getFileId());
  116. return map;
  117. }
  118. //更新redis中的切片上传信息
  119. redisUtil.incrby(fileVO.getFileMd5(),1);
  120. //上传中
  121. map.put("status",UploadStatus.UPLOADING.getStatus());
  122. return map;
  123. }

如何做到秒传?

一个文件有个不重复的md5值,所谓的秒传其实就是你要上传的文件,别人已经上传过了,minio中已经有这个文件了,再解析完文件的md5值之后,后端发现数据库中md5存在了,所以就不用上传文件了,直接在数据库中创建一个信息即可,也就实现了秒传。

如何做到断点传递?

传统传递过程是一整个文件上传,如果中断了下次传的时候,需要重新上传;断点传递,每次传递的时候,可以把分片信息放到redis中,同时下一次传分片的时候,判断一下,redis中时候已经有了这个分片,如果有就不用上传此分片文件,即断点传递。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/753161
推荐阅读
相关标签
  

闽ICP备14008679号