Springboot2.7+Minio8 實(shí)現(xiàn)大文件分片上傳
1. 介紹:
分片上傳: 將一個(gè)文件按照指定大小分割成多份數(shù)據(jù)塊(Part)分開上傳, 上傳之后再由服務(wù)端整合為原本的文件
分片上傳場(chǎng)景:
- 網(wǎng)絡(luò)環(huán)境差: 當(dāng)出現(xiàn)上傳失敗的時(shí)候,只需要對(duì)失敗的Part進(jìn)行重新上傳
- 斷點(diǎn)續(xù)傳: 中途暫停之后,可以從上次上傳完成的Part的位置繼續(xù)上傳
- 加速上傳: 要上傳到OSS的本地文件很大的時(shí)候,可以并行上傳多個(gè)Part以加快上傳速度
- 流式上傳: 可以在需要上傳的文件大小還不確定的情況下開始上傳,這種場(chǎng)景在視頻監(jiān)控等行業(yè)應(yīng)用中比較常見
- 文件較大: 一般文件比較大時(shí),默認(rèn)情況下一般都會(huì)采用分片上傳
分片上傳流程:
- 將需要上傳的文件按照一定大小進(jìn)行分割(推薦1MB或者5MB),分割成相同大小的數(shù)據(jù)塊
- 初始化一個(gè)分片上傳任務(wù),返回本次分片上傳唯一標(biāo)識(shí)(md5)
- 按照一定的策略(串行或并行)發(fā)送各個(gè)分片數(shù)據(jù)塊
- 發(fā)送完成后,服務(wù)端根據(jù)判斷數(shù)據(jù)上傳是否完整,如果完整,則進(jìn)行數(shù)據(jù)塊合成得到原始文件。
2. 代碼部分:
application.yml
minio:
minioUrl: http://ip地址:9000 # MinIO 服務(wù)地址-需要修改
minioName: 賬號(hào) # MinIO 訪問密鑰-需要修改
minioPass: 密碼 # MinIO 秘鑰密碼-需要修改
bucketName: 桶名 # MinIO 桶名稱-需要修改
region: ap-southeast-1 # MinIO 存儲(chǔ)區(qū)域,可以指定為 "ap-southeast-1"
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MBpom.xml
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.0.3</version>
</dependency>MinioTemplate
/**
* @author xiaoyi
*/
@Slf4j
@AllArgsConstructor
public class MinioTemplate {
/**
* MinIO 客戶端
*/
private final MinioClient minioClient;
/**
* MinIO 配置類
*/
private final MinioConfig minioConfig;
/**
* 查詢所有存儲(chǔ)桶
*
* @return Bucket 集合
*/
@SneakyThrows
public List<Bucket> listBuckets() {
return minioClient.listBuckets();
}
/**
* 查詢文件大小
*
* @return Bucket 集合
*/
@SneakyThrows
public Long getObjectSize(String bucketName, String objectName) {
return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()).size();
}
/**
* 桶是否存在
*
* @param bucketName 桶名
* @return 是否存在
*/
@SneakyThrows
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 創(chuàng)建存儲(chǔ)桶
*
* @param bucketName 桶名
*/
@SneakyThrows
public void makeBucket(String bucketName) {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 刪除一個(gè)空桶 如果存儲(chǔ)桶存在對(duì)象不為空時(shí),刪除會(huì)報(bào)錯(cuò)。
*
* @param bucketName 桶名
*/
@SneakyThrows
public void removeBucket(String bucketName) {
removeBucket(bucketName, false);
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 刪除一個(gè)桶 根據(jù)桶是否存在數(shù)據(jù)進(jìn)行不同的刪除
* 桶為空時(shí)直接刪除
* 桶不為空時(shí)先刪除桶中的數(shù)據(jù),然后再刪除桶
*
* @param bucketName 桶名
*/
@SneakyThrows
public void removeBucket(String bucketName, boolean bucketNotNull) {
if (bucketNotNull) {
deleteBucketAllObject(bucketName);
}
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 上傳文件
*
* @param inputStream 流
* @param originalFileName 原始文件名
* @param bucketName 桶名
* @return ObjectWriteResponse
*/
@SneakyThrows
public OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {
String uuidFileName = generateFileInMinioName(originalFileName);
try {
if (ObjectUtils.isEmpty(bucketName)) {
bucketName = minioConfig.getBucketName();
}
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(uuidFileName)
.stream(inputStream, inputStream.available(), -1)
.build());
return new OssFile(uuidFileName, originalFileName);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* 刪除桶中所有的對(duì)象
*
* @param bucketName 桶對(duì)象
*/
@SneakyThrows
public void deleteBucketAllObject(String bucketName) {
List<String> list = listObjectNames(bucketName);
if (!list.isEmpty()) {
for (String objectName : list) {
deleteObject(bucketName, objectName);
}
}
}
/**
* 查詢桶中所有的對(duì)象名
*
* @param bucketName 桶名
* @return objectNames
*/
@SneakyThrows
public List<String> listObjectNames(String bucketName) {
List<String> objectNameList = new ArrayList<>();
if (bucketExists(bucketName)) {
Iterable<Result<Item>> results = listObjects(bucketName, true);
for (Result<Item> result : results) {
String objectName = result.get().objectName();
objectNameList.add(objectName);
}
}
return objectNameList;
}
/**
* 刪除一個(gè)對(duì)象
*
* @param bucketName 桶名
* @param objectName 對(duì)象名
*/
@SneakyThrows
public void deleteObject(String bucketName, String objectName) {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 上傳分片文件
*
* @param inputStream 流
* @param objectName 存入桶中的對(duì)象名
* @param bucketName 桶名
* @return ObjectWriteResponse
*/
@SneakyThrows
public OssFile putChunkObject(InputStream inputStream, String bucketName, String objectName) {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build());
return new OssFile(objectName, objectName);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* 返回臨時(shí)帶簽名、Get請(qǐng)求方式的訪問URL
*
* @param bucketName 桶名
* @param filePath Oss文件路徑
* @return 臨時(shí)帶簽名、Get請(qǐng)求方式的訪問URL
*/
@SneakyThrows
public String getPresignedObjectUrl(String bucketName, String filePath) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(filePath)
.build());
}
/**
* 返回臨時(shí)帶簽名、過期時(shí)間為1天的PUT請(qǐng)求方式的訪問URL
*
* @param bucketName 桶名
* @param filePath Oss文件路徑
* @param queryParams 查詢參數(shù)
* @return 臨時(shí)帶簽名、過期時(shí)間為1天的PUT請(qǐng)求方式的訪問URL
*/
@SneakyThrows
public String getPresignedObjectUrl(String bucketName, String filePath, Map<String, String> queryParams) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucketName)
.object(filePath)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(queryParams)
.build());
}
/**
* GetObject接口用于獲取某個(gè)文件(Object)。此操作需要對(duì)此Object具有讀權(quán)限。
*
* @param bucketName 桶名
* @param objectName 文件路徑
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
return minioClient.getObject(
GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
/**
* 查詢桶的對(duì)象信息
*
* @param bucketName 桶名
* @param recursive 是否遞歸查詢
* @return 桶的對(duì)象信息
*/
@SneakyThrows
public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
return minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
}
/**
* 獲取帶簽名的臨時(shí)上傳元數(shù)據(jù)對(duì)象,前端可獲取后,直接上傳到Minio
*
* @param bucketName 桶名稱
* @param fileName 文件名
* @return Map<String, String>
*/
@SneakyThrows
public Map<String, String> getPresignedPostFormData(String bucketName, String fileName) {
// 為存儲(chǔ)桶創(chuàng)建一個(gè)上傳策略,過期時(shí)間為7天
PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));
// 設(shè)置一個(gè)參數(shù)key,值為上傳對(duì)象的名稱
policy.addEqualsCondition("key", fileName);
// 添加Content-Type,例如以"image/"開頭,表示只能上傳照片,這里吃吃所有
policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);
// 設(shè)置上傳文件的大小 64kiB to 10MiB.
//policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
return minioClient.getPresignedPostFormData(policy);
}
public String generateFileInMinioName(String originalFilename) {
return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;
}
/**
* 初始化默認(rèn)存儲(chǔ)桶
*/
@PostConstruct
public void initDefaultBucket() {
String defaultBucketName = minioConfig.getBucketName();
if (bucketExists(defaultBucketName)) {
log.info("默認(rèn)存儲(chǔ)桶:defaultBucketName已存在");
} else {
log.info("創(chuàng)建默認(rèn)存儲(chǔ)桶:defaultBucketName");
makeBucket(minioConfig.getBucketName());
}
}
/**
* 文件合并,將分塊文件組成一個(gè)新的文件
*
* @param bucketName 合并文件生成文件所在的桶
* @param objectName 原始文件名
* @param sourceObjectList 分塊文件集合
* @return OssFile
*/
@SneakyThrows
public OssFile composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) {
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.sources(sourceObjectList)
.build());
String presignedObjectUrl = getPresignedObjectUrl(bucketName, objectName);
return new OssFile(presignedObjectUrl, objectName);
}
/**
* 文件合并,將分塊文件組成一個(gè)新的文件
*
* @param originBucketName 分塊文件所在的桶
* @param targetBucketName 合并文件生成文件所在的桶
* @param objectName 存儲(chǔ)于桶中的對(duì)象名
* @return OssFile
*/
@SneakyThrows
public OssFile composeObject(String originBucketName, String targetBucketName, String objectName) {
Iterable<Result<Item>> results = listObjects(originBucketName, true);
List<String> objectNameList = new ArrayList<>();
for (Result<Item> result : results) {
Item item = result.get();
objectNameList.add(item.objectName());
}
if (ObjectUtils.isEmpty(objectNameList)) {
throw new IllegalArgumentException(originBucketName + "桶中沒有文件,請(qǐng)檢查");
}
List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
// 對(duì)文件名集合進(jìn)行升序排序
objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);
for (String object : objectNameList) {
composeSourceList.add(ComposeSource.builder()
.bucket(originBucketName)
.object(object)
.build());
}
return composeObject(composeSourceList, targetBucketName, objectName);
}
}
MinioConfig
import ai.gantong.common.constant.CommonConstant;
import ai.gantong.common.constant.SymbolConstant;
import ai.gantong.common.util.MinioUtil;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Minio文件上傳配置文件
*
* @author xiaoyi
*/
@Slf4j
@Configuration
public class MinioConfig {
@Value(value = "${minio.minioUrl}")
private String minioUrl;
@Value(value = "${minio.minioName}")
private String minioName;
@Value(value = "${minio.minioPass}")
private String minioPass;
@Value(value = "${minio.bucketName}")
private String bucketName;
public String getBucketName() {
return bucketName;
}
@Bean
public void initMinio() {
MinioUtil.setMinioUrl(minioUrl);
MinioUtil.setMinioName(minioName);
MinioUtil.setMinioPass(minioPass);
MinioUtil.setBucketName(bucketName);
}
// 將 MinIOClient 注入到 Spring 上下文中
@Bean("minioClient")
public MinioClient minioClient() {
return MinioClient.builder().endpoint(minioUrl).credentials(minioName, minioPass).region(region).build();
}
// 初始化MinioTemplate,封裝了一些MinIOClient的基本操作
@Bean(name = "minioTemplate")
public MinioTemplate minioTemplate() {
return new MinioTemplate(minioClient(), this);
}
}
Controller
/**
* 根據(jù)文件大小和文件的md5校驗(yàn)文件是否存在, 實(shí)現(xiàn)秒傳接口
*
* @param md5 文件的md5
* @return 操作是否成功
*/
@ApiOperation(value = "極速秒傳接口")
@GetMapping(value = "/fastUpload")
public Result<String> checkFileExists(@ApiParam(value = "文件的md5") String md5) {
return fileService.checkFileExists(md5);
}
/**
* 大文件分片上傳
*
* @param md5 文件的md5
* @param file 文件
* @param fileName 文件名
* @param index 分片索引
* @return 分片執(zhí)行結(jié)果
*/
@ApiOperation(value = "上傳分片的接口")
@PostMapping(value = "/upload")
public Result<String> upload(@ApiParam(value = "文件的md5") String md5, @ApiParam(value = "文件") MultipartFile file,
@ApiParam(value = "文件名") String fileName, @ApiParam(value = "分片索引") Integer index) {
return fileService.upload(md5, file, fileName, index);
}
/**
* 大文件合并
*
* @param mergeInfo 合并信息
* @return 分片合并的狀態(tài)
*/
@ApiOperation(value = "合并分片的接口")
@PostMapping(value = "/merge")
public Result<String> merge(@RequestBody MergeInfo mergeInfo) {
return fileService.merge(mergeInfo);
}ServiceImpl
@Slf4j
@Service
public class FileServiceImpl implements IFileService {
private static final String MD5_KEY = "自定義前綴:minio:file:md5List";
@Resource
private MinioClient minioClient;
@Resource
private MinioConfig minioConfig;
@Resource
private MinioTemplate minioTemplate;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public Result<String> checkFileExists(String md5) {
Result<String> result = new Result<>();
// 先從Redis中查詢
String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(md5);
// 文件不存在
if (StrUtil.isEmpty(url)) {
result.setSuccess(false);
result.setMessage("資源不存在");
} else {
// 文件已經(jīng)存在了
result.setSuccess(true);
result.setResult(url);
result.setMessage("極速秒傳成功");
}
return result;
}
@Override
public Result<String> upload(String md5, MultipartFile file, String fileName, Integer index) {
// 上傳過程中出現(xiàn)異常
Assert.notNull(file, "文件上傳異常=>文件不能為空!");
// 創(chuàng)建文件桶
minioTemplate.makeBucket(md5);
String objectName = String.valueOf(index);
try {
// 上傳文件
minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
// 設(shè)置上傳分片的狀態(tài)
return Result.ok("文件上傳成功!");
} catch (Exception e) {
e.printStackTrace();
return Result.error("文件上傳失敗!");
}
}
@Override
public Result<String> merge(MergeInfo mergeInfo) {
Assert.notNull(mergeInfo, "mergeInfo不能為空!");
String md5 = mergeInfo.getMd5();
String fileType = mergeInfo.getFileType();
try {
// 開始合并請(qǐng)求
String targetBucketName = minioConfig.getBucketName();
String fileNameWithoutExtension = UUID.randomUUID().toString();
String objectName = fileNameWithoutExtension + "." + fileType;
// 合并文件
minioTemplate.composeObject(md5, targetBucketName, objectName);
log.info("桶:{} 中的分片文件,已經(jīng)在桶:{},文件 {} 合并成功", md5, targetBucketName, objectName);
// 合并成功之后刪除對(duì)應(yīng)的臨時(shí)桶
minioTemplate.removeBucket(md5, true);
log.info("刪除桶 {} 成功", md5);
// 表示是同一個(gè)文件, 且文件后綴名沒有被修改過
String url = minioTemplate.getPresignedObjectUrl(targetBucketName, objectName);
// 存入redis中
redisTemplate.boundHashOps(MD5_KEY).put(md5, url);
return Result.ok("文件合并成功");// 成功
} catch (Exception e) {
log.error("文件合并執(zhí)行異常 => ", e);
return Result.error("文件合并異常");// 失敗
}
}
}MergeInfo
@Data
@ApiModel(description = "大文件合并信息")
public class MergeInfo implements Serializable {
@ApiModelProperty(value = "文件的md5")
public String md5;
@ApiModelProperty(value = "文件名")
public String fileName;
@ApiModelProperty(value = "文件類型")
public String fileType;
}OssFile
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OssFile {
/**
* OSS 存儲(chǔ)時(shí)文件路徑
*/
private String ossFilePath;
/**
* 原始文件名
*/
private String originalFileName;
}到此這篇關(guān)于Springboot2.7+Minio8 實(shí)現(xiàn)大文件分片上傳的文章就介紹到這了,更多相關(guān)SpringBoot 大文件分片上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Spring Boot應(yīng)用ApplicationEvent案例場(chǎng)景
這篇文章主要介紹了基于Spring Boot應(yīng)用ApplicationEvent,利用Spring的機(jī)制發(fā)布ApplicationEvent和監(jiān)聽ApplicationEvent,需要的朋友可以參考下2023-03-03
SpringBoot集成pf4j實(shí)現(xiàn)插件開發(fā)功能的代碼示例
pf4j是一個(gè)插件框架,用于實(shí)現(xiàn)插件的動(dòng)態(tài)加載,支持的插件格式(zip、jar),本文給大家介紹了SpringBoot集成pf4j實(shí)現(xiàn)插件開發(fā)功能的示例,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-07-07
SpringSecurity在分布式環(huán)境下的使用流程分析
文章介紹了Spring?Security在分布式環(huán)境下的使用,包括單點(diǎn)登錄(SSO)的概念、流程圖以及JWT(JSON?Web?Token)的生成和校驗(yàn),通過使用JWT和RSA非對(duì)稱加密,可以實(shí)現(xiàn)安全的分布式認(rèn)證,感興趣的朋友一起看看吧2025-02-02
java 動(dòng)態(tài)加載的實(shí)現(xiàn)代碼
這篇文章主要介紹了java 動(dòng)態(tài)加載的實(shí)現(xiàn)代碼的相關(guān)資料,Java動(dòng)態(tài)加載類主要是為了不改變主程序代碼,通過修改配置文件就可以操作不同的對(duì)象執(zhí)行不同的功能,需要的朋友可以參考下2017-07-07
淺談collection標(biāo)簽的oftype屬性能否為java.util.Map
這篇文章主要介紹了collection標(biāo)簽的oftype屬性能否為java.util.Map,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
springboot單獨(dú)使用feign簡(jiǎn)化接口調(diào)用方式
這篇文章主要介紹了springboot單獨(dú)使用feign簡(jiǎn)化接口調(diào)用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03

