SpringBoot 后端項目利用 Minio 實現分片上傳、斷點續傳

一、準備工作

安裝 Minio 服務后,在 SpringBoot 項目中添加依賴:

	<!-- MinIO --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.2.1</version></dependency>

在代碼中獲取 MinioClient(用于操作 Minio 的服務端):

MinioClient client = MinioClient.builder().endpoint("http://192.168.xx.133:9000")  // 服務端IP+端口.credentials(minioProperties.getAccessKey(), // 服務端用戶名minioProperties.getSecretKey()) // 服務端密碼.build();

二、實現分片上傳+斷點續傳

2.1 思路

分片上傳和斷點續傳的實現過程中,需要在Minio內部記錄已上傳的分片文件。

這些分片文件將以文件md5作為父目錄,分片文件的名字按照01,02,...的順序進行命名。同時,還必須知道當前文件的分片總數,這樣就能夠根據總數來判斷文件是否上傳完畢了。

比如,一個文件被分成了10片,所以總數是10。當前端發起上傳請求時,把一個個文件分片依次上傳,Minio 服務器中存儲的臨時文件依次是010203 等等。

假設前端把05分片上傳完畢了之后斷開了連接,由于 Minio 服務器仍然存儲著01~05的分片文件,因此前端再次上傳文件時,只需從06序號開始上傳分片,而不用從頭開始傳輸。這就是所謂的斷點續傳

2.2 代碼

① 分片上傳API

為了實現以上思路,考慮實現一個方法,用于上傳文件的某一個分片。

