Java大檔案上傳、分片上傳、多檔案上傳、斷點續傳、上傳檔案minio、分片上傳minio等解決方案

Angelasp發表於2024-06-24
  • 上傳說明

    檔案上傳花樣百出,根據不同場景使用不同方案進行實現尤為必要。通常開發過程中,檔案較小,直接將檔案轉化為位元組流上傳到伺服器,但是檔案較大時,用普通的方法上傳,顯然效果不是很好,當檔案上傳一半中斷再次上傳時,發現需要重新開始,這種體驗不是很爽,下面介紹幾種好一點兒的上傳方式。

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

程式碼結構圖:

  • 前端程式碼

整體的過程如下
前端將檔案按照百分比進行計算,每次上傳檔案的百分之一(檔案分片),給檔案分片做上序號及檔案uuid,然後在迴圈裡面對檔案片段上傳的時候在將當前分片值一起傳給後端。
後端將前端每次上傳的檔案,放入到快取目錄;
前端將全部的檔案內容都上傳完畢後,傳送一個合併請求;
後端合併分片的之後對檔案進行命名儲存;
後端儲存臨時分片的時候命名索引,方便合併的時候按照分片索引進行合併;

vue模板程式碼:

 1       <!-- 單檔案分片上傳 -->
 2       <div class="fileUploadStyle">
 3         <h3>單檔案分片上傳</h3>
 4         <el-upload ref="upload" name="files" action="#" :on-change="selectSinglePartFile"
 5           :on-remove="removeSingleFilePart" :file-list="singleFilePart.fileList" :auto-upload="false">
 6           <el-button slot="trigger" size="small" type="primary">選取檔案</el-button>
 7           <el-button style="margin-left: 10px;" size="small" type="success"
 8             @click="singleFilePartUpload">點選進行單檔案分片上傳</el-button>
 9           <div slot="tip" class="el-upload__tip">主要用於測試單檔案分片上傳</div>
10         </el-upload>
11         <el-progress :text-inside="true" class="progress" :stroke-width="26" :percentage="singlePartFileProgress" />
12       </div>
13       <!-- 多檔案分片上傳 -->
14       <div class="fileUploadStyle">
15         <h3>多檔案分片上傳</h3>
16         <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFile"
17           :on-remove="removeMultiplePartFile" :file-list="multipleFilePart.fileList" :auto-upload="false">
18           <el-button slot="trigger" size="small" type="primary">選取檔案</el-button>
19           <el-button style="margin-left: 10px;" size="small" type="success"
20             @click="multipleFilePartUpload">點選進行多檔案分片上傳</el-button>
21           <div slot="tip" class="el-upload__tip">主要用於測試多檔案分片上傳</div>
22         </el-upload>
23       </div>
24       <!-- 多檔案(分片)秒傳 -->
25       <div class="fileUploadStyle">
26         <h3>多檔案(分片MD5值)秒傳</h3>
27         <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFlashFile"
28           :on-remove="removeMultiplePartFlashFile" :file-list="multipleFilePartFlash.fileList" :auto-upload="false">
29           <el-button slot="trigger" size="small" type="primary">選取檔案</el-button>
30           <el-button style="margin-left: 10px;" size="small" type="success"
31             @click="multipleFilePartFlashUpload">點選進行檔案秒傳</el-button>
32           <div slot="tip" class="el-upload__tip">主要用於測試多檔案(分片MD5值)秒傳</div>
33         </el-upload>
34       </div>

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分片上傳等功能


相關文章