SpringBoot基于Minio實(shí)現(xiàn)分片上傳、斷點(diǎn)續(xù)傳的實(shí)現(xiàn)
一、準(zhǔn)備工作
安裝 Minio 服務(wù)后,在 SpringBoot 項(xiàng)目中使用以下代碼來(lái)獲取 MinioClient(用于操作 Minio 的服務(wù)端):
MinioClient client = MinioClient.builder()
.endpoint("http://192.168.xx.133:9000") // 服務(wù)端IP+端口
.credentials(minioProperties.getAccessKey(), // 服務(wù)端用戶(hù)名
minioProperties.getSecretKey()) // 服務(wù)端密碼
.build();二、實(shí)現(xiàn)分片上傳+斷點(diǎn)續(xù)傳
2.1 思路
分片上傳和斷點(diǎn)續(xù)傳的實(shí)現(xiàn)過(guò)程中,需要在Minio內(nèi)部記錄已上傳的分片文件。
這些分片文件將以文件md5作為父目錄,分片文件的名字按照01,02,...的順序進(jìn)行命名。同時(shí),還必須知道當(dāng)前文件的分片總數(shù),這樣就能夠根據(jù)總數(shù)來(lái)判斷文件是否上傳完畢了。
比如,一個(gè)文件被分成了10片,所以總數(shù)是10。當(dāng)前端發(fā)起上傳請(qǐng)求時(shí),把一個(gè)個(gè)文件分片依次上傳,Minio 服務(wù)器中存儲(chǔ)的臨時(shí)文件依次是01、02、03 等等。
假設(shè)前端把05分片上傳完畢了之后斷開(kāi)了連接,由于 Minio 服務(wù)器仍然存儲(chǔ)著01~05的分片文件,因此前端再次上傳文件時(shí),只需從06序號(hào)開(kāi)始上傳分片,而不用從頭開(kāi)始傳輸。這就是所謂的斷點(diǎn)續(xù)傳。
2.2 代碼
① 分片上傳API
為了實(shí)現(xiàn)以上思路,考慮實(shí)現(xiàn)一個(gè)方法,用于上傳文件的某一個(gè)分片。
/**
* 將文件進(jìn)行分片上傳
* <p>有一個(gè)未處理的bug(雖然概率很低很低):</p>
* 當(dāng)兩個(gè)線(xiàn)程同時(shí)上傳md5相同的文件時(shí),由于兩者會(huì)定位到同一個(gè)桶的同一個(gè)臨時(shí)目錄,兩個(gè)線(xiàn)程會(huì)相互產(chǎn)生影響!
*
* @param file 分片文件
* @param currIndex 當(dāng)前文件的分片索引
* @param totalPieces 切片總數(shù)(對(duì)于同一個(gè)文件,請(qǐng)確保切片總數(shù)始終不變)
* @param md5 整體文件MD5
* @return 剩余未上傳的文件索引集合
*/
public FragResult uploadFileFragment(MultipartFile file,
Integer currIndex, Integer totalPieces, String md5) throws Exception {
checkNull(currIndex, totalPieces, md5);
// 臨時(shí)文件存放桶
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());
// 還剩一個(gè)索引未上傳,當(dāng)前上傳索引剛好是未上傳索引,上傳完當(dāng)前索引后就完全結(jié)束了。
if ( remainIndex.size() == 1 && remainIndex.contains(currIndex) ) {
return new FragResult(true, null, "completed");
}
return new FragResult(false, remainIndex, "index [" + currIndex + "] has been uploaded");
}值得注意的是,我在項(xiàng)目中實(shí)踐該方法時(shí),上述參數(shù)都是由前端傳來(lái)的,因此文件分片過(guò)程發(fā)生在前端,分片的大小也由前端定義。
② 合并文件API
當(dāng)所有分片文件上傳完畢,需要手動(dòng)調(diào)用 Minio 原生 API 來(lái)合并臨時(shí)文件(當(dāng)然,在上面的那個(gè)方法中,當(dāng)最后一個(gè)分片上傳完畢后直接執(zhí)行合并操作也是可以的)
臨時(shí)文件合并完畢后,將會(huì)自動(dòng)刪除所有臨時(shí)文件。
/**
* 合并分片文件,并放到指定目錄
* 前提是之前已把所有分片上傳完畢。
*
* @param bucketName 目標(biāo)文件桶名
* @param targetName 目標(biāo)文件名(含完整路徑)
* @param totalPieces 切片總數(shù)(對(duì)于同一個(gè)文件,請(qǐng)確保切片總數(shù)始終不變)
* @param md5 文件md5
* @return minio原生對(duì)象,記錄了文件上傳信息
*/
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) {
// 文件路徑 轉(zhuǎn) 文件合并對(duì)象
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());
// 上傳成功,則刪除所有的臨時(shí)分片文件
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);
// 遍歷錯(cuò)誤集合(無(wú)元素則成功)
for (Result<DeleteError> result : deleteResults) {
DeleteError error = result.get();
System.err.printf("[Bigfile] 分片'%s'刪除失敗! 錯(cuò)誤信息: %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,對(duì)原生的 Minio API 進(jìn)行了封裝,抽取成了minio-spring-boot-starter組件,感興趣的朋友歡迎前去查看。
2.3 后端調(diào)用API示例
這里以單線(xiàn)程的分片上傳為例(即前端每次只上傳一個(gè)分片文件,調(diào)用分片上傳接口后,接口返回下一個(gè)分片文件的序號(hào))
① Controller 層
/**
* 分片上傳
* @param user 用戶(hù)對(duì)象
* @param fileAddDto file: 分片文件,
* currIndex: 當(dāng)前分片索引,
* totalPieces: 分片總數(shù),
* md5: 文件md5
* @return 前端需上傳的下一個(gè)分片序號(hào)(-1表示上傳完成)
*/
@PostMapping("/file/big/upload")
public ResultData<String> uploadBigFile(User user, BigFileAddDto fileAddDto) {
// 1.文件為空,返回失敗 (一般不是用戶(hù)的問(wèn)題)
if (fileAddDto.getFile() == null) {
throw new GlobalException();
}
// 2.名字為空,或包含特殊字符,則提示錯(cuò)誤
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ā)起文件合并請(qǐng)求, 無(wú)異常則成功
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] 下一個(gè)需上傳的文件索引是:{}", nextIndex);
return nextIndex;
}
}
} catch (Exception e) {
e.printStackTrace();
}
log.error("[Bigfile] 上傳文件時(shí)出現(xiàn)異常");
throw new GlobalException(ResultCode.FILE_UPLOAD_ERROR);
}2.4 前端
前端主要負(fù)責(zé):
- 規(guī)定文件分片的大?。ū热?M),然后把文件進(jìn)行拆分。
- 計(jì)算文件分片的總數(shù),并按序號(hào)把分片文件依次傳遞給后端。
- 前端每上傳完一個(gè)分片文件,接口都會(huì)返回下一個(gè)需要上傳的分片文件。此時(shí)前端把對(duì)應(yīng)的分片文件繼續(xù)上傳即可。
- 當(dāng)接口返回“-1”,表示所有文件已上傳完畢。
前端代碼此處不展示,有緣后續(xù)再花時(shí)間補(bǔ)充吧………………
到此這篇關(guān)于SpringBoot基于Minio實(shí)現(xiàn)分片上傳、斷點(diǎn)續(xù)傳的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot Minio分片上傳、斷點(diǎn)續(xù)傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用try-with-resource機(jī)制關(guān)閉連接
這篇文章主要介紹了使用try-with-resource機(jī)制關(guān)閉連接的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java使用Freemarker頁(yè)面靜態(tài)化生成的實(shí)現(xiàn)
這篇文章主要介紹了Java使用Freemarker頁(yè)面靜態(tài)化生成的實(shí)現(xiàn),頁(yè)面靜態(tài)化是將原來(lái)的動(dòng)態(tài)網(wǎng)頁(yè)改為通過(guò)靜態(tài)化技術(shù)生成的靜態(tài)網(wǎng)頁(yè),FreeMarker?是一個(gè)用?Java?語(yǔ)言編寫(xiě)的模板引擎,它基于模板來(lái)生成文本輸,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-06-06
解決maven?maven.compiler.source和maven.compiler.target的坑
這篇文章主要介紹了解決maven?maven.compiler.source和maven.compiler.target的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決)
這篇文章主要介紹了Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
IDEA利用jclasslib 修改class文件的實(shí)現(xiàn)
這篇文章主要介紹了IDEA利用jclasslib 修改class文件的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
微服務(wù)Spring?Cloud?Alibaba?的介紹及主要功能詳解
Spring?Cloud?是一個(gè)通用的微服務(wù)框架,適合于多種環(huán)境下的開(kāi)發(fā),而?Spring?Cloud?Alibaba?則是為阿里巴巴技術(shù)棧量身定制的解決方案,本文給大家介紹Spring?Cloud?Alibaba?的介紹及主要功能,感興趣的朋友跟隨小編一起看看吧2024-08-08