/*** 將文件進行分片上傳* <p>有一個未處理的bug(雖然概率很低很低):</p>* 當兩個線程同時上傳md5相同的文件時,由于兩者會定位到同一個桶的同一個臨時目錄,兩個線程會相互產生影響!* * @param file 分片文件* @param currIndex 當前文件的分片索引* @param totalPieces 切片總數(對于同一個文件,請確保切片總數始終不變)* @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");}

值得注意的是,我在項目中實踐該方法時,上述參數都是由前端傳來的,因此文件分片過程發生在前端,分片的大小也由前端定義。

② 合并文件API

當所有分片文件上傳完畢,需要手動調用 Minio 原生 API 來合并臨時文件(當然,在上面的那個方法中,當最后一個分片上傳完畢后直接執行合并操作也是可以的)

臨時文件合并完畢后,將會自動刪除所有臨時文件。

/*** 合并分片文件,并放到指定目錄* 前提是之前已把所有分片上傳完畢。* * @param bucketName 目標文件桶名* @param targetName 目標文件名(含完整路徑)* @param totalPieces 切片總數(對于同一個文件,請確保切片總數始終不變)* @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: 分片總數,*                   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. 執行分片上傳String result = fileSystemService.uploadBigFile(user, fileAddDto);return GlobalResult.success(result);}

② Service 層

	@Overridepublic 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()));// 發起文件合并請求, 無異常則成功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] 上傳文件時出現異常");throw new GlobalException(ResultCode.FILE_UPLOAD_ERROR);}

2.4 前端

前端主要負責:

  • 規定文件分片的大小(比如5M),然后把文件進行拆分。
  • 計算文件分片的總數,并按序號把分片文件依次傳遞給后端。
  • 前端每上傳完一個分片文件,接口都會返回下一個需要上傳的分片文件。此時前端把對應的分片文件繼續上傳即可。
  • 當接口返回“-1”,表示所有文件已上傳完畢。

前端代碼此處不展示,有緣后續再花時間補充吧………………

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/34992.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/34992.shtml
英文地址,請注明出處:http://en.pswp.cn/news/34992.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【js】日期、時間正則匹配

1、日期的正則表達式 格式&#xff1a;2023-08-11 var reg /^[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/; var regExp new RegExp(reg); if(!regExp.test(value)){alert("日期格式不正確");return; }2、時間的正則表達式 格式&#xff1a;23:00:00…

英碼國產高配邊緣計算盒子上市!搭載TPU處理器BM1684X,適配麒麟系統,支持OTA升級!

隨著人工智能技術不斷深入實際應用場景&#xff0c;加速各行各業場景應用落地&#xff0c;邊緣計算的重要性越發凸顯。相較于傳統的集中式云計算&#xff0c;邊緣計算在距離數據源或用戶更近的地方提供計算能力&#xff0c;不僅滿足了對實時性要求較高的場景應用需求&#xff0…

操作系統結構

操作系統結構 分層法模塊化宏內核微內核微內核的基本概念微內核的基本功能 內核 分層法 分層法是將操作系統分為若干層&#xff0c;最底層為硬件&#xff0c;最高層為用戶接口&#xff0c;每層只能調用緊鄰它的底層的功能和服務&#xff08;單向依賴&#xff09; 分層法的優點…

如何通過CSS選擇器選擇一個元素的子元素?如何選擇第一個子元素和最后一個子元素?

聚沙成塔每天進步一點點 ? 專欄簡介? 選擇一個元素的子元素? 選擇第一個子元素和最后一個子元素? 注意事項? 寫在最后 ? 專欄簡介 前端入門之旅&#xff1a;探索Web開發的奇妙世界 記得點擊上方或者右側鏈接訂閱本專欄哦 幾何帶你啟航前端之旅 歡迎來到前端入門之旅&…

線程池,以及線程池的實現以及面試常問的問題,工廠模式,常見的鎖策略(面試常考,要了解,不行就背)

一、&#x1f49b; 線程池的基本介紹 內存池&#xff0c;進程池&#xff0c;連接池&#xff0c;常量池&#xff0c;這些池子概念上都是一樣的&#xff5e;&#xff5e; 如果我們需要頻繁的創建銷毀線程&#xff0c;此時創建銷毀的成本就不能忽視了&#xff0c;因此就可以使用線…

Java中使用instanceof判斷對象類型

記錄&#xff1a;470 場景&#xff1a;Java中使用instanceof判斷對象類型。例如在解析JSON字符串轉換為指定類型時&#xff0c;先判斷類型&#xff0c;再定向轉換。在List<Object>中遍歷Object時&#xff0c;先判斷類型&#xff0c;再定向轉換。 版本&#xff1a;JDK 1…

Redis系列(一):深入了解Redis數據類型和底層數據結構

Redis有以下幾種常用的數據類型&#xff1a; redis數據是如何組織的 為了實現從鍵到值的快速訪問&#xff0c;Redis 使用了一個哈希表來保存所有鍵值對。 Redis全局哈希表&#xff08;Global Hash Table&#xff09;是指在Redis數據庫內部用于存儲所有鍵值對的主要數據結構。…

安卓13不再支持PPTP怎么辦?新的連接解決方案分享

隨著Android 13的發布&#xff0c;我們迎來了一個令人興奮的新品時刻。然而&#xff0c;對于一些用戶而言&#xff0c;這也意味著必須面對一個重要的問題&#xff1a;Android 13不再支持PPTP協議。如果你是一個習慣使用PPTP協議來連接換地址的用戶&#xff0c;那么你可能需要重…

C++ 泛型編程:函數模板

文章目錄 前言一、什么是泛型編程二、函數模板三、函數模板的使用四、多參數函數模板五&#xff0c;示例代碼&#xff1a;總結 前言 當需要編寫通用的代碼以處理不同類型的數據時&#xff0c;C 中的函數模板是一個很有用的工具。函數模板允許我們編寫一個通用的函數定義&#…

Vue day02 Computed和Watch

1.事件綁定 可以用 v-on 指令監聽DOM 事件&#xff0c;并在觸發時運行一些 JavaScript 代碼。v-on 還可以接收一個需要調用的方法名稱。 <button v-on:click"handler">good</button> methods: { handler: function (event) { if (event) { alert(event.t…

接口測試之Jmeter+Ant+Jenkins接口自動化測試平臺

平臺簡介 一個完整的接口自動化測試平臺需要支持接口的自動執行&#xff0c;自動生成測試報告&#xff0c;以及持續集成。Jmeter支持接口的測試&#xff0c;Ant支持自動構建&#xff0c;而Jenkins支持持續集成&#xff0c;所以三者組合在一起可以構成一個功能完善的接口自動化…

BOLT- 識別和優化熱門的基本塊

在BOLT中&#xff0c;識別和優化熱門的基本塊之所以關鍵&#xff0c;是因為BOLT的主要目標是優化程序以更好地利用硬件特性&#xff0c;特別是指令緩存&#xff08;ICache&#xff09;。以下是BOLT如何識別和優化熱門基本塊的流程&#xff1a; 收集性能數據: BOLT開始的時候并不…

idea - 刷新 Git 分支數據 / 命令刷新 Git 分支數據

一、idea - 刷新 Git 分支數據 idea 找到 fetch 選項&#xff0c;重新獲取分支數據 二、命令刷新 Git 分支數據 git fetch參考鏈接 1. 遠程Gitlab新建的分支在IDEA里不顯示

jxls導出問題

![請添加圖片描述](https://img-blog.csdnimg.cn/bc74c4207818491c93b75e19b3333451.png 為什么最后導出的文件還是按原樣導出啊&#xff0c;沒有填充數據 ![在這里插入圖片描述](https://img-blog.csdnimg.cn/d4500b9a98c042f6b64a5d0650071303.png

qt多線程使用方式

有5個方式&#xff1a;可以參考這個博客&#xff1a;Qt 中開啟線程的五種方式_qt 線程_lucky-billy的博客-CSDN博客 注&#xff1a;為了實現更加靈活的線程管理&#xff08;因為這5種都有一些不方便之處&#xff1a;QThread需要子類化且不能傳參&#xff0c;moveToThread不能傳…

【leetcode】459. 重復的子字符串(easy)

給定一個非空的字符串 s &#xff0c;檢查是否可以通過由它的一個子串重復多次構成。 示例 1: 輸入: s “abab” 輸出: true 解釋: 可由子串 “ab” 重復兩次構成。 示例 2: 輸入: s “aba” 輸出: false 示例 3: 輸入: s “abcabcabcabc” 輸出: true 解釋: 可由子串 “ab…

ChatGPT等人工智能編寫文章的內容今后將成為常態

BuzzFeed股價上漲200%可能標志著“轉向人工智能”媒體趨勢的開始。 周四&#xff0c;一份內部備忘錄被華爾街日報透露BuzzFeed正計劃使用ChatGPT聊天機器人-風格文本合成技術來自OpenAI&#xff0c;用于創建個性化盤問和將來可能的其他內容。消息傳出后&#xff0c;BuzzFeed的…

ubuntu 20.04 RK3568網絡的優先級設置

1、背景 硬件使用RK3568 CPU&#xff0c;操作系統采用ubuntu 20.04 Lxqt桌面的版本。硬件上具有一個有線以太網卡&#xff0c;一個wifi網卡&#xff0c;一個5G網卡。由于操作系統默認的網絡優先級為有線網卡的最高&#xff0c;5G網卡次之。在一個業務應用中需要5G網卡的連接外…

文本三劍客之grep命令和awk命令 1.0 版本

grep awk 1.grep命令1.1 基本格式1.2 常用選項 2.awk命令2.1 awk工作原理2.2 awk命令格式2.3 awk常用內置變量 1.grep命令 1.1 基本格式 grep [選項]… 查找條件 目標文件1.2 常用選項 選項功能 -m [ x ]匹配x次 后停止,x為具體數字-v取反 -i忽略字符大小寫 -n顯示匹配的 …

Dynamic CRM開發 - 實體介紹

實體簡介 在CRM中,實體(Entity)是數據的基本載體,也是構建業務邏輯網絡的基礎節點。 實體可以理解為數據庫中的一張表(實體中的字段對應數據庫表的字段),比如創建一個實體存儲客戶信息,創建一個實體存儲產品信息,產品實體里可以創建一個查找類型的字段(類似表的外鍵)…