当前位置:   article > 正文

java文件上传Minio——文件、文件分片上传_java minio

java minio

本文主要介绍了文件上传——单\多文件分片上传;当然还有文件的md5值秒传、文件断点续传等内容,本文结合minio文件服务和redis做计数来实现的场景(下方地址是单机情况,即上传至本地demo)。有什么问题可以在留言哦!并在文章末尾附上demo源码下载!

 大文件分片上传博文地址:大文件分片上传

大文件(md5分片)上传博文地址:大文件(md5分片)上传

大文件断点续传博文地址:大文件断点续传 

一、创建前后端项目及演示

创建前后端项目,采用springboot项目结构,前端采用的vue框架搭建(前端完全是边用边百度,这里就不附前面项目结构,就一个页面)

后端控制台的输出日志

minio文件服务上面的结果

关于minio的配置与基本使用:(37条消息) minio的基本使用——java_java使用minio_寒夜憨憨的博客-CSDN博客

二、文件上传minio核心代码 

其中包括了单文件上传minio和多文件分片上传minio;至于文件分片上传采用的是:先保存分片文件——>然后合并分片文件——>再删除分片文件;其实minio有自带的文件分片上传策略。

文件上传至minio,如果单纯的保存文件的话,其实可以直接采用前端去对接minio,然后当上传成功之后,返回上传至minio文件的相关信息给后端进行数据存储处理即可;这样上传文件就不要经过后端服务器了,减少数据处理环节。

1、minio配置信息核心代码

