当前位置:   article > 正文

VUE+SPRINGBOOT实现断点续传,分片上传大文件_spring boot + vue 大文件上传

spring boot + vue 大文件上传

前端代码:

  1. <!--上传附件弹出框 -->
  2. <el-dialog v-dialogDrag title="文件上传" center v-model="uploadVisible" width="60%" @close="handlerClose" destroy-on-close>
  3. <UploadBigFile class="uploadSlot" @closeFileDialog="closeFileDialog"></UploadBigFile>
  4. </el-dialog>

弹出框代码:

  1. <template>
  2. <!-- 上传器 -->
  3. <uploader
  4. ref="uploader"
  5. :options="options"
  6. :autoStart=false
  7. :file-status-text="fileStatusText"
  8. @file-added="onFileAdded"
  9. @file-success="onFileSuccess"
  10. @file-progress="onFileProgress"
  11. @file-error="onFileError">
  12. <uploader-unsupport></uploader-unsupport>
  13. <uploader-drop>
  14. <div>
  15. <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件<i class="el-icon-upload el-icon--right"></i></uploader-btn>
  16. </div>
  17. </uploader-drop>
  18. <uploader-list></uploader-list>
  19. </uploader>
  20. </template>
  21. <script>
  22. import {ACCEPT_CONFIG} from '../../../assets/js/config';
  23. import SparkMD5 from 'spark-md5';
  24. import {mergeFile} from "@/api/tool/uploadFile";
  25. import { getToken } from "@/utils/auth";
  26. export default {
  27. data () {
  28. return {
  29. options: {
  30. //目标上传 URL,默认POST
  31. target: process.env.VITE_APP_BASE_API +"/vm/chunk",
  32. //分块大小(单位:字节) 单个分片暂定200M
  33. chunkSize: '204800000',
  34. //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
  35. fileParameterName: 'upfile',
  36. //失败后最多自动重试上传次数
  37. maxChunkRetries: 3,
  38. //是否开启服务器分片校验,对应GET类型同名的target URL
  39. testChunks: true,
  40. headers: { Authorization: "Bearer " + getToken() },
  41. /*
  42. 服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
  43. reponse码是successStatuses码时,才会进入该方法
  44. reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
  45. reponse码是其他状态码,不会进入该方法,正常走标准上传
  46. checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
  47. */
  48. checkChunkUploadedByResponse: function (chunk, response_msg) {
  49. // console.log("response_msg的值",response_msg)
  50. let objMessage = JSON.parse(response_msg);
  51. if (objMessage.skipUpload) {
  52. return true;
  53. }
  54. return (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
  55. }
  56. },
  57. attrs: {
  58. //上传文件的类型
  59. accept: ACCEPT_CONFIG.getAll()
  60. },
  61. fileStatusText: {
  62. success: '上传成功',
  63. error: '上传失败',
  64. uploading: '上传中',
  65. paused: '暂停',
  66. waiting: '等待上传'
  67. },
  68. }
  69. },
  70. methods: {
  71. onFileAdded(file) {
  72. this.computeMD5(file);
  73. },
  74. /*
  75. 第一个参数 rootFile 就是成功上传的文件所属的根 Uploader.File 对象,它应该包含或者等于成功上传文件;
  76. 第二个参数 file 就是当前成功的 Uploader.File 对象本身;
  77. 第三个参数就是 message 就是服务端响应内容,永远都是字符串;
  78. 第四个参数 chunk 就是 Uploader.Chunk 实例,它就是该文件的最后一个块实例,如果你想得到请求响应码的话,chunk.xhr.status就是
  79. */
  80. onFileSuccess(rootFile, file, response, chunk) {
  81. //refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
  82. file.refProjectId = "ommb";
  83. this.$emit('closeFileDialog');
  84. mergeFile(file).then( responseData=> {
  85. if(responseData === "Failure"){
  86. console.log("合并操作未成功");
  87. }
  88. }).catch(function (error){
  89. console.log("合并后捕获的未知异常:"+error);
  90. });
  91. },
  92. onFileProgress(rootFile, file, chunk) {
  93. console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
  94. },
  95. onFileError(rootFile, file, response, chunk) {
  96. console.log('上传完成后异常信息:'+response);
  97. this.$message({
  98. message: response,
  99. type: 'error'
  100. })
  101. },
  102. /**
  103. * 计算md5,实现断点续传及秒传
  104. * @param file
  105. */
  106. computeMD5(file) {
  107. file.pause();
  108. //单个文件的大小限制2G
  109. let fileSizeLimit = 2 * 1024 * 1024 * 1024;
  110. // console.log("文件大小:"+file.size);
  111. // console.log("限制大小:"+fileSizeLimit);
  112. if(file.size > fileSizeLimit){
  113. this.$message({
  114. showClose: true,
  115. message: '文件大小不能超过2G'
  116. });
  117. file.cancel();
  118. }
  119. let fileReader = new FileReader();
  120. let time = new Date().getTime();
  121. let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  122. let currentChunk = 0;
  123. const chunkSize = 10 * 1024 * 1000;
  124. let chunks = Math.ceil(file.size / chunkSize);
  125. let spark = new SparkMD5.ArrayBuffer();
  126. //由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
  127. let chunkNumberMD5 = 1;
  128. loadNext();
  129. fileReader.onload = (e => {
  130. spark.append(e.target.result);
  131. if (currentChunk < chunkNumberMD5) {
  132. currentChunk++;
  133. loadNext();
  134. // 实时展示MD5的计算进度
  135. this.$nextTick(() => {
  136. $(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%')
  137. })
  138. } else {
  139. let md5 = spark.end();
  140. file.uniqueIdentifier = md5;
  141. file.resume();
  142. // console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
  143. }
  144. });
  145. fileReader.onerror = function () {
  146. this.error(`文件${file.name}读取出错,请检查该文件`)
  147. file.cancel();
  148. };
  149. function loadNext() {
  150. let start = currentChunk * chunkSize;
  151. let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
  152. fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
  153. currentChunk++;
  154. // console.log("计算第"+currentChunk+"块");
  155. }
  156. },
  157. close() {
  158. this.uploader.cancel();
  159. },
  160. /**
  161. * 新增的自定义的状态: 'md5''transcoding''failed'
  162. * @param id
  163. * @param status
  164. */
  165. statusSet(id, status) {
  166. let statusMap = {
  167. md5: {
  168. text: '校验MD5',
  169. bgc: '#fff'
  170. },
  171. merging: {
  172. text: '合并中',
  173. bgc: '#e2eeff'
  174. },
  175. transcoding: {
  176. text: '转码中',
  177. bgc: '#e2eeff'
  178. },
  179. failed: {
  180. text: '上传失败',
  181. bgc: '#e2eeff'
  182. }
  183. }
  184. this.$nextTick(() => {
  185. $(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
  186. 'position': 'absolute',
  187. 'top': '0',
  188. 'left': '0',
  189. 'right': '0',
  190. 'bottom': '0',
  191. 'zIndex': '1',
  192. 'line-height': 'initial',
  193. 'backgroundColor': statusMap[status].bgc
  194. }).text(statusMap[status].text);
  195. })
  196. },
  197. statusRemove(id) {
  198. this.$nextTick(() => {
  199. $(`.myStatus_${id}`).remove();
  200. })
  201. },
  202. error(msg) {
  203. this.$notify({
  204. title: '错误',
  205. message: msg,
  206. type: 'error',
  207. duration: 2000
  208. })
  209. }
  210. }
  211. }
  212. </script>
  213. <style scoped>
  214. .handle-box {
  215. margin-bottom: 20px;
  216. }
  217. .handle-select {
  218. width: 120px;
  219. }
  220. .handle-input {
  221. width: 300px;
  222. display: inline-block;
  223. }
  224. .table {
  225. width: 1200px;
  226. font-size: 14px;
  227. }
  228. .red {
  229. color: #ff0000;
  230. }
  231. .mr10 {
  232. margin-right: 10px;
  233. }
  234. .table-td-thumb {
  235. display: block;
  236. margin: auto;
  237. width: 40px;
  238. height: 40px;
  239. }
  240. .uploadSlot {
  241. margin: -10px 10px 10px 30px;
  242. }
  243. </style>

后台代码:

  1. @Service
  2. public class SysChunkServiceImpl implements ISysChunkService {
  3. private static final Logger logger = LoggerFactory.getLogger(SysChunkServiceImpl.class);
  4. /**
  5. * 上传文件存储在本地的根路径
  6. */
  7. @Value("${file.path}")
  8. private String localFilePath;
  9. @Autowired
  10. private SysChunkMapper sysChunkMapper;
  11. @Autowired
  12. private VersionPackageMapper versionPackageMapper;
  13. /**
  14. * 查询版本管理
  15. *
  16. * @param id 版本管理主键
  17. * @return 版本管理
  18. */
  19. @Override
  20. public SysChunk selectSysChunkById(String id) {
  21. return sysChunkMapper.selectSysChunkById(id);
  22. }
  23. /**
  24. * 查询版本管理列表
  25. *
  26. * @param sysChunk 版本管理
  27. * @return 版本管理
  28. */
  29. @Override
  30. public List<SysChunk> selectSysChunkList(SysChunk sysChunk) {
  31. return sysChunkMapper.selectSysChunkList(sysChunk);
  32. }
  33. /**
  34. * 新增版本管理
  35. *
  36. * @param sysChunk 版本管理
  37. * @return 结果
  38. */
  39. @Override
  40. public int insertSysChunk(SysChunk sysChunk) {
  41. sysChunk.setId(SnowflakeIdWorker.getUUID());
  42. return sysChunkMapper.insertSysChunk(sysChunk);
  43. }
  44. /**
  45. * 修改版本管理
  46. *
  47. * @param sysChunk 版本管理
  48. * @return 结果
  49. */
  50. @Override
  51. public int updateSysChunk(SysChunk sysChunk) {
  52. return sysChunkMapper.updateSysChunk(sysChunk);
  53. }
  54. /**
  55. * 批量删除版本管理
  56. *
  57. * @param ids 需要删除的版本管理主键
  58. * @return 结果
  59. */
  60. @Override
  61. public int deleteSysChunkByIds(String[] ids) {
  62. return sysChunkMapper.deleteSysChunkByIds(ids);
  63. }
  64. /**
  65. * 删除分片
  66. *
  67. * @param id 版本管理主键
  68. * @return 结果
  69. */
  70. @Override
  71. public int deleteSysChunkById(String id) {
  72. return sysChunkMapper.deleteSysChunkById(id);
  73. }
  74. @Override
  75. public int deleteSysChunkByIdentifier(String identifier) {
  76. return sysChunkMapper.deleteSysChunkByIdentifier(identifier);
  77. }
  78. /**
  79. * 删除版本管理信息
  80. *
  81. * @param versionPackage
  82. * @return
  83. */
  84. @Transactional(rollbackFor = Exception.class)
  85. @Override
  86. public int deleteFile(VersionPackage versionPackage) {
  87. //删除版本
  88. versionPackageMapper.deleteVersionPackageByVersionId(versionPackage.getVersionId());
  89. //删除版本分片
  90. sysChunkMapper.deleteSysChunkByIdentifier(versionPackage.getIdentifier());
  91. //删除版本存放的目录
  92. FileUtils.delFile(versionPackage.getLocation());
  93. return 1;
  94. }
  95. @Override
  96. public String uploadChunk(SysChunk chunk) {
  97. String apiRlt = VmConstants.SUCCESS_CODE;
  98. MultipartFile file = chunk.getUpfile();
  99. logger.info("file originName: {}, chunkNumber: {}", file.getOriginalFilename(), chunk.getChunkNumber());
  100. try {
  101. byte[] bytes = file.getBytes();
  102. Path path = Paths.get(generatePath(localFilePath, chunk));
  103. //文件写入指定路径
  104. Files.write(path, bytes);
  105. if (insertSysChunk(chunk) < 0) {
  106. apiRlt = VmConstants.FAIL_CODE;
  107. }
  108. } catch (IOException e) {
  109. logger.error("uploadChunk IOException" , e);
  110. apiRlt = VmConstants.FAIL_CODE;
  111. }
  112. return apiRlt;
  113. }
  114. @Override
  115. public UploadResult checkChunk(SysChunk chunk) {
  116. UploadResult ur = new UploadResult();
  117. //完整文件路径
  118. String file = localFilePath + "/" + chunk.getIdentifier() + "/" + chunk.getFilename();
  119. //先判断整个文件是否已经上传过了,如果是,则告诉前端跳过上传,实现秒传
  120. if (fileExists(file)) {
  121. ur.setSkipUpload(true);
  122. ur.setLocation(file);
  123. ur.setMessage("完整文件已存在,直接跳过上传,实现秒传");
  124. return ur;
  125. }
  126. //如果完整文件不存在,则去数据库判断当前哪些文件块已经上传过了,把结果告诉前端,跳过这些文件块的上传,实现断点续传
  127. ArrayList<Integer> list = sysChunkMapper.selectChunkNumbers(chunk);
  128. if (CollectionUtils.isNotEmpty(list)) {
  129. ur.setSkipUpload(false);
  130. ur.setUploadedChunks(list);
  131. ur.setMessage("部分文件块已存在,继续上传剩余文件块,实现断点续传");
  132. return ur;
  133. }
  134. return ur;
  135. }
  136. /**
  137. * 生成上传后的文件路径
  138. *
  139. * @param uploadFolder 本地路径
  140. * @param chunk 分片信息
  141. * @return
  142. */
  143. public String generatePath(String uploadFolder, SysChunk chunk) {
  144. StringBuilder sb = new StringBuilder();
  145. sb.append(uploadFolder).append("/").append(chunk.getIdentifier());
  146. //把字符串拼接为Path
  147. Path path = Paths.get(sb.toString());
  148. //判断uploadFolder/identifier 路径是否存在,不存在则创建
  149. if (!Files.isWritable(path)) {
  150. logger.info("path not exist,create path: {}", sb.toString());
  151. try {
  152. Files.createDirectories(path);
  153. } catch (IOException e) {
  154. logger.error(e.getMessage(), e);
  155. }
  156. }
  157. return sb.append("/")
  158. .append(chunk.getFilename())
  159. .append("-")
  160. .append(chunk.getChunkNumber()).toString();
  161. }
  162. /**
  163. * 根据文件的全路径名判断文件是否存在
  164. *
  165. * @param file
  166. * @return
  167. */
  168. public boolean fileExists(String file) {
  169. Path path = Paths.get(file);
  170. boolean fileExists = Files.exists(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS});
  171. return fileExists;
  172. }
  173. @Override
  174. public String mergeFile(SysFileVO fileInfoVO) {
  175. String rlt = VmConstants.FAIL;
  176. //前端组件参数转换为model对象
  177. VersionPackage fileInfo = buildVersionPackage(fileInfoVO);
  178. LoginUser user = SecurityUtils.getLoginUser();
  179. Date now = DateUtils.getNowDate();
  180. fileInfo.setCreateBy(user.getUsername());
  181. fileInfo.setCreateTime(now);
  182. fileInfo.setUpdateBy(user.getUsername());
  183. fileInfo.setUpdateTime(now);
  184. String filename = fileInfoVO.getName();
  185. //进行文件的合并操作
  186. String file = localFilePath + "/" + fileInfo.getIdentifier() + "/" + filename;
  187. String folder = localFilePath + "/" + fileInfo.getIdentifier();
  188. String fileSuccess = merge(file, folder, filename);
  189. fileInfo.setLocation(file);
  190. //文件合并成功后,保存记录至数据库
  191. if (VmConstants.SUCCESS_CODE.equals(fileSuccess)) {
  192. if (versionPackageMapper.insertVersionPackage(fileInfo) > 0) {
  193. rlt = VmConstants.SUCCESS;
  194. }
  195. }
  196. //如果已经存在,则判断是否同一个项目,同一个项目的不用新增记录,否则新增
  197. if (VmConstants.EXIST_CODE.equals(fileSuccess)) {
  198. List<VersionPackage> tfList = versionPackageMapper.selectVersionPackageList(fileInfo);
  199. if (tfList != null) {
  200. if (tfList.size() == 0 || (tfList.size() > 0 && !fileInfo.getSuitScope().equals(tfList.get(0).getSuitScope()))) {
  201. if (versionPackageMapper.insertVersionPackage(fileInfo) > 0) {
  202. rlt = VmConstants.SUCCESS;
  203. }
  204. }
  205. }
  206. }
  207. return rlt;
  208. }
  209. /**
  210. * 根据上传的文件构造版本包对象
  211. * @param fileInfoVO
  212. * @return
  213. */
  214. private VersionPackage buildVersionPackage(SysFileVO fileInfoVO) {
  215. String filename = fileInfoVO.getName();
  216. String id = SnowflakeIdWorker.getUUID();
  217. VersionPackage fileInfo = new VersionPackage();
  218. fileInfo.setVersionId(id);
  219. fileInfo.setGenerateDate(new Date());
  220. fileInfo.setVersionPackage(filename);
  221. fileInfo.setVersionType("1");
  222. fileInfo.setVersionRadio("1");
  223. //第一个下划线到最后一个点
  224. int startIndex = filename.indexOf("_");
  225. int endIndex = filename.lastIndexOf(".");
  226. String versionNo = filename.substring(startIndex+1 , endIndex);
  227. fileInfo.setVersionNo(versionNo);
  228. fileInfo.setIdentifier(fileInfoVO.getUniqueIdentifier());
  229. fileInfo.setTotalSize(fileInfoVO.getSize());
  230. fileInfo.setSuitScope(fileInfoVO.getRefProjectId());
  231. fileInfo.setStatus("0");
  232. return fileInfo;
  233. }
  234. /**
  235. * 文件合并
  236. *
  237. * @param file
  238. * @param folder
  239. * @param filename
  240. * @return
  241. */
  242. public String merge(String file, String folder, String filename) {
  243. //默认合并成功
  244. String rlt = VmConstants.SUCCESS_CODE;
  245. try {
  246. //先判断文件是否存在
  247. if (fileExists(file)) {
  248. //文件已存在
  249. return VmConstants.EXIST_CODE;
  250. }
  251. //不存在的话,进行合并
  252. Files.createFile(Paths.get(file));
  253. Files.list(Paths.get(folder))
  254. .filter(path -> !path.getFileName().toString().equals(filename))
  255. .sorted((o1, o2) -> {
  256. String p1 = o1.getFileName().toString();
  257. String p2 = o2.getFileName().toString();
  258. int i1 = p1.lastIndexOf("-");
  259. int i2 = p2.lastIndexOf("-");
  260. return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
  261. })
  262. .forEach(path -> {
  263. try {
  264. //以追加的形式写入文件
  265. Files.write(Paths.get(file), Files.readAllBytes(path), StandardOpenOption.APPEND);
  266. //合并后删除该块
  267. Files.delete(path);
  268. } catch (IOException e) {
  269. logger.error(e.getMessage(), e);
  270. }
  271. });
  272. } catch (IOException e) {
  273. logger.error(e.getMessage(), e);
  274. //合并失败
  275. rlt = VmConstants.FAIL_CODE;
  276. }
  277. return rlt;
  278. }
  279. }

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

闽ICP备14008679号