SpringBoot基于Minio實現(xiàn)分片上傳、斷點續(xù)傳的實現(xiàn)
一、準備工作
安裝 Minio 服務后,在 SpringBoot 項目中使用以下代碼來獲取 MinioClient(用于操作 Minio 的服務端):
MinioClient client = MinioClient.builder() .endpoint("http://192.168.xx.133:9000") // 服務端IP+端口 .credentials(minioProperties.getAccessKey(), // 服務端用戶名 minioProperties.getSecretKey()) // 服務端密碼 .build();
二、實現(xiàn)分片上傳+斷點續(xù)傳
2.1 思路
分片上傳和斷點續(xù)傳的實現(xiàn)過程中,需要在Minio內部記錄已上傳的分片文件。
這些分片文件將以文件md5作為父目錄,分片文件的名字按照01,02,...的順序進行命名。同時,還必須知道當前文件的分片總數(shù),這樣就能夠根據(jù)總數(shù)來判斷文件是否上傳完畢了。
比如,一個文件被分成了10片,所以總數(shù)是10。當前端發(fā)起上傳請求時,把一個個文件分片依次上傳,Minio 服務器中存儲的臨時文件依次是01、02、03 等等。
假設前端把05分片上傳完畢了之后斷開了連接,由于 Minio 服務器仍然存儲著01~05的分片文件,因此前端再次上傳文件時,只需從06序號開始上傳分片,而不用從頭開始傳輸。這就是所謂的斷點續(xù)傳。
2.2 代碼
① 分片上傳API
為了實現(xiàn)以上思路,考慮實現(xiàn)一個方法,用于上傳文件的某一個分片。
/** * 將文件進行分片上傳 * <p>有一個未處理的bug(雖然概率很低很低):</p> * 當兩個線程同時上傳md5相同的文件時,由于兩者會定位到同一個桶的同一個臨時目錄,兩個線程會相互產生影響! * * @param file 分片文件 * @param currIndex 當前文件的分片索引 * @param totalPieces 切片總數(shù)(對于同一個文件,請確保切片總數(shù)始終不變) * @param md5 整體文件MD5 * @return 剩余未上傳的文件索引集合 */ public FragResult uploadFileFragment(MultipartFile file, Integer currIndex, Integer totalPieces, String md5) throws Exception { checkNull(currIndex, totalPieces, md5); // 臨時文件存放桶 if ( !this.bucketExists(DEFAULT_TEMP_BUCKET_NAME) ) { this.createBucket(DEFAULT_TEMP_BUCKET_NAME); } // 得到已上傳的文件索引 Iterable<Result<Item>> results = this.getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat("/"), false); Set<Integer> savedIndex = Sets.newHashSet(); boolean fileExists = false; for (Result<Item> item : results) { Integer idx = Integer.valueOf( getContentAfterSlash(item.get().objectName()) ); if (currIndex.equals( idx )) { fileExists = true; } savedIndex.add( idx ); } // 得到未上傳的文件索引 Set<Integer> remainIndex = Sets.newTreeSet(); for (int i = 0; i < totalPieces; i++) { if ( !savedIndex.contains(i) ) { remainIndex.add(i); } } if (fileExists) { return new FragResult(false, remainIndex, "index [" + currIndex + "] exists"); } this.uploadFileStream(DEFAULT_TEMP_BUCKET_NAME, this.getFileTempPath(md5, currIndex, totalPieces), file.getInputStream()); // 還剩一個索引未上傳,當前上傳索引剛好是未上傳索引,上傳完當前索引后就完全結束了。 if ( remainIndex.size() == 1 && remainIndex.contains(currIndex) ) { return new FragResult(true, null, "completed"); } return new FragResult(false, remainIndex, "index [" + currIndex + "] has been uploaded"); }
值得注意的是,我在項目中實踐該方法時,上述參數(shù)都是由前端傳來的,因此文件分片過程發(fā)生在前端,分片的大小也由前端定義。
② 合并文件API
當所有分片文件上傳完畢,需要手動調用 Minio 原生 API 來合并臨時文件(當然,在上面的那個方法中,當最后一個分片上傳完畢后直接執(zhí)行合并操作也是可以的)
臨時文件合并完畢后,將會自動刪除所有臨時文件。
/** * 合并分片文件,并放到指定目錄 * 前提是之前已把所有分片上傳完畢。 * * @param bucketName 目標文件桶名 * @param targetName 目標文件名(含完整路徑) * @param totalPieces 切片總數(shù)(對于同一個文件,請確保切片總數(shù)始終不變) * @param md5 文件md5 * @return minio原生對象,記錄了文件上傳信息 */ public boolean composeFileFragment(String bucketName, String targetName, Integer totalPieces, String md5) throws Exception { checkNull(bucketName, targetName, totalPieces, md5); // 檢查文件索引是否都上傳完畢 Iterable<Result<Item>> results = this.getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat("/"), false); Set<String> savedIndex = Sets.newTreeSet(); for (Result<Item> item : results) { savedIndex.add( item.get().objectName() ); } if (savedIndex.size() == totalPieces) { // 文件路徑 轉 文件合并對象 List<ComposeSource> sourceObjectList = savedIndex.stream() .map(filePath -> ComposeSource.builder() .bucket(DEFAULT_TEMP_BUCKET_NAME) .object( filePath ) .build()) .collect(Collectors.toList()); ObjectWriteResponse objectWriteResponse = client.composeObject( ComposeObjectArgs.builder() .bucket(bucketName) .object(targetName) .sources(sourceObjectList) .build()); // 上傳成功,則刪除所有的臨時分片文件 List<String> filePaths = Stream.iterate(0, i -> ++i) .limit(totalPieces) .map(i -> this.getFileTempPath(md5, i, totalPieces) ) .collect(Collectors.toList()); Iterable<Result<DeleteError>> deleteResults = this.removeFiles(DEFAULT_TEMP_BUCKET_NAME, filePaths); // 遍歷錯誤集合(無元素則成功) for (Result<DeleteError> result : deleteResults) { DeleteError error = result.get(); System.err.printf("[Bigfile] 分片'%s'刪除失敗! 錯誤信息: %s", error.objectName(), error.message()); } return true; } throw new GlobalException("The fragment index is not complete. Please check parameters [totalPieces] or [md5]"); }
以上方法的源碼我放到了https://github.com/sky-boom/minio-spring-boot-starter,對原生的 Minio API 進行了封裝,抽取成了minio-spring-boot-starter組件,感興趣的朋友歡迎前去查看。
2.3 后端調用API示例
這里以單線程的分片上傳為例(即前端每次只上傳一個分片文件,調用分片上傳接口后,接口返回下一個分片文件的序號)
① Controller 層
/** * 分片上傳 * @param user 用戶對象 * @param fileAddDto file: 分片文件, * currIndex: 當前分片索引, * totalPieces: 分片總數(shù), * md5: 文件md5 * @return 前端需上傳的下一個分片序號(-1表示上傳完成) */ @PostMapping("/file/big/upload") public ResultData<String> uploadBigFile(User user, BigFileAddDto fileAddDto) { // 1.文件為空,返回失敗 (一般不是用戶的問題) if (fileAddDto.getFile() == null) { throw new GlobalException(); } // 2.名字為空,或包含特殊字符,則提示錯誤 String fileName = fileAddDto.getFile().getOriginalFilename(); if (StringUtils.isEmpty(fileName) || fileName.matches(FileSysConstant.NAME_EXCEPT_SYMBOL)) { throw new GlobalException(ResultCode.INCORRECT_FILE_NAME); } // 3. 執(zhí)行分片上傳 String result = fileSystemService.uploadBigFile(user, fileAddDto); return GlobalResult.success(result); }
② Service 層
@Override public String uploadBigFile(User user, BigFileAddDto fileAddDto) { try { MultipartFile file = fileAddDto.getFile(); Integer currIndex = fileAddDto.getCurrIndex(); Integer totalPieces = fileAddDto.getTotalPieces(); String md5 = fileAddDto.getMd5(); log.info("[Bigfile] 上傳文件md5: {} ,分片索引: {}", md5, currIndex); FragResult fragResult = minioUtils.uploadFileFragment(file, currIndex, totalPieces, md5); // 分片全部上傳完畢 if ( fragResult.isAllCompleted() ) { FileInfo fileInfo = getFileInfo(fileAddDto, user.getId()); DBUtils.checkOperation( fileSystemMapper.insertFile(fileInfo) ); String realPath = generateRealPath(generateVirtPath(fileAddDto.getParentPath(), file.getOriginalFilename())); // 發(fā)起文件合并請求, 無異常則成功 minioUtils.composeFileFragment(getBucketByUsername(user.getUsername()), realPath, totalPieces, md5); return "-1"; } else { Iterator<Integer> iterator = fragResult.getRemainIndex().iterator(); if (iterator.hasNext()) { String nextIndex = iterator.next().toString(); log.info("[BigFile] 下一個需上傳的文件索引是:{}", nextIndex); return nextIndex; } } } catch (Exception e) { e.printStackTrace(); } log.error("[Bigfile] 上傳文件時出現(xiàn)異常"); throw new GlobalException(ResultCode.FILE_UPLOAD_ERROR); }
2.4 前端
前端主要負責:
- 規(guī)定文件分片的大?。ū热?M),然后把文件進行拆分。
- 計算文件分片的總數(shù),并按序號把分片文件依次傳遞給后端。
- 前端每上傳完一個分片文件,接口都會返回下一個需要上傳的分片文件。此時前端把對應的分片文件繼續(xù)上傳即可。
- 當接口返回“-1”,表示所有文件已上傳完畢。
前端代碼此處不展示,有緣后續(xù)再花時間補充吧………………
到此這篇關于SpringBoot基于Minio實現(xiàn)分片上傳、斷點續(xù)傳的實現(xiàn)的文章就介紹到這了,更多相關SpringBoot Minio分片上傳、斷點續(xù)傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java使用Freemarker頁面靜態(tài)化生成的實現(xiàn)
這篇文章主要介紹了Java使用Freemarker頁面靜態(tài)化生成的實現(xiàn),頁面靜態(tài)化是將原來的動態(tài)網(wǎng)頁改為通過靜態(tài)化技術生成的靜態(tài)網(wǎng)頁,FreeMarker?是一個用?Java?語言編寫的模板引擎,它基于模板來生成文本輸,更多相關內容需要的小伙伴可以參考一下2022-06-06解決maven?maven.compiler.source和maven.compiler.target的坑
這篇文章主要介紹了解決maven?maven.compiler.source和maven.compiler.target的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12Eureka注冊不上或注冊后IP不對(多網(wǎng)卡的坑及解決)
這篇文章主要介紹了Eureka注冊不上或注冊后IP不對(多網(wǎng)卡的坑及解決),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11IDEA利用jclasslib 修改class文件的實現(xiàn)
這篇文章主要介紹了IDEA利用jclasslib 修改class文件的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-02-02微服務Spring?Cloud?Alibaba?的介紹及主要功能詳解
Spring?Cloud?是一個通用的微服務框架,適合于多種環(huán)境下的開發(fā),而?Spring?Cloud?Alibaba?則是為阿里巴巴技術棧量身定制的解決方案,本文給大家介紹Spring?Cloud?Alibaba?的介紹及主要功能,感興趣的朋友跟隨小編一起看看吧2024-08-08