当前位置:   article > 正文

Springboot2.7 + Minio8 实现大文件分片上传_minio分片上传

minio分片上传

 1. 介绍:

分片上传: 将一个文件按照指定大小分割成多份数据块(Part)分开上传, 上传之后再由服务端整合为原本的文件

分片上传场景:

  1. 网络环境差: 当出现上传失败的时候,只需要对失败的Part进行重新上传
  2. 断点续传: 中途暂停之后,可以从上次上传完成的Part的位置继续上传
  3. 加速上传: 要上传到OSS的本地文件很大的时候,可以并行上传多个Part以加快上传速度
  4. 流式上传: 可以在需要上传的文件大小还不确定的情况下开始上传,这种场景在视频监控等行业应用中比较常见
  5. 文件较大: 一般文件比较大时,默认情况下一般都会采用分片上传

分片上传流程:

  1. 将需要上传的文件按照一定大小进行分割(推荐1MB或者5MB),分割成相同大小的数据块
  2. 初始化一个分片上传任务,返回本次分片上传唯一标识(md5)
  3. 按照一定的策略(串行或并行)发送各个分片数据块
  4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

 2. 代码部分:

application.yml

  1. minio:
  2. minioUrl: http://ip地址:9000 # MinIO 服务地址-需要修改
  3. minioName: 账号 # MinIO 访问密钥-需要修改
  4. minioPass: 密码 # MinIO 秘钥密码-需要修改
  5. bucketName: 桶名 # MinIO 桶名称-需要修改
  6. region: ap-southeast-1 # MinIO 存储区域,可以指定为 "ap-southeast-1"
  7. spring:
  8. servlet:
  9. multipart:
  10. max-file-size: 10MB
  11. max-request-size: 10MB

 pom.xml

  1. <!--minio-->
  2. <dependency>
  3. <groupId>io.minio</groupId>
  4. <artifactId>minio</artifactId>
  5. <version>8.0.3</version>
  6. </dependency>