在代码的注释都详细说明了每一步是做什么的,如果不懂的可以详细看看每一步的注释,当然也可以留言!

  1. package com.jdh.fileUpload.config;
  2. import io.minio.*;
  3. import io.minio.errors.*;
  4. import io.minio.messages.Item;
  5. import lombok.*;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10. import org.springframework.http.MediaType;
  11. import org.springframework.mock.web.MockMultipartFile;
  12. import org.springframework.util.StringUtils;
  13. import org.springframework.web.multipart.MultipartFile;
  14. import io.minio.MinioClient;
  15. import javax.annotation.Resource;
  16. import java.io.*;
  17. import java.util.Objects;
  18. import java.util.UUID;
  19. /**
  20. * @ClassName: minioConfig
  21. * @Author: jdh
  22. * @CreateTime: 2022-03-12
  23. * @Description:
  24. */
  25. @Slf4j
  26. @Data
  27. @Configuration
  28. @ConditionalOnProperty(prefix = "autoconfigure", value = "isMinio", matchIfMissing = false)
  29. public class MinioUtil {
  30. private MinioClient minioClient;
  31. @Resource
  32. private MinioProperties minioProperties;
  33. // @Autowired
  34. // private MyMinioClient myMinioClient;
  35. /**
  36. * 获取一个连接minio服务端的客户端
  37. *
  38. * @return MinioClient
  39. */
  40. @Bean
  41. public MinioClient getMinioClient() {
  42. try {
  43. String url = "http:" + minioProperties.getIp() + ":" + minioProperties.getPort();
  44. MinioClient minioClient = MinioClient.builder()
  45. .endpoint(url) //两种都可以,这种全路径的其实就是下面分开配置一样的
  46. // .endpoint(minioProperties.getIp(),minioProperties.getPort(),minioProperties.getSecure())
  47. .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
  48. .build();
  49. this.minioClient = minioClient;
  50. return minioClient;
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. log.info("创建minio客户端失败!", e);
  54. return null;
  55. }
  56. }
  57. /**
  58. * 创建桶
  59. *
  60. * @param bucketName 桶名称
  61. */
  62. public Boolean createBucket(String bucketName) throws Exception {
  63. if (!StringUtils.hasLength(bucketName)) {
  64. throw new RuntimeException("创建桶的时候,桶名不能为空!");
  65. }
  66. try {
  67. minioClient.makeBucket(MakeBucketArgs.builder()
  68. .bucket(bucketName)
  69. .build());
  70. return true;
  71. } catch (Exception e) {
  72. log.info("创建桶失败!", e);
  73. return false;
  74. }
  75. }
  76. /**
  77. * 检查桶是否存在
  78. *
  79. * @param bucketName 桶名称
  80. * @return boolean true-存在 false-不存在
  81. */
  82. public boolean checkBucketExist(String bucketName) throws Exception {
  83. if (!StringUtils.hasLength(bucketName)) {
  84. throw new RuntimeException("检测桶的时候,桶名不能为空!");
  85. }
  86. return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
  87. }
  88. /**
  89. * 检测某个桶内是否存在某个文件
  90. *
  91. * @param objectName 文件名称
  92. * @param bucketName 桶名称
  93. */
  94. public boolean getBucketFileExist(String objectName, String bucketName) throws Exception {
  95. if (!StringUtils.hasLength(objectName) || !StringUtils.hasLength(bucketName)) {
  96. throw new RuntimeException("检测文件的时候,文件名和桶名不能为空!");
  97. }
  98. try {
  99. // 判断文件是否存在
  100. return (minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()) &&
  101. minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()) != null);
  102. } catch (ErrorResponseException e) {
  103. log.info("文件不存在 ! Object does not exist");
  104. return false;
  105. } catch (Exception e) {
  106. throw new Exception(e);
  107. }
  108. }
  109. /**
  110. * 删除文件夹
  111. *
  112. * @param bucketName 桶名
  113. * @param objectName 文件夹名
  114. * @param isDeep 是否递归删除
  115. * @return
  116. */
  117. public Boolean deleteBucketFolder(String bucketName, String objectName, Boolean isDeep) {
  118. if (!StringUtils.hasLength(bucketName) || !StringUtils.hasLength(objectName)) {
  119. throw new RuntimeException("删除文件夹的时候,桶名或文件名不能为空!");
  120. }
  121. try {
  122. ListObjectsArgs args = ListObjectsArgs.builder().bucket(bucketName).prefix(objectName + "/").recursive(isDeep).build();
  123. Iterable<Result<Item>> listObjects = minioClient.listObjects(args);
  124. listObjects.forEach(objectResult -> {
  125. try {
  126. Item item = objectResult.get();
  127. System.out.println(item.objectName());
  128. minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(item.objectName()).build());
  129. } catch (Exception e) {
  130. log.info("删除文件夹中的文件异常", e);
  131. }
  132. });
  133. return true;
  134. } catch (Exception e) {
  135. log.info("删除文件夹失败");
  136. return false;
  137. }
  138. }
  139. /**
  140. * 文件上传文件
  141. *
  142. * @param file 文件
  143. * @param bucketName 桶名
  144. * @param objectName 文件名,如果有文件夹则格式为 "文件夹名/文件名"
  145. * @return
  146. */
  147. public Boolean uploadFile(MultipartFile file, String bucketName, String objectName) {
  148. if (Objects.isNull(file) || Objects.isNull(bucketName)) {
  149. throw new RuntimeException("文件或者桶名参数不全!");
  150. }
  151. try {
  152. //资源的媒体类型
  153. String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//默认未知二进制流
  154. InputStream inputStream = file.getInputStream();
  155. PutObjectArgs args = PutObjectArgs.builder()
  156. .bucket(bucketName).object(objectName)
  157. .stream(inputStream, file.getSize(), -1)
  158. .contentType(file.getContentType())
  159. .build();
  160. ObjectWriteResponse response = minioClient.putObject(args);
  161. inputStream.close();
  162. return response.etag() != null;
  163. } catch (Exception e) {
  164. log.info("单文件上传失败!", e);
  165. return false;
  166. }
  167. }
  168. /**
  169. * 分片合并
  170. *
  171. * @param bucketName
  172. * @param folderName
  173. * @param objectName
  174. * @param partNum
  175. * @return
  176. */
  177. public Boolean uploadFileComplete(String bucketName, String folderName, String objectName, Integer partNum) {
  178. try {
  179. //获取临时文件下的所有文件信息
  180. Iterable<Result<Item>> listObjects = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(folderName + "/").build());
  181. //计算minio中分片个数
  182. Integer num = 0;
  183. for (Result<Item> result : listObjects) {
  184. num++;
  185. }
  186. //在依次校验实际分片数和预计分片数是否一致
  187. if (!num.equals(partNum)) {
  188. log.info("文件 {} 分片合并的时候,检测到实际分片数 {} 和预计分片数 {} 不一致", folderName, num, partNum);
  189. return false;
  190. }
  191. InputStream inputStream = null;
  192. log.info("开始合并文件 {} 分片合并,实际分片数 {} 和预计分片数 {}", folderName, num, partNum);
  193. for (int i = 0; i < num; i++) {
  194. String tempName = folderName + "/" + objectName.substring(0, objectName.lastIndexOf(".")) + "_" + i + ".temp";
  195. try {
  196. //获取分片文件流
  197. InputStream response = minioClient.getObject(
  198. GetObjectArgs.builder().bucket(bucketName).object(tempName).build());
  199. //流合并
  200. if (inputStream == null) {
  201. inputStream = response;
  202. } else {
  203. inputStream = new SequenceInputStream(inputStream, response);
  204. }
  205. } catch (Exception e) {
  206. log.info("读取分片文件失败!", e);
  207. }
  208. }
  209. if (inputStream == null) {
  210. log.info("合并流数据为空!");
  211. return false;
  212. }
  213. //转换为文件格式
  214. MockMultipartFile file = new MockMultipartFile(objectName, inputStream);
  215. //将合并的文件流写入到minio中
  216. PutObjectArgs args = PutObjectArgs.builder()
  217. .bucket(bucketName).object(objectName)
  218. .stream(file.getInputStream(), file.getSize(), -1)
  219. // .contentType(file.getContentType())//这里可以不知道类型
  220. .build();
  221. String etag = minioClient.putObject(args).etag();
  222. // 删除临时文件
  223. if (etag != null) {
  224. listObjects.forEach(objectResult -> {
  225. try {
  226. Item item = objectResult.get();
  227. minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(item.objectName()).build());
  228. } catch (Exception e) {
  229. log.info("删除文件夹中的文件异常", e);
  230. }
  231. });
  232. log.info("{}:临时文件夹文件已删除!", folderName);
  233. }
  234. inputStream.close();
  235. return etag != null;
  236. } catch (Exception e) {
  237. log.info("合并 {} - {} 文件失败!", folderName, objectName, e);
  238. return false;
  239. }
  240. }
  241. public static String generateUniqueId(int length) {
  242. UUID uuid = UUID.randomUUID();
  243. String hash = uuid.toString().replaceAll("-", "");
  244. return hash.substring(0, Math.min(length, hash.length()));
  245. }
  246. }

