Java大文件上传、分片上传、多文件上传、断点续传、上传文件minio、分片上 ...

打印 上一主题 下一主题

主题 906|帖子 906|积分 2718


  • 上传说明

           文件上传花样百出,根据差别场景使用差别方案进行实现尤为必要。通常开发过程中,文件较小,直接将文件转化为字节流上传到服务器,但是文件较大时,用普通的方法上传,显然效果不是很好,当文件上传一半中断再次上传时,发现需要重新开始,这种体验不是很爽,下面介绍几种好一点儿的上传方式。
这里讲讲如何在Spring boot 编写上传代码,如有题目可以在下留言,我并在文章末尾附上Java上传源码供各人下载。



    • 分片上传

      分片上传,就是将所要上传的文件,按照肯定的大小,将整个文件分
隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再
由服务端对全部上传的文件进行汇总整合成原始的文件。



    • 断点续传

          断点续传是在下载/上传时,将下载/上传任务(一个文件或一个压缩
包)人为的划分为几个部门,每一个部门采用一个线程进行上传/下载,
如果碰到网络故障,可以从已经上传/下载的部门开始继续上传/下载
未完成的部门,而没有必要从头开始上传/下载。

  • Redis启动安装
Redis安装包分为 Windows 版和 Linux 版:
Windows版下载地址:https://github.com/microsoftarchive/redis/releases
Linux版下载地址: https://download.redis.io/releases/
我当前使用的Windows版本:
 

 

  • minio下载启动
