Springboot2.7+Minio8 實現(xiàn)大文件分片上傳
1. 介紹:
分片上傳: 將一個文件按照指定大小分割成多份數(shù)據(jù)塊(Part)分開上傳, 上傳之后再由服務(wù)端整合為原本的文件
分片上傳場景:
- 網(wǎng)絡(luò)環(huán)境差: 當(dāng)出現(xiàn)上傳失敗的時候,只需要對失敗的Part進(jìn)行重新上傳
- 斷點續(xù)傳: 中途暫停之后,可以從上次上傳完成的Part的位置繼續(xù)上傳
- 加速上傳: 要上傳到OSS的本地文件很大的時候,可以并行上傳多個Part以加快上傳速度
- 流式上傳: 可以在需要上傳的文件大小還不確定的情況下開始上傳,這種場景在視頻監(jiān)控等行業(yè)應(yīng)用中比較常見
- 文件較大: 一般文件比較大時,默認(rèn)情況下一般都會采用分片上傳
分片上傳流程:
- 將需要上傳的文件按照一定大小進(jìn)行分割(推薦1MB或者5MB),分割成相同大小的數(shù)據(jù)塊
- 初始化一個分片上傳任務(wù),返回本次分片上傳唯一標(biāo)識(md5)
- 按照一定的策略(串行或并行)發(fā)送各個分片數(shù)據(jù)塊
- 發(fā)送完成后,服務(wù)端根據(jù)判斷數(shù)據(jù)上傳是否完整,如果完整,則進(jìn)行數(shù)據(jù)塊合成得到原始文件。
2. 代碼部分:
application.yml
minio: minioUrl: http://ip地址:9000 # MinIO 服務(wù)地址-需要修改 minioName: 賬號 # MinIO 訪問密鑰-需要修改 minioPass: 密碼 # MinIO 秘鑰密碼-需要修改 bucketName: 桶名 # MinIO 桶名稱-需要修改 region: ap-southeast-1 # MinIO 存儲區(qū)域,可以指定為 "ap-southeast-1" spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB
pom.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; /** * 查詢所有存儲桶 * * @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)建存儲桶 * * @param bucketName 桶名 */ @SneakyThrows public void makeBucket(String bucketName) { if (!bucketExists(bucketName)) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } } /** * 刪除一個空桶 如果存儲桶存在對象不為空時,刪除會報錯。 * * @param bucketName 桶名 */ @SneakyThrows public void removeBucket(String bucketName) { removeBucket(bucketName, false); minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } /** * 刪除一個桶 根據(jù)桶是否存在數(shù)據(jù)進(jìn)行不同的刪除 * 桶為空時直接刪除 * 桶不為空時先刪除桶中的數(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(); } } } /** * 刪除桶中所有的對象 * * @param bucketName 桶對象 */ @SneakyThrows public void deleteBucketAllObject(String bucketName) { List<String> list = listObjectNames(bucketName); if (!list.isEmpty()) { for (String objectName : list) { deleteObject(bucketName, objectName); } } } /** * 查詢桶中所有的對象名 * * @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; } /** * 刪除一個對象 * * @param bucketName 桶名 * @param objectName 對象名 */ @SneakyThrows public void deleteObject(String bucketName, String objectName) { minioClient.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); } /** * 上傳分片文件 * * @param inputStream 流 * @param objectName 存入桶中的對象名 * @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(); } } } /** * 返回臨時帶簽名、Get請求方式的訪問URL * * @param bucketName 桶名 * @param filePath Oss文件路徑 * @return 臨時帶簽名、Get請求方式的訪問URL */ @SneakyThrows public String getPresignedObjectUrl(String bucketName, String filePath) { return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(filePath) .build()); } /** * 返回臨時帶簽名、過期時間為1天的PUT請求方式的訪問URL * * @param bucketName 桶名 * @param filePath Oss文件路徑 * @param queryParams 查詢參數(shù) * @return 臨時帶簽名、過期時間為1天的PUT請求方式的訪問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接口用于獲取某個文件(Object)。此操作需要對此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()); } /** * 查詢桶的對象信息 * * @param bucketName 桶名 * @param recursive 是否遞歸查詢 * @return 桶的對象信息 */ @SneakyThrows public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) { return minioClient.listObjects( ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build()); } /** * 獲取帶簽名的臨時上傳元數(shù)據(jù)對象,前端可獲取后,直接上傳到Minio * * @param bucketName 桶名稱 * @param fileName 文件名 * @return Map<String, String> */ @SneakyThrows public Map<String, String> getPresignedPostFormData(String bucketName, String fileName) { // 為存儲桶創(chuàng)建一個上傳策略,過期時間為7天 PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1)); // 設(shè)置一個參數(shù)key,值為上傳對象的名稱 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)存儲桶 */ @PostConstruct public void initDefaultBucket() { String defaultBucketName = minioConfig.getBucketName(); if (bucketExists(defaultBucketName)) { log.info("默認(rèn)存儲桶:defaultBucketName已存在"); } else { log.info("創(chuàng)建默認(rèn)存儲桶:defaultBucketName"); makeBucket(minioConfig.getBucketName()); } } /** * 文件合并,將分塊文件組成一個新的文件 * * @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); } /** * 文件合并,將分塊文件組成一個新的文件 * * @param originBucketName 分塊文件所在的桶 * @param targetBucketName 合并文件生成文件所在的桶 * @param objectName 存儲于桶中的對象名 * @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 + "桶中沒有文件,請檢查"); } List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size()); // 對文件名集合進(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校驗文件是否存在, 實現(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 { // 開始合并請求 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); // 合并成功之后刪除對應(yīng)的臨時桶 minioTemplate.removeBucket(md5, true); log.info("刪除桶 {} 成功", md5); // 表示是同一個文件, 且文件后綴名沒有被修改過 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 存儲時文件路徑 */ private String ossFilePath; /** * 原始文件名 */ private String originalFileName; }
到此這篇關(guān)于Springboot2.7+Minio8 實現(xiàn)大文件分片上傳的文章就介紹到這了,更多相關(guān)SpringBoot 大文件分片上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Spring Boot應(yīng)用ApplicationEvent案例場景
這篇文章主要介紹了基于Spring Boot應(yīng)用ApplicationEvent,利用Spring的機(jī)制發(fā)布ApplicationEvent和監(jiān)聽ApplicationEvent,需要的朋友可以參考下2023-03-03SpringBoot集成pf4j實現(xiàn)插件開發(fā)功能的代碼示例
pf4j是一個插件框架,用于實現(xiàn)插件的動態(tài)加載,支持的插件格式(zip、jar),本文給大家介紹了SpringBoot集成pf4j實現(xiàn)插件開發(fā)功能的示例,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-07-07SpringSecurity在分布式環(huán)境下的使用流程分析
文章介紹了Spring?Security在分布式環(huán)境下的使用,包括單點登錄(SSO)的概念、流程圖以及JWT(JSON?Web?Token)的生成和校驗,通過使用JWT和RSA非對稱加密,可以實現(xiàn)安全的分布式認(rèn)證,感興趣的朋友一起看看吧2025-02-02淺談collection標(biāo)簽的oftype屬性能否為java.util.Map
這篇文章主要介紹了collection標(biāo)簽的oftype屬性能否為java.util.Map,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02springboot單獨(dú)使用feign簡化接口調(diào)用方式
這篇文章主要介紹了springboot單獨(dú)使用feign簡化接口調(diào)用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03