2、文件上传minio的实现核心代码

  1. package com.jdh.fileUpload.service.fileUpload;
  2. import com.alibaba.fastjson2.JSONObject;
  3. import com.jdh.fileUpload.config.MinioUtil;
  4. import com.jdh.fileUpload.utils.Result;
  5. import com.jdh.fileUpload.utils.Uuid;
  6. import com.jdh.fileUpload.vo.PartFileIndexVo;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.redisson.api.RedissonClient;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  11. import org.springframework.data.redis.core.RedisTemplate;
  12. import org.springframework.stereotype.Service;
  13. import org.springframework.web.multipart.MultipartFile;
  14. import javax.annotation.Resource;
  15. /**
  16. * @ClassName: FileUploadMinioServiceImpl
  17. * @Author: jdh
  18. * @CreateTime: 2022-03-15
  19. * @Description:
  20. */
  21. @Slf4j
  22. @Service
  23. @ConditionalOnProperty(prefix = "autoconfigure", value = "isMinio", matchIfMissing = false)
  24. public class FileUploadMinioServiceImpl implements FileUploadMinioService {
  25. @Autowired
  26. private MinioUtil minioUtil;
  27. @Resource
  28. private RedissonClient redissonClient;
  29. @Autowired
  30. private RedisTemplate<String, Object> redisTemplate;
  31. @Override
  32. public Result<Boolean> fileUploadMinio(MultipartFile file) {
  33. String objectName = Uuid.generateUniqueId(8) + "_" + file.getOriginalFilename();
  34. try {
  35. Boolean aBoolean = minioUtil.uploadFile(file, "minio-file", objectName);
  36. log.info("文件上传是否完成:{}", aBoolean);
  37. return Result.success(aBoolean, "文件上传完成!");
  38. } catch (Exception e) {
  39. log.info("文件上传异常!", e);
  40. return Result.error(false, "文件上传异常!");
  41. }
  42. }
  43. @Override
  44. public Result<Boolean> filePartUploadMinio(MultipartFile file, String partFile) {
  45. // String objectName = Uuid.generateUniqueId(8) + "_" + file.getOriginalFilename();
  46. try {
  47. PartFileIndexVo partFileIndexVo = JSONObject.parseObject(partFile, PartFileIndexVo.class);
  48. String objectName = "temp/" + partFileIndexVo.getFileUid() + "/"
  49. + partFileIndexVo.getFileName().substring(0, partFileIndexVo.getFileName().lastIndexOf(".")) + "_"
  50. + partFileIndexVo.getPartIndex() + ".temp";
  51. //上传文件分片
  52. Boolean partState = minioUtil.uploadFile(file, "minio-file", objectName);
  53. if (partState) {
  54. Long result = redisTemplate.opsForValue().increment("partCount:" + partFileIndexVo.getFileUid());
  55. if (result.equals(partFileIndexVo.getPartTotalNum().longValue())) {
  56. log.info("{}:开始合并分片请求...", partFileIndexVo.getFileName());
  57. //开始合并分片
  58. Boolean complete = minioUtil.uploadFileComplete("minio-file", "temp/" + partFileIndexVo.getFileUid(),
  59. partFileIndexVo.getFileName(), partFileIndexVo.getPartTotalNum());
  60. //移除文件分片上传情况,这个也应该在redis中
  61. Boolean delete = false;
  62. if (complete) {
  63. delete = redisTemplate.delete("partCount:" + partFileIndexVo.getFileUid());
  64. log.info("{}:分片文件合并完成!", partFileIndexVo.getFileName());
  65. }
  66. return Result.success(complete && delete, partFileIndexVo.getFileUid());
  67. }
  68. }
  69. return Result.success(partState, partFileIndexVo.getPartIndex().toString());
  70. } catch (Exception e) {
  71. log.info("文件上传异常!", e);
  72. return Result.error("文件上传异常!");
  73. }
  74. }
  75. }