MinioTemplate

  1. /**
  2. * @author xiaoyi
  3. */
  4. @Slf4j
  5. @AllArgsConstructor
  6. public class MinioTemplate {
  7. /**
  8. * MinIO 客户端
  9. */
  10. private final MinioClient minioClient;
  11. /**
  12. * MinIO 配置类
  13. */
  14. private final MinioConfig minioConfig;
  15. /**
  16. * 查询所有存储桶
  17. *
  18. * @return Bucket 集合
  19. */
  20. @SneakyThrows
  21. public List<Bucket> listBuckets() {
  22. return minioClient.listBuckets();
  23. }
  24. /**
  25. * 查询文件大小
  26. *
  27. * @return Bucket 集合
  28. */
  29. @SneakyThrows
  30. public Long getObjectSize(String bucketName, String objectName) {
  31. return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()).size();
  32. }
  33. /**
  34. * 桶是否存在
  35. *
  36. * @param bucketName 桶名
  37. * @return 是否存在
  38. */
  39. @SneakyThrows
  40. public boolean bucketExists(String bucketName) {
  41. return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
  42. }
  43. /**
  44. * 创建存储桶
  45. *
  46. * @param bucketName 桶名
  47. */
  48. @SneakyThrows
  49. public void makeBucket(String bucketName) {
  50. if (!bucketExists(bucketName)) {
  51. minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
  52. }
  53. }
  54. /**
  55. * 删除一个空桶 如果存储桶存在对象不为空时,删除会报错。
  56. *
  57. * @param bucketName 桶名
  58. */
  59. @SneakyThrows
  60. public void removeBucket(String bucketName) {
  61. removeBucket(bucketName, false);
  62. minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
  63. }
  64. /**
  65. * 删除一个桶 根据桶是否存在数据进行不同的删除
  66. * 桶为空时直接删除
  67. * 桶不为空时先删除桶中的数据,然后再删除桶
  68. *
  69. * @param bucketName 桶名
  70. */
  71. @SneakyThrows
  72. public void removeBucket(String bucketName, boolean bucketNotNull) {
  73. if (bucketNotNull) {
  74. deleteBucketAllObject(bucketName);
  75. }
  76. minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
  77. }
  78. /**
  79. * 上传文件
  80. *
  81. * @param inputStream 流
  82. * @param originalFileName 原始文件名
  83. * @param bucketName 桶名
  84. * @return ObjectWriteResponse
  85. */
  86. @SneakyThrows
  87. public OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {
  88. String uuidFileName = generateFileInMinioName(originalFileName);
  89. try {
  90. if (ObjectUtils.isEmpty(bucketName)) {
  91. bucketName = minioConfig.getBucketName();
  92. }
  93. minioClient.putObject(
  94. PutObjectArgs.builder()
  95. .bucket(bucketName)
  96. .object(uuidFileName)
  97. .stream(inputStream, inputStream.available(), -1)
  98. .build());
  99. return new OssFile(uuidFileName, originalFileName);
  100. } finally {
  101. if (inputStream != null) {
  102. inputStream.close();
  103. }
  104. }
  105. }
  106. /**
  107. * 删除桶中所有的对象
  108. *
  109. * @param bucketName 桶对象
  110. */
  111. @SneakyThrows
  112. public void deleteBucketAllObject(String bucketName) {
  113. List<String> list = listObjectNames(bucketName);
  114. if (!list.isEmpty()) {
  115. for (String objectName : list) {
  116. deleteObject(bucketName, objectName);
  117. }
  118. }
  119. }
  120. /**
  121. * 查询桶中所有的对象名
  122. *
  123. * @param bucketName 桶名
  124. * @return objectNames
  125. */
  126. @SneakyThrows
  127. public List<String> listObjectNames(String bucketName) {
  128. List<String> objectNameList = new ArrayList<>();
  129. if (bucketExists(bucketName)) {
  130. Iterable<Result<Item>> results = listObjects(bucketName, true);
  131. for (Result<Item> result : results) {
  132. String objectName = result.get().objectName();
  133. objectNameList.add(objectName);
  134. }
  135. }
  136. return objectNameList;
  137. }
  138. /**
  139. * 删除一个对象
  140. *
  141. * @param bucketName 桶名
  142. * @param objectName 对象名
  143. */
  144. @SneakyThrows
  145. public void deleteObject(String bucketName, String objectName) {
  146. minioClient.removeObject(RemoveObjectArgs.builder()
  147. .bucket(bucketName)
  148. .object(objectName)
  149. .build());
  150. }
  151. /**
  152. * 上传分片文件
  153. *
  154. * @param inputStream 流
  155. * @param objectName 存入桶中的对象名
  156. * @param bucketName 桶名
  157. * @return ObjectWriteResponse
  158. */
  159. @SneakyThrows
  160. public OssFile putChunkObject(InputStream inputStream, String bucketName, String objectName) {
  161. try {
  162. minioClient.putObject(
  163. PutObjectArgs.builder()
  164. .bucket(bucketName)
  165. .object(objectName)
  166. .stream(inputStream, inputStream.available(), -1)
  167. .build());
  168. return new OssFile(objectName, objectName);
  169. } finally {
  170. if (inputStream != null) {
  171. inputStream.close();
  172. }
  173. }
  174. }
  175. /**
  176. * 返回临时带签名、Get请求方式的访问URL
  177. *
  178. * @param bucketName 桶名
  179. * @param filePath Oss文件路径
  180. * @return 临时带签名、Get请求方式的访问URL
  181. */
  182. @SneakyThrows
  183. public String getPresignedObjectUrl(String bucketName, String filePath) {
  184. return minioClient.getPresignedObjectUrl(
  185. GetPresignedObjectUrlArgs.builder()
  186. .method(Method.GET)
  187. .bucket(bucketName)
  188. .object(filePath)
  189. .build());
  190. }
  191. /**
  192. * 返回临时带签名、过期时间为1天的PUT请求方式的访问URL
  193. *
  194. * @param bucketName 桶名
  195. * @param filePath Oss文件路径
  196. * @param queryParams 查询参数
  197. * @return 临时带签名、过期时间为1天的PUT请求方式的访问URL
  198. */
  199. @SneakyThrows
  200. public String getPresignedObjectUrl(String bucketName, String filePath, Map<String, String> queryParams) {
  201. return minioClient.getPresignedObjectUrl(
  202. GetPresignedObjectUrlArgs.builder()
  203. .method(Method.PUT)
  204. .bucket(bucketName)
  205. .object(filePath)
  206. .expiry(1, TimeUnit.DAYS)
  207. .extraQueryParams(queryParams)
  208. .build());
  209. }
  210. /**
  211. * GetObject接口用于获取某个文件(Object)。此操作需要对此Object具有读权限。
  212. *
  213. * @param bucketName 桶名
  214. * @param objectName 文件路径
  215. */
  216. @SneakyThrows
  217. public InputStream getObject(String bucketName, String objectName) {
  218. return minioClient.getObject(
  219. GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
  220. }
  221. /**
  222. * 查询桶的对象信息
  223. *
  224. * @param bucketName 桶名
  225. * @param recursive 是否递归查询
  226. * @return 桶的对象信息
  227. */
  228. @SneakyThrows
  229. public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
  230. return minioClient.listObjects(
  231. ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
  232. }
  233. /**
  234. * 获取带签名的临时上传元数据对象,前端可获取后,直接上传到Minio
  235. *
  236. * @param bucketName 桶名称
  237. * @param fileName 文件名
  238. * @return Map<String, String>
  239. */
  240. @SneakyThrows
  241. public Map<String, String> getPresignedPostFormData(String bucketName, String fileName) {
  242. // 为存储桶创建一个上传策略,过期时间为7天
  243. PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));
  244. // 设置一个参数key,值为上传对象的名称
  245. policy.addEqualsCondition("key", fileName);
  246. // 添加Content-Type,例如以"image/"开头,表示只能上传照片,这里吃吃所有
  247. policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);
  248. // 设置上传文件的大小 64kiB to 10MiB.
  249. //policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
  250. return minioClient.getPresignedPostFormData(policy);
  251. }
  252. public String generateFileInMinioName(String originalFilename) {
  253. return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;
  254. }
  255. /**
  256. * 初始化默认存储桶
  257. */
  258. @PostConstruct
  259. public void initDefaultBucket() {
  260. String defaultBucketName = minioConfig.getBucketName();
  261. if (bucketExists(defaultBucketName)) {
  262. log.info("默认存储桶:defaultBucketName已存在");
  263. } else {
  264. log.info("创建默认存储桶:defaultBucketName");
  265. makeBucket(minioConfig.getBucketName());
  266. }
  267. }
  268. /**
  269. * 文件合并,将分块文件组成一个新的文件
  270. *
  271. * @param bucketName 合并文件生成文件所在的桶
  272. * @param objectName 原始文件名
  273. * @param sourceObjectList 分块文件集合
  274. * @return OssFile
  275. */
  276. @SneakyThrows
  277. public OssFile composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) {
  278. minioClient.composeObject(ComposeObjectArgs.builder()
  279. .bucket(bucketName)
  280. .object(objectName)
  281. .sources(sourceObjectList)
  282. .build());
  283. String presignedObjectUrl = getPresignedObjectUrl(bucketName, objectName);
  284. return new OssFile(presignedObjectUrl, objectName);
  285. }
  286. /**
  287. * 文件合并,将分块文件组成一个新的文件
  288. *
  289. * @param originBucketName 分块文件所在的桶
  290. * @param targetBucketName 合并文件生成文件所在的桶
  291. * @param objectName 存储于桶中的对象名
  292. * @return OssFile
  293. */
  294. @SneakyThrows
  295. public OssFile composeObject(String originBucketName, String targetBucketName, String objectName) {
  296. Iterable<Result<Item>> results = listObjects(originBucketName, true);
  297. List<String> objectNameList = new ArrayList<>();
  298. for (Result<Item> result : results) {
  299. Item item = result.get();
  300. objectNameList.add(item.objectName());
  301. }
  302. if (ObjectUtils.isEmpty(objectNameList)) {
  303. throw new IllegalArgumentException(originBucketName + "桶中没有文件,请检查");
  304. }
  305. List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
  306. // 对文件名集合进行升序排序
  307. objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);
  308. for (String object : objectNameList) {
  309. composeSourceList.add(ComposeSource.builder()
  310. .bucket(originBucketName)
  311. .object(object)
  312. .build());
  313. }
  314. return composeObject(composeSourceList, targetBucketName, objectName);
  315. }
  316. }

 MinioConfig

  1. import ai.gantong.common.constant.CommonConstant;
  2. import ai.gantong.common.constant.SymbolConstant;
  3. import ai.gantong.common.util.MinioUtil;
  4. import io.minio.MinioClient;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. /**
  10. * Minio文件上传配置文件
  11. *
  12. * @author xiaoyi
  13. */
  14. @Slf4j
  15. @Configuration
  16. public class MinioConfig {
  17. @Value(value = "${minio.minioUrl}")
  18. private String minioUrl;
  19. @Value(value = "${minio.minioName}")
  20. private String minioName;
  21. @Value(value = "${minio.minioPass}")
  22. private String minioPass;
  23. @Value(value = "${minio.bucketName}")
  24. private String bucketName;
  25. public String getBucketName() {
  26. return bucketName;
  27. }
  28. @Bean
  29. public void initMinio() {
  30. MinioUtil.setMinioUrl(minioUrl);
  31. MinioUtil.setMinioName(minioName);
  32. MinioUtil.setMinioPass(minioPass);
  33. MinioUtil.setBucketName(bucketName);
  34. }
  35. // 将 MinIOClient 注入到 Spring 上下文中
  36. @Bean("minioClient")
  37. public MinioClient minioClient() {
  38. return MinioClient.builder().endpoint(minioUrl).credentials(minioName, minioPass).region(region).build();
  39. }
  40. // 初始化MinioTemplate,封装了一些MinIOClient的基本操作
  41. @Bean(name = "minioTemplate")
  42. public MinioTemplate minioTemplate() {
  43. return new MinioTemplate(minioClient(), this);
  44. }
  45. }