windows版本可以参考我之前的文档:window10安装minio_minio windows安装-CSDN博客
启动会提示:

 
以上是密码设置题目需要修改如下:
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
启动成功后会输出相应地址



  • 上传后端Java代码
   后端采用Spring boot项目布局,主要代码如下:
  1.   1   /**
  2.   2      * 单文件上传
  3.   3      * 直接将传入的文件通过io流形式直接写入(服务器)指定路径下
  4.   4      *
  5.   5      * @param file 上传的文件
  6.   6      * @return
  7.   7      */
  8.   8     @Override
  9.   9     public ResultEntity<Boolean> singleFileUpload(MultipartFile file) {
  10. 10         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  11. 11         String filePath = System.getProperty("user.dir") + "\\file\";
  12. 12         File dir = new File(filePath);
  13. 13         if (!dir.exists()) dir.mkdir();
  14. 14  
  15. 15         if (file == null) {
  16. 16             return ResultEntity.error(false, "上传文件为空!");
  17. 17         }
  18. 18         InputStream fileInputStream = null;
  19. 19         FileOutputStream fileOutputStream = null;
  20. 20         try {
  21. 21             String filename = file.getOriginalFilename();
  22. 22             fileOutputStream = new FileOutputStream(filePath + filename);
  23. 23             fileInputStream = file.getInputStream();
  24. 24  
  25. 25             byte[] buf = new byte[1024 * 8];
  26. 26             int length;
  27. 27             while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
  28. 28                 fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
  29. 29             }
  30. 30             log.info("单文件上传完成!文件路径:{},文件名:{},文件大小:{}", filePath, filename, file.getSize());
  31. 31             return ResultEntity.success(true, "单文件上传完成!");
  32. 32         } catch (IOException e) {
  33. 33             return ResultEntity.error(true, "单文件上传失败!");
  34. 34         } finally {
  35. 35             try {
  36. 36                 if (fileOutputStream != null) {
  37. 37                     fileOutputStream.close();
  38. 38                     fileOutputStream.flush();
  39. 39                 }
  40. 40                 if (fileInputStream != null) {
  41. 41                     fileInputStream.close();
  42. 42                 }
  43. 43             } catch (Exception e) {
  44. 44                 e.printStackTrace();
  45. 45             }
  46. 46         }
  47. 47     }
  48. 48  
  49. 49     /**
  50. 50      * 多文件上传
  51. 51      * 直接将传入的多个文件通过io流形式直接写入(服务器)指定路径下
  52. 52      * 写入指定路径下是通过多线程进行文件写入的,文件写入线程执行功能就和上面单文件写入是一样的
  53. 53      *
  54. 54      * @param files 上传的所有文件
  55. 55      * @return
  56. 56      */
  57. 57     @Override
  58. 58     public ResultEntity<Boolean> multipleFileUpload(MultipartFile[] files) {
  59. 59         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  60. 60         String filePath = System.getProperty("user.dir") + "\\file\";
  61. 61         File dir = new File(filePath);
  62. 62         if (!dir.exists()) dir.mkdir();
  63. 63  
  64. 64         if (files.length == 0) {
  65. 65             return ResultEntity.error(false, "上传文件为空!");
  66. 66         }
  67. 67         ArrayList<String> uploadFiles = new ArrayList<>();
  68. 68         try {
  69. 69  
  70. 70             ArrayList<Future<String>> futures = new ArrayList<>();
  71. 71             //使用多线程来完成对每个文件的写入
  72. 72             for (MultipartFile file : files) {
  73. 73                 futures.add(partMergeTask.submit(new MultipleFileTaskExecutor(filePath, file)));
  74. 74             }
  75. 75  
  76. 76             //这里主要用于监听各个文件写入线程是否执行结束
  77. 77             int count = 0;
  78. 78             while (count != futures.size()) {
  79. 79                 for (Future<String> future : futures) {
  80. 80                     if (future.isDone()) {
  81. 81                         uploadFiles.add(future.get());
  82. 82                         count++;
  83. 83                     }
  84. 84                 }
  85. 85                 Thread.sleep(1);
  86. 86             }
  87. 87             log.info("多文件上传完成!文件路径:{},文件信息:{}", filePath, uploadFiles);
  88. 88             return ResultEntity.success(true, "多文件上传完成!");
  89. 89         } catch (Exception e) {
  90. 90             log.error("多文件分片上传失败!", e);
  91. 91             return ResultEntity.error(true, "多文件上传失败!");
  92. 92         }
  93. 93  
  94. 94     }
  95. 95  
  96. 96     /**
  97. 97      * 单文件分片上传
  98. 98      * 直接将传入的文件分片通过io流形式写入(服务器)指定临时路径下
  99. 99      * 然后判断是否分片都上传完成,如果所有分片都上传完成的话,就把临时路径下的分片文件通过流形式读入合并并从新写入到(服务器)指定文件路径下
  100. 100      * 最后删除临时文件和临时文件夹,临时文件夹是通过文件的uuid进行命名的
  101. 101      *
  102. 102      * @param filePart  分片文件
  103. 103      * @param partIndex 当前分片值
  104. 104      * @param partNum   所有分片数
  105. 105      * @param fileName  当前文件名称
  106. 106      * @param fileUid   当前文件uuid
  107. 107      * @return
  108. 108      */
  109. 109     @Override
  110. 110     public ResultEntity<Boolean> singleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) {
  111. 111         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  112. 112         String filePath = System.getProperty("user.dir") + "\\file\";//文件存放路径
  113. 113         String tempPath = filePath + "temp\" + fileUid;//临时文件存放路径
  114. 114         File dir = new File(tempPath);
  115. 115         if (!dir.exists()) dir.mkdirs();
  116. 116  
  117. 117         //生成一个临时文件名
  118. 118         String tempFileNamePath = tempPath + "\" + fileName + "_" + partIndex + ".part";
  119. 119         try {
  120. 120             //将分片存储到临时文件夹中
  121. 121             filePart.transferTo(new File(tempFileNamePath));
  122. 122  
  123. 123             File tempDir = new File(tempPath);
  124. 124             File[] tempFiles = tempDir.listFiles();
  125. 125  
  126. 126             one:
  127. 127             if (partNum.equals(Objects.requireNonNull(tempFiles).length)) {
  128. 128                 //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
  129. 129                 if (isMergePart.get(fileUid) != null) {
  130. 130                     break one;
  131. 131                 }
  132. 132                 isMergePart.put(fileUid, tempFiles.length);
  133. 133                 System.out.println("所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);
  134. 134  
  135. 135                 FileOutputStream fileOutputStream = new FileOutputStream(filePath + fileName);
  136. 136                 //这里如果分片很多的情况下,可以采用多线程来执行
  137. 137                 for (int i = 0; i < partNum; i++) {
  138. 138                     //读取分片数据,进行分片合并
  139. 139                     FileInputStream fileInputStream = new FileInputStream(tempPath + "\" + fileName + "_" + i + ".part");
  140. 140                     byte[] buf = new byte[1024 * 8];//8MB
  141. 141                     int length;
  142. 142                     while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
  143. 143                         fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
  144. 144                     }
  145. 145                     fileInputStream.close();
  146. 146                 }
  147. 147                 fileOutputStream.flush();
  148. 148                 fileOutputStream.close();
  149. 149  
  150. 150                 // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
  151. 151                 for (int i = 0; i < partNum; i++) {
  152. 152                     boolean delete = new File(tempPath + "\" + fileName + "_" + i + ".part").delete();
  153. 153                     File file = new File(tempPath + "\" + fileName + "_" + i + ".part");
  154. 154                 }
  155. 155                 //在删除对应的临时文件夹
  156. 156                 if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
  157. 157                     tempDir.delete();
  158. 158                 }
  159. 159                 isMergePart.remove(fileUid);
  160. 160             }
  161. 161  
  162. 162         } catch (Exception e) {
  163. 163             log.error("单文件分片上传失败!", e);
  164. 164             return ResultEntity.error(false, "单文件分片上传失败");
  165. 165         }
  166. 166         //通过返回成功的分片值,来验证分片是否有丢失
  167. 167         return ResultEntity.success(true, partIndex.toString());
  168. 168     }
  169. 169  
  170. 170     /**
  171. 171      * 多文件分片上传
  172. 172      * 先将所有文件分片读入到(服务器)指定临时路径下,每个文件的分片文件的临时文件夹都是已文件的uuid进行命名的
  173. 173      * 然后判断对已经上传所有分片的文件进行合并,此处是通过多线程对每一个文件的分片文件进行合并的
  174. 174      * 最后对已经合并完成的分片临时文件和文件夹进行删除
  175. 175      *
  176. 176      * @param filePart  分片文件
  177. 177      * @param partIndex 当前分片值
  178. 178      * @param partNum   总分片数
  179. 179      * @param fileName  当前文件名称
  180. 180      * @param fileUid   当前文件uuid
  181. 181      * @return
  182. 182      */
  183. 183     @Override
  184. 184     public ResultEntity<String> multipleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) {
  185. 185         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  186. 186         String filePath = System.getProperty("user.dir") + "\\file\";//文件存放路径
  187. 187         String tempPath = filePath + "temp\" + fileUid;//临时文件存放路径
  188. 188         File dir = new File(tempPath);
  189. 189         if (!dir.exists()) dir.mkdirs();
  190. 190         //生成一个临时文件名
  191. 191         String tempFileNamePath = tempPath + "\" + fileName + "_" + partIndex + ".part";
  192. 192         try {
  193. 193             filePart.transferTo(new File(tempFileNamePath));
  194. 194  
  195. 195             File tempDir = new File(tempPath);
  196. 196             File[] tempFiles = tempDir.listFiles();
  197. 197             //如果临时文件夹中分片数量和实际分片数量一致的时候,就需要进行分片合并
  198. 198             one:
  199. 199             if (partNum.equals(tempFiles.length)) {
  200. 200                 //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
  201. 201                 if (isMergePart.get(fileUid) != null) {
  202. 202                     break one;
  203. 203                 }
  204. 204                 isMergePart.put(fileUid, tempFiles.length);
  205. 205                 System.out.println(fileName + ":所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);
  206. 206  
  207. 207                 //使用多线程来完成对每个文件的合并
  208. 208                 Future<Integer> submit = partMergeTask.submit(new PartMergeTaskExecutor(filePath, tempPath, fileName, partNum));
  209. 209                 System.out.println("上传文件名:" + fileName + "; 总大小:" + submit.get());
  210. 210                 isMergePart.remove(fileUid);
  211. 211             }
  212. 212         } catch (Exception e) {
  213. 213             log.error("{}:多文件分片上传失败!", fileName, e);
  214. 214             return ResultEntity.error("", "多文件分片上传失败");
  215. 215         }
  216. 216         //通过返回成功的分片值,来验证分片是否有丢失
  217. 217         return ResultEntity.success(partIndex.toString(), fileUid);
  218. 218     }
  219. 219  
  220. 220     /**
  221. 221      * 多文件(分片)秒传
  222. 222      * 通过对比已有的文件分片md5值和需要上传文件分片的MD5值,
  223. 223      * 在文件分片合并的时候,对已有的文件进行地址索引,对没有的文件进行临时文件写入
  224. 224      * 最后合并的时候根据不同的文件分片进行文件读取写入
  225. 225      *
  226. 226      * @param filePart  上传没有的分片文件
  227. 227      * @param fileInfo  当前分片文件相关信息
  228. 228      * @param fileOther 已存在文件分片相关信息
  229. 229      * @return
  230. 230      */
  231. 231     @Override
  232. 232     public ResultEntity<String> multipleFilePartFlashUpload(MultipartFile filePart, String fileInfo, String fileOther) {
  233. 233         DiskFileIndexVo upFileInfo = JSONObject.parseObject(fileInfo, DiskFileIndexVo.class);
  234. 234         List<DiskFileIndexVo> notUpFileInfoList = JSON.parseArray(fileOther, DiskFileIndexVo.class);
  235. 235         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  236. 236         String filePath = System.getProperty("user.dir") + "\\file\";//文件存放路径
  237. 237         //正常情况下,这个临时文件也应该放入(服务器)非临时文件夹中,这样方便下次其他文件上传查找是否曾经上传过类似的
  238. 238         //当前demo是单独存放在临时文件夹中,文件合并完成之后直接删除的
  239. 239         String tempPath = filePath + "temp\" + upFileInfo.getFileUid();//临时文件存放路径
  240. 240  
  241. 241         File dir = new File(tempPath);
  242. 242         if (!dir.exists()) dir.mkdirs();
  243. 243         //生成一个临时文件名
  244. 244         String tempFileNamePath = tempPath + "\" + upFileInfo.getFileName() + "_" + upFileInfo.getPartIndex() + ".part";
  245. 245  
  246. 246         try {
  247. 247             filePart.transferTo(new File(tempFileNamePath));
  248. 248  
  249. 249             File tempDir = new File(tempPath);
  250. 250             File[] tempFiles = tempDir.listFiles();
  251. 251             notUpFileInfoList = notUpFileInfoList.stream().filter(e ->
  252. 252                     upFileInfo.getFileUid().equals(e.getFileUid())).collect(Collectors.toList());
  253. 253             //如果临时文件夹中分片数量和实际分片数量一致的时候,就需要进行分片合并
  254. 254             one:
  255. 255             if ((upFileInfo.getPartNum() - notUpFileInfoList.size()) == tempFiles.length) {
  256. 256                 //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
  257. 257                 if (isMergePart.get(upFileInfo.getFileUid()) != null) {
  258. 258                     break one;
  259. 259                 }
  260. 260                 isMergePart.put(upFileInfo.getFileUid(), tempFiles.length);
  261. 261                 System.out.println(upFileInfo.getFileName() + ":所有分片上传完成,预计总分片:" + upFileInfo.getPartNum()
  262. 262                         + "; 实际总分片:" + tempFiles.length + "; 已存在分片数:" + notUpFileInfoList.size());
  263. 263  
  264. 264                 //使用多线程来完成对每个文件的合并
  265. 265                 Future<Integer> submit = partMergeTask.submit(
  266. 266                         new PartMergeFlashTaskExecutor(filePath, upFileInfo, notUpFileInfoList));
  267. 267                 isMergePart.remove(upFileInfo.getFileUid());
  268. 268             }
  269. 269         } catch (Exception e) {
  270. 270             log.error("{}:多文件(分片)秒传失败!", upFileInfo.getFileName(), e);
  271. 271             return ResultEntity.error("", "多文件(分片)秒传失败!");
  272. 272         }
  273. 273         //通过返回成功的分片值,来验证分片是否有丢失
  274. 274         return ResultEntity.success(upFileInfo.getPartIndex().toString(), upFileInfo.getFileUid());
  275. 275     }
  276. 276  
  277. 277     /**
  278. 278      * 根据传入需要上传的文件片段的md5值来对比服务器中的文件的md5值,将已有对应的md5值的文件过滤出来,
  279. 279      * 通知前端或者自行出来这些文件,即为不需要上传的文件分片,并将已有的文件分片地址索引返回给前端进行出来
  280. 280      *
  281. 281      * @param upLoadFileListMd5 原本需要上传文件的索引分片信息
  282. 282      * @return
  283. 283      */
  284. 284     @Override
  285. 285     public ResultEntity<List<DiskFileIndexVo>> checkDiskFile(List<DiskFileIndexVo> upLoadFileListMd5) {
  286. 286         List<DiskFileIndexVo> notUploadFile;
  287. 287         try {
  288. 288             //后端服务器已经存在的分片md5值集合
  289. 289             List<DiskFileIndexVo> diskFileMd5IndexList = diskFileIndexVos;
  290. 290  
  291. 291             notUploadFile = upLoadFileListMd5.stream().filter(uf -> diskFileMd5IndexList.stream().anyMatch(
  292. 292                     df -> {
  293. 293                         if (df.getFileMd5().equals(uf.getFileMd5())) {
  294. 294                             uf.setFileIndex(df.getFileName());//不需要上传文件的地址索引
  295. 295                             return true;
  296. 296                         }
  297. 297                         return false;
  298. 298                     })).collect(Collectors.toList());
  299. 299             log.info("过滤出不需要上传的文件分片:{}", notUploadFile);
  300. 300         } catch (Exception e) {
  301. 301             log.error("上传文件检测异常!", e);
  302. 302             return ResultEntity.error("上传文件检测异常!");
  303. 303         }
  304. 304         return ResultEntity.success(notUploadFile);
  305. 305     }
  306. 306  
  307. 307     /**
  308. 308      * 根据文件uuid(md5生成的)来判断此文件在服务器中是否未上传完整,
  309. 309      * 如果没上传完整,则返回相关上传进度等信息
  310. 310      *
  311. 311      * @param pointFileIndexVo
  312. 312      * @return
  313. 313      */
  314. 314     @Override
  315. 315     public ResultEntity<PointFileIndexVo> checkUploadFileIndex(PointFileIndexVo pointFileIndexVo) {
  316. 316         try {
  317. 317             List<String> list = uploadProgress.get(pointFileIndexVo.getFileMd5());
  318. 318             if (list == null) list = new ArrayList<>();
  319. 319             pointFileIndexVo.setParts(list);
  320. 320             System.out.println("已上传部分:" + list);
  321. 321             return ResultEntity.success(pointFileIndexVo);
  322. 322         } catch (Exception e) {
  323. 323             log.error("上传文件检测异常!", e);
  324. 324             return ResultEntity.error("上传文件检测异常!");
  325. 325         }
  326. 326     }
  327. 327  
  328. 328     /**
  329. 329      * 单文件(分片)断点上传
  330. 330      *
  331. 331      * @param filePart 需要上传的分片文件
  332. 332      * @param fileInfo 当前需要上传的分片文件信息,如uuid,文件名,文件总分片数量等
  333. 333      * @return
  334. 334      */
  335. 335     @Override
  336. 336     public ResultEntity<String> singleFilePartPointUpload(MultipartFile filePart, String fileInfo) {
  337. 337         PointFileIndexVo pointFileIndexVo = JSONObject.parseObject(fileInfo, PointFileIndexVo.class);
  338. 338         //实际情况下,这些路径都应该是服务器上面存储文件的路径
  339. 339         String filePath = System.getProperty("user.dir") + "\\file\";//文件存放路径
  340. 340         String tempPath = filePath + "temp\" + pointFileIndexVo.getFileMd5();//临时文件存放路径
  341. 341         File dir = new File(tempPath);
  342. 342         if (!dir.exists()) dir.mkdirs();
  343. 343  
  344. 344         //生成一个临时文件名
  345. 345         String tempFileNamePath = tempPath + "\" + pointFileIndexVo.getFileName() + "_" + pointFileIndexVo.getPartIndex() + ".part";
  346. 346         try {
  347. 347             //将分片存储到临时文件夹中
  348. 348             filePart.transferTo(new File(tempFileNamePath));
  349. 349  
  350. 350             List<String> partIndex = uploadProgress.get(pointFileIndexVo.getFileMd5());
  351. 351             if (Objects.isNull(partIndex)) {
  352. 352                 partIndex = new ArrayList<>();
  353. 353             }
  354. 354             partIndex.add(pointFileIndexVo.getPartIndex().toString());
  355. 355             uploadProgress.put(pointFileIndexVo.getFileMd5(), partIndex);
  356. 356  
  357. 357             File tempDir = new File(tempPath);
  358. 358             File[] tempFiles = tempDir.listFiles();
  359. 359  
  360. 360             one:
  361. 361             if (pointFileIndexVo.getPartNum().equals(Objects.requireNonNull(tempFiles).length)) {
  362. 362                 //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
  363. 363                 if (isMergePart.get(pointFileIndexVo.getFileMd5()) != null) {
  364. 364                     break one;
  365. 365                 }
  366. 366                 isMergePart.put(pointFileIndexVo.getFileMd5(), tempFiles.length);
  367. 367                 System.out.println("所有分片上传完成,预计总分片:" + pointFileIndexVo.getPartNum() + "; 实际总分片:" + tempFiles.length);
  368. 368                 //读取分片数据,进行分片合并
  369. 369                 FileOutputStream fileOutputStream = new FileOutputStream(filePath + pointFileIndexVo.getFileName());
  370. 370                 //这里如果分片很多的情况下,可以采用多线程来执行
  371. 371                 for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) {
  372. 372                     FileInputStream fileInputStream = new FileInputStream(tempPath + "\" + pointFileIndexVo.getFileName() + "_" + i + ".part");
  373. 373                     byte[] buf = new byte[1024 * 8];//8MB
  374. 374                     int length;
  375. 375                     while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
  376. 376                         fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
  377. 377                     }
  378. 378                     fileInputStream.close();
  379. 379                 }
  380. 380                 fileOutputStream.flush();
  381. 381                 fileOutputStream.close();
  382. 382  
  383. 383                 // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
  384. 384                 for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) {
  385. 385                     boolean delete = new File(tempPath + "\" + pointFileIndexVo.getFileName() + "_" + i + ".part").delete();
  386. 386                     File file = new File(tempPath + "\" + pointFileIndexVo.getFileName() + "_" + i + ".part");
  387. 387                 }
  388. 388                 //在删除对应的临时文件夹
  389. 389                 if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
  390. 390                   tempDir.delete();
  391. 391                 }
  392. 392                 isMergePart.remove(pointFileIndexVo.getFileMd5());
  393. 393                 uploadProgress.remove(pointFileIndexVo.getFileMd5());
  394. 394             }
  395. 395  
  396. 396         } catch (Exception e) {
  397. 397             log.error("单文件分片上传失败!", e);
  398. 398             return ResultEntity.error(pointFileIndexVo.getFileMd5(), "单文件分片上传失败");
  399. 399         }
  400. 400         //通过返回成功的分片值,来验证分片是否有丢失
  401. 401         return ResultEntity.success(pointFileIndexVo.getFileMd5(), pointFileIndexVo.getPartIndex().toString());
  402. 402     }
  403. 403  
  404. 404     /**
  405. 405      * 获取(服务器)指定文件存储路径下所有文件MD5值
  406. 406      * 实际情况下,每一个文件的md5值都是单独保存在数据库或者其他存储机制中的,
  407. 407      * 不需要每次都去读取文件然后获取md5值,这样多次io读取很耗性能
  408. 408      *
  409. 409      * @return
  410. 410      * @throws Exception
  411. 411      */
  412. 412     @Bean
  413. 413     private List<DiskFileIndexVo> getDiskFileMd5Index() throws Exception {
  414. 414         String filePath = System.getProperty("user.dir") + "\\file\\part\";
  415. 415         File saveFileDir = new File(filePath);
  416. 416         if (!saveFileDir.exists()) saveFileDir.mkdirs();
  417. 417  
  418. 418         List<DiskFileIndexVo> diskFileIndexVoList = new ArrayList<>();
  419. 419         File[] listFiles = saveFileDir.listFiles();
  420. 420         if (listFiles == null) return diskFileIndexVoList;
  421. 421  
  422. 422         for (File listFile : listFiles) {
  423. 423             String fileName = listFile.getName();
  424. 424             FileInputStream fileInputStream = new FileInputStream(filePath + fileName);
  425. 425             String md5DigestAsHex = DigestUtils.md5DigestAsHex(fileInputStream);
  426. 426  
  427. 427             DiskFileIndexVo diskFileIndexVo = new DiskFileIndexVo();
  428. 428             diskFileIndexVo.setFileName(fileName);
  429. 429             diskFileIndexVo.setFileMd5(md5DigestAsHex);
  430. 430             diskFileIndexVoList.add(diskFileIndexVo);
  431. 431             fileInputStream.close();
  432. 432         }
  433. 433  
  434. 434         diskFileIndexVos = diskFileIndexVoList;
  435. 435         log.info("服务器磁盘所有文件 {}", diskFileIndexVoList);
  436. 436         return diskFileIndexVoList;
  437. 437     }
复制代码
代码布局图:

 

  • 前端代码
整体的过程如下
前端将文件按照百分比进行盘算,每次上传文件的百分之一(文件分片),给文件分片做上序号及文件uuid,然后在循环内里对文件片断上传的时候在将当前分片值一起传给后端。
后端将前端每次上传的文件,放入到缓存目录;
前端将全部的文件内容都上传完毕后,发送一个合并哀求;
后端合并分片的之后对文件进行命名生存;
后端生存临时分片的时候命名索引,方便合并的时候按照分片索引进行合并;
vue模板代码:
  1. 1      
  2. 2      
  3. 3         <h3>单文件分片上传</h3>
  4. 4         <el-upload ref="upload" name="files" action="#" :on-change="selectSinglePartFile"
  5. 5           :on-remove="removeSingleFilePart" :file-list="singleFilePart.fileList" :auto-upload="false">
  6. 6           <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  7. 7           <el-button style="margin-left: 10px;" size="small" type="success"
  8. 8             @click="singleFilePartUpload">点击进行单文件分片上传</el-button>
  9. 9           主要用于测试单文件分片上传
  10. 10         </el-upload>
  11. 11         <el-progress :text-inside="true" class="progress" :stroke-width="26" :percentage="singlePartFileProgress" />
  12. 12      
  13. 13      
  14. 14      
  15. 15         <h3>多文件分片上传</h3>
  16. 16         <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFile"
  17. 17           :on-remove="removeMultiplePartFile" :file-list="multipleFilePart.fileList" :auto-upload="false">
  18. 18           <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  19. 19           <el-button style="margin-left: 10px;" size="small" type="success"
  20. 20             @click="multipleFilePartUpload">点击进行多文件分片上传</el-button>
  21. 21           主要用于测试多文件分片上传
  22. 22         </el-upload>
  23. 23      
  24. 24      
  25. 25      
  26. 26         <h3>多文件(分片MD5值)秒传</h3>
  27. 27         <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFlashFile"
  28. 28           :on-remove="removeMultiplePartFlashFile" :file-list="multipleFilePartFlash.fileList" :auto-upload="false">
  29. 29           <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  30. 30           <el-button style="margin-left: 10px;" size="small" type="success"
  31. 31             @click="multipleFilePartFlashUpload">点击进行文件秒传</el-button>
  32. 32           主要用于测试多文件(分片MD5值)秒传
  33. 33         </el-upload>
  34. 34      
复制代码
js属性定义:

 上传部门代码:

 minio分片上传:

 上传样式:

 

  • 功能演示及源码
部门演示图: 这里就以上传minio为例,测试上传minio以分片方式上

 以8M进行分切 28M刚好分了四个区,我们使用redis客户工具查看

 最后成功上传到minio中

 
而且看到上传文件大小为:28M
文件上传代码其实功能也简单也很明确,先将一个大文件分成n个小文件,然后给后端检测这些分片是否曾经上传中断过,即对这些分片进行过滤出来,并将过滤出对应的分片定位值结果返回给前端处置惩罚出不需要上传的分片和需要上传的文件分片,这里主要照旧区分到确定是这个文件的分区文件。
这里,为了方便各人直接可以大概使用Java源码,本文全部都采用Spring boot框架模式,另外使用了第三方插件,如果各人使用中没有使用到minio可以不需要引入并把相关代码移除即可,代码使用了redis作为分区数量缓存,相对于Java内存更稳定些。
demo源码下载gitee地址(代码包含Java后端工程和vue2前端工程):java-file-upload-demo: java 多文件上传、多文件分片上传、多文件秒传、minio分片上传等功能

 
 
 

 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

商道如狼道

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表