3、关于相关前端核心代码

页面代码

  1. <!-- 单文件上传Minio -->
  2. <div class="singleFileUploadMinio">
  3. <el-upload ref="upload" name="files" action="#"
  4. :on-change="selectSingleFileMinio" :on-remove="removeSingleFileMinio" :file-list="singleFileMinio.fileList" :auto-upload="false">
  5. <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  6. <el-button style="margin-left: 10px;" size="small" type="success" @click="singleFileUploadMinio">点击进行单文件分片上传Minio</el-button>
  7. <div slot="tip" class="el-upload__tip">主要用于测试单文件上传Minio</div>
  8. </el-upload>
  9. </div>
  10. <!-- 多文件分片上传Minio -->
  11. <div class="multipleFilePartUploadMinio">
  12. <el-upload ref="upload" name="files" action="#"
  13. :on-change="selectMultiplePartFileMinio" :on-remove="removeMultiplePartFileMinio" :file-list="multipleFilePartMinio.fileList" :auto-upload="false">
  14. <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  15. <el-button style="margin-left: 10px;" size="small" type="success" @click="multipleFilePartUploadMinio">点击进行多文件分片上传Minio</el-button>
  16. <div slot="tip" class="el-upload__tip">主要用于测试多文件分片上传Minio</div>
  17. </el-upload>
  18. </div>

数据定义组件代码

  1. data() {
  2. return {
  3. urlPrefix: 'http://localhost:8082',
  4. headers: {
  5. 'Content-Type': 'multipart/form-data'
  6. },
  7. singleFileMinio: {
  8. file: '',
  9. fileList: []
  10. },
  11. multipleFilePartMinio: {
  12. fileList: [],
  13. fileData: []
  14. },
  15. };
  16. },