Controller

  1. /**
  2. * 根据文件大小和文件的md5校验文件是否存在, 实现秒传接口
  3. *
  4. * @param md5 文件的md5
  5. * @return 操作是否成功
  6. */
  7. @ApiOperation(value = "极速秒传接口")
  8. @GetMapping(value = "/fastUpload")
  9. public Result<String> checkFileExists(@ApiParam(value = "文件的md5") String md5) {
  10. return fileService.checkFileExists(md5);
  11. }
  12. /**
  13. * 大文件分片上传
  14. *
  15. * @param md5 文件的md5
  16. * @param file 文件
  17. * @param fileName 文件名
  18. * @param index 分片索引
  19. * @return 分片执行结果
  20. */
  21. @ApiOperation(value = "上传分片的接口")
  22. @PostMapping(value = "/upload")
  23. public Result<String> upload(@ApiParam(value = "文件的md5") String md5, @ApiParam(value = "文件") MultipartFile file,
  24. @ApiParam(value = "文件名") String fileName, @ApiParam(value = "分片索引") Integer index) {
  25. return fileService.upload(md5, file, fileName, index);
  26. }
  27. /**
  28. * 大文件合并
  29. *
  30. * @param mergeInfo 合并信息
  31. * @return 分片合并的状态
  32. */
  33. @ApiOperation(value = "合并分片的接口")
  34. @PostMapping(value = "/merge")
  35. public Result<String> merge(@RequestBody MergeInfo mergeInfo) {
  36. return fileService.merge(mergeInfo);
  37. }

 ServiceImpl

  1. @Slf4j
  2. @Service
  3. public class FileServiceImpl implements IFileService {
  4. private static final String MD5_KEY = "自定义前缀:minio:file:md5List";
  5. @Resource
  6. private MinioClient minioClient;
  7. @Resource
  8. private MinioConfig minioConfig;
  9. @Resource
  10. private MinioTemplate minioTemplate;
  11. @Resource
  12. private RedisTemplate<String, Object> redisTemplate;
  13. @Override
  14. public Result<String> checkFileExists(String md5) {
  15. Result<String> result = new Result<>();
  16. // 先从Redis中查询
  17. String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(md5);
  18. // 文件不存在
  19. if (StrUtil.isEmpty(url)) {
  20. result.setSuccess(false);
  21. result.setMessage("资源不存在");
  22. } else {
  23. // 文件已经存在了
  24. result.setSuccess(true);
  25. result.setResult(url);
  26. result.setMessage("极速秒传成功");
  27. }
  28. return result;
  29. }
  30. @Override
  31. public Result<String> upload(String md5, MultipartFile file, String fileName, Integer index) {
  32. // 上传过程中出现异常
  33. Assert.notNull(file, "文件上传异常=>文件不能为空!");
  34. // 创建文件桶
  35. minioTemplate.makeBucket(md5);
  36. String objectName = String.valueOf(index);
  37. try {
  38. // 上传文件
  39. minioTemplate.putChunkObject(file.getInputStream(), md5, objectName);
  40. // 设置上传分片的状态
  41. return Result.ok("文件上传成功!");
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. return Result.error("文件上传失败!");
  45. }
  46. }
  47. @Override
  48. public Result<String> merge(MergeInfo mergeInfo) {
  49. Assert.notNull(mergeInfo, "mergeInfo不能为空!");
  50. String md5 = mergeInfo.getMd5();
  51. String fileType = mergeInfo.getFileType();
  52. try {
  53. // 开始合并请求
  54. String targetBucketName = minioConfig.getBucketName();
  55. String fileNameWithoutExtension = UUID.randomUUID().toString();
  56. String objectName = fileNameWithoutExtension + "." + fileType;
  57. // 合并文件
  58. minioTemplate.composeObject(md5, targetBucketName, objectName);
  59. log.info("桶:{} 中的分片文件,已经在桶:{},文件 {} 合并成功", md5, targetBucketName, objectName);
  60. // 合并成功之后删除对应的临时桶
  61. minioTemplate.removeBucket(md5, true);
  62. log.info("删除桶 {} 成功", md5);
  63. // 表示是同一个文件, 且文件后缀名没有被修改过
  64. String url = minioTemplate.getPresignedObjectUrl(targetBucketName, objectName);
  65. // 存入redis中
  66. redisTemplate.boundHashOps(MD5_KEY).put(md5, url);
  67. return Result.ok("文件合并成功");// 成功
  68. } catch (Exception e) {
  69. log.error("文件合并执行异常 => ", e);
  70. return Result.error("文件合并异常");// 失败
  71. }
  72. }
  73. }

MergeInfo

  1. @Data
  2. @ApiModel(description = "大文件合并信息")
  3. public class MergeInfo implements Serializable {
  4. @ApiModelProperty(value = "文件的md5")
  5. public String md5;
  6. @ApiModelProperty(value = "文件名")
  7. public String fileName;
  8. @ApiModelProperty(value = "文件类型")
  9. public String fileType;
  10. }

OssFile

  1. @Data
  2. @NoArgsConstructor
  3. @AllArgsConstructor
  4. public class OssFile {
  5. /**
  6. * OSS 存储时文件路径
  7. */
  8. private String ossFilePath;
  9. /**
  10. * 原始文件名
  11. */
  12. private String originalFileName;
  13. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/873390
推荐阅读
相关标签
  

闽ICP备14008679号