各方法代码

  1. methods: {
  2. // 下面是单文件上传Minio的一些方法
  3. selectSingleFileMinio(file, fileList){
  4. this.singleFileMinio.file = file
  5. this.singleFileMinio.fileList = []
  6. this.singleFileMinio.fileList.push(file)
  7. console.log("单文件上传Minio->选中的文件:",this.singleFileMinio.file)
  8. },
  9. removeSingleFileMinio(file, fileList){
  10. this.singleFileMinio.file = ''
  11. console.log("单文件上传Minio->移除选中的文件:",this.singleFileMinio.file)
  12. },
  13. singleFileUploadMinio(){
  14. let url = this.urlPrefix + "/minio/singleFileUploadMinio";
  15. var fileParam = new FormData();
  16. fileParam.append("file",this.singleFileMinio.file.raw);
  17. this.$upFile(url,fileParam,null,resp => {
  18. let res = resp.data;
  19. if (res.data){
  20. this.$message.success(res.msg)
  21. }else{
  22. this.$message.error(res.msg)
  23. }
  24. })
  25. },
  26. // 下面多文件分片上传Minio的一些方法
  27. selectMultiplePartFileMinio(file, fileList){
  28. this.multipleFilePartMinio.fileList.push(file)
  29. console.log("多文件分片上传->选中的文件:",this.multipleFilePartMinio.fileList)
  30. },
  31. removeMultiplePartFileMinio(file, fileList){
  32. this.multipleFilePartMinio.fileList = this.multipleFilePartMinio.fileList.filter(item => item.uid != file.uid);
  33. console.log("多文件分片上传->移除选中的文件:",this.multipleFilePartMinio.fileList)
  34. },
  35. multipleFilePartUploadMinio(){
  36. let fileUploadCount = 0;
  37. let url = this.urlPrefix + "/minio/partFileUploadMinio";
  38. let partSize = 8; //MB
  39. let partFileUid = [];
  40. for(let i = 0; i < this.multipleFilePartMinio.fileList.length; i++ ){
  41. let partNum = Math.ceil(this.multipleFilePartMinio.fileList[i].size / 1024 / 1024 / partSize)
  42. let partFileIndex = {
  43. fileName: this.multipleFilePartMinio.fileList[i].name,
  44. fileUid: this.multipleFilePartMinio.fileList[i].uid,
  45. partTotalNum: partNum,
  46. partIndex: ''
  47. }
  48. partFileUid.push(this.multipleFilePartMinio.fileList[i].uid + '')
  49. for(let j = 0; j < partNum; j++ ){
  50. partFileIndex.partIndex = j
  51. var fileParam = new FormData();
  52. fileParam.append('file', this.multipleFilePartMinio.fileList[i].raw.slice(j * 1024 * 1024 * partSize, (j + 1) * 1024 * 1024 * partSize))
  53. fileParam.append('partFileIndex', JSON.stringify(partFileIndex))
  54. this.$upFile(url,fileParam,null,resp => {
  55. let res = resp.data;
  56. //判断最后一个分片文件传完之后,后台检验文件上传情况
  57. if (res.code == 200){
  58. if(!res.data){
  59. var info = '当前分片上传Minio失败!uid:' + partFileUid + ';当前分片:' + j + ';返回分片:' + res.msg
  60. console.log(info)
  61. }
  62. let isUid = partFileUid.includes(res.msg)
  63. if(isUid){
  64. fileUploadCount ++;
  65. if(fileUploadCount == this.multipleFilePartMinio.fileList.length){
  66. console.log('多文件分片上传Minio完成')
  67. this.$message.success('多文件分片上传Minio完成')
  68. }
  69. }
  70. }else{
  71. var info = '多文件分片上传Minio失败!当前分片:' + j + ';当前uid:' + partFileUid
  72. console.log(info)
  73. this.$message.error(res.msg)
  74. }
  75. })
  76. }
  77. }
  78. }
  79. },

三、源码下载和结尾

源码默认只有上传本地的各种demo方案,如果需要打开上传至minio的话,需要再配置文件修改配置文件,如下:

gitee后端链接:java_fileUpload_demo: 关于文件上传的一些基本介绍(如文件上传、分片上传、md5值秒传、断点续传等)

gitee前端链接:https://gitee.com/java_utils_demo/vue2_demo.git

其实思路很简单也很明确,就是一个大文件分成n个小文件进行小文件依次单独上传,后端检测到所有分片上传完成之后,即对这些分片进行合并,并删除对应临时分片文件。

再次说明下,如果需要上传至minio,那么需要先配置好minio和redis并启动这两个服务,然后再启动demo项目。

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

闽ICP备14008679号