2.SpringBoot操作MinIO单文件上传

42 阅读18分钟

环境(pom.xml)

MinIO相关的maven包(核心)

<!--  MinIO相关   -->

<!-- 解决minio报错:okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.12</version>
</dependency>

其他辅助可能会用到的包

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.32</version>
    <scope>provided</scope>
</dependency>
<!--  国密  -->
<!--  要使用 SM 工具(中国商用密码算法)来计算并比较 InputStream 和 MultipartFile 的 MD5(实际上是 SM3 哈希值)  -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

<!--  hutool工具包:https://www.hutool.cn/docs/#/  -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.32</version>
</dependency>

<!-- Knife4j是基于springboot构建的一个文档生成工具,它可以让开发者为我们的应用生成API文档
        =>
        访问地址:项目根地址/doc.html => https://doc.xiaominfo.com/docs/quick-start -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.5.0</version>
</dependency>

<!-- 参数校验 -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

<!--  springWeb  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
         <!--去掉Jackson依赖,用fastjson -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--  fastjson:https://springdoc.cn/spring-boot-fastjson2/  -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension-spring6</artifactId>
    <version>2.0.52</version>
</dependency>
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.52</version>
</dependency>

application.yaml

MinIO配置部分(核心)

# minio配置 =》 参考:https://juejin.cn/post/7412486655862489114
minio:
  # 服务地址
  url: http://${dataResource.ip}:9010
  # 用户名
  access-key: ${dataResource.minio.accessKey}
  # 密码
  secret-key: ${dataResource.minio.password}
  # 桶对象
  bucket-name: upload-center
  #临时切片存放位置
  chunk-bucket-name: chunk-files

这里涉及到了我上一篇博客提到的配置,说明如下

  • url:MInIO对外暴露的接口服务地址
  • access-key:服务通行证里的accessKey字段,若没有配置则是你的Root用户名
  • secret-key:服务通行证里的secretKey字段,若没有配置则是你的Root密码
  • bucket-name:你想将文件统一上传到MInIO的哪个桶里
  • chunk-bucket-name:你想将切片文件统一上传到MInIO的哪个桶里

整个yaml配置为

server:
  port: 4190
#    context-path: /
spring:
  mvc:
    async:
      # 请求响应时间设置 6s
      request-timeout: 6000
  application:
    name: minio-upload-01
  servlet:
    # 限制文件上传大小
    multipart:
      # 最大上传大小限制为10MB。任何尝试上传超过这个大小的单个文件都会被拒绝。
      max-file-size: 10MB
      # 整个请求的最大大小限制为10MB。这包括所有上传文件的总和以及其他请求参数的大小
      max-request-size: 10MB
      enabled: true

# minio配置 =》 参考:https://juejin.cn/post/7412486655862489114
minio:
  # 服务地址
  url: http://${dataResource.ip}:9010
  # 用户名
  access-key: ${dataResource.minio.username}
  # 密码
  secret-key: ${dataResource.minio.password}
  # 桶对象
  bucket-name: upload-center
  # 分片对象过期时间 单位(天)
  #expiry: 1
  # 断点续传有效时间,在redis存储任务的时间 单位(天)
  #breakpoint-time: 1
  #临时切片存放位置
  bucket-name-slice: chunk-files

############## 自定义knife4j配置 ##############
# springdoc-openapi项目访问访问地址: http://127.0.0.1:8080/doc.html
springdoc:
  swagger-ui:
    # path: 配置swagger-ui.html/UI界面的访问路径,默认为/swagger-ui.html
    path: /swagger-ui.html
    # tags-sorter: 接口文档中的tags排序规则,默认为alpha,可选值为alpha(按字母顺序排序)或as-is(按照在代码中定义的顺序排序)
    tags-sorter: alpha
    # 该参数是swagger默认的排序规则,如果设置为alpha,那么Knife4j提供的按照order排序的增强规则不生效
    # 使用增强order属性进行排序,或者不设置该参数
    #    operations-sorter: alpha
    operations-sorter: order

  api-docs:
    path: /v3/api-docs
    # path: 配置api-docs的访问路径,默认为/v3/api-docs
    enabled: true   #是否开启文档功能

  group-configs:
    # group-configs: 配置分组信息
    - group: '默认'
      # group: 分组名称
      paths-to-match: '/**'
      # paths-to-match: 配置要匹配的路径,默认为/**
      packages-to-scan: com.ayo.controller
      # packages-to-scan: 配置要扫描的包的路径,直接配置为Controller类所在的包名即可

# knife4j项目访问访问地址:http://127.0.0.1:8080/doc.html#/home
knife4j:
  enable: true
  # 设置为true以启用Knife4j增强功能,这将再应用程序中启用Knife4j UI
  setting:
    # language: 设置Knife4j UI的语言,默认为zh_cn,可选值为zh_cn或en
    language: zh_cn
    swagger-model-name: 数据对象   #重命名SwaggerModel名称,默认
  #开启生产环境屏蔽
  production: false
  #是否启用登录认证
  basic:
    enable: true
    username: root # 自己设置一个
    password: 123456 # 自己设置一个

配置类

MinIO配置类(核心)

@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {

    /* 用户名 */
    @NotEmpty(message = "minio认证账户不可为空")
    private String accessKey;
    /* 密码 */
    @NotEmpty(message = "minio认证密码不可为空")
    private String secretKey;
    /* 服务地址 */
    @NotEmpty(message = "minio服务地址不可为空")
    @Pattern(regexp = "^(http|https)://.*$", message = "minio服务地址格式错误")
    private String url;
    /* 上传的数据桶 */
    private String bucketName = "upload-files";
    /* 切片存放的临时数据桶 */
    private String chunkBucketName = "chunk-files";
    /* 断点续传有效时间,在redis存储任务的时间 单位(天) */
    //private String sliceExpiry = "1";

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(url)
                .credentials(accessKey, secretKey)
                .build();
    }

}

Knife4j配置

@Configuration
public class Knife4jConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        //创建一个 OpenAPI 对象,用于表示整个 API 的文档信息
        return new OpenAPI()
                // 接口文档标题
                .info(new Info().title("MinIO接口文档")
                        // 接口文档简介
                        .description("MinIO相关服务接口文档")
                        // 接口文档版本
                        .version("0.0.1-SNAPSHOT")
                        // 开发者的联系方式,包括姓名和电子邮件地址
                        .contact(new Contact().name("Ayo").email("2192475085@qq.com")));

    }

}

FastJson配置

@Configuration
public class WebMvcConfig  implements WebMvcConfigurer  {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        //自定义配置...
        FastJsonConfig config = new FastJsonConfig();
        config.setDateFormat("yyyy-MM-dd HH:mm:ss");
        config.setReaderFeatures(JSONReader.Feature.FieldBased, JSONReader.Feature.SupportArrayToBean);
        config.setWriterFeatures(JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.PrettyFormat);
        converter.setFastJsonConfig(config);
        converter.setDefaultCharset(StandardCharsets.UTF_8);
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
        converters.add(0, converter);
    }

}

其他配置相关

先准备一个自定义的工具类

@Slf4j
@Component
public class AyoUtils {
    /**
     * 检查服务运行状况
     *
     * @param urlString url字符串
     * @return boolean
     * @throws IOException ioexception
     */
    public  boolean checkMinioHealth(String urlString) throws IOException {
        Socket socket = new Socket();
        URL url = new URL(urlString);
        String host = url.getHost();
        int port = url.getPort();
        try {
            socket.connect(new InetSocketAddress(host, port), 2000);
            return true;
        } catch (IOException exception) {
            return false;
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                log.error("socket关闭失败");
            }
        }
    }

    /**
     * 文件大小换算
     *
     * @param bytes 字节
     * @return {@link String }
     */
    public  String formatFileSize(long bytes) {
        if (bytes < 1024) {
            return bytes + " Bytes";
        } else if (bytes < 1024 * 1024) {
            return String.format("%.2f KB", bytes / 1024.0);
        } else if (bytes < 1024 * 1024 * 1024) {
            return String.format("%.2f MB", bytes / (1024.0 * 1024));
        } else {
            return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
        }
    }


    /**
     * 移除 URL 中 ? 之后的所有部分
     *
     * @param url 包含查询参数的完整 URL
     * @return 清理后的 URL
     */
    public  String removeUrlParameters(String url) {
        // 查找 ? 的位置
        int queryIndex = url.indexOf('?');
        // 如果 ? 存在,返回 ? 之前的部分;否则返回原始 URL
        if (queryIndex != -1) {
            return url.substring(0, queryIndex);
        } else {
            return url;
        }
    }

}

预检MinIO服务

@Slf4j
@Component
@RequiredArgsConstructor
public class InitConfig implements InitializingBean {

    private final MinioUtils minioUtils;
    private final MinioConfig minioConfig;
    private final AyoUtils ayoUtils;

    @Override
    public void afterPropertiesSet()  {
        //验证MinIO服务器是否在线
        try {
            if(!ayoUtils.checkMinioHealth(minioConfig.getUrl())){
                log.error("MinIO服务器不在线");
                return;
            }else{
                log.info("MinIO服务正常");
            }
        } catch (IOException e) {
            log.error("验证出错,服务启动失败");
            return;
        }

   
        String[] bucketNameArr = {minioConfig.getBucketName(), minioConfig.getChunkBucketName()};
        for (String s : bucketNameArr) {
            //验证桶是否存在
            if (!minioUtils.bucketExists(s)) {
                minioUtils.createBucket(s);
                minioUtils.setBucketPolicy(s, BuckerPolicyEnum.READ.getRight());
            }
        }
    }
}

我这里显示预检MinIO服务是否在线,不在线就打印输出日志信息,具体你可以自己实现你要实现的功能
如果MInIO服务在线,再检查对应的Yaml配置文件,验证文件上传桶切片上传桶是否存在,不存在则创建

MinioUtils配置在下方实现

MinioUtils工具类(核心)

由于 MinIO的创建需要配置一些访问策略,所以我这里先将策略配置提了出来

MinIO设置桶策略

桶策略接口

public interface IBucketPolicy {
    boolean createBucketPolicy(MinioClient client, String bucket);
}

桶只读策略

@Slf4j
public class BuckerReadPolicy implements IBucketPolicy {
    /**
     * 桶占位符
     */
    private static final String BUCKET_PARAM = "${bucket}";
    /**
     * bucket权限-只读
     */
    private static final String READ_ONLY = "{\n" +
            "    "Version": "2012-10-17",\n" +
            "    "Statement": [\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:GetBucketLocation"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
            "            ]\n" +
            "        },\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:ListBucket"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
            "            ],\n" +
            "            "Condition": {\n" +
            "                "StringEquals": {\n" +
            "                    "s3:prefix": [\n" +
            "                        "*"\n" +
            "                    ]\n" +
            "                }\n" +
            "            }\n" +
            "        },\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:GetObject"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::" + BUCKET_PARAM + "/**"\n" +
            "            ]\n" +
            "        }\n" +
            "    ]\n" +
            "}";

    @Override
    public boolean createBucketPolicy(MinioClient client, String bucket) {
        // TODO Auto-generated method stub
        try {
            client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(READ_ONLY.replace(BUCKET_PARAM, bucket)).build());
            return true;
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                 | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                 | IllegalArgumentException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            log.error("error: {}", e.getMessage(), e);
        }
        return false;
    }


}

桶只写策略

@Slf4j
public class BuckerWritePoliy implements IBucketPolicy {
    /**
     * 桶占位符
     */
    private static final String BUCKET_PARAM = "${bucket}";

    /**
     * bucket权限-只写
     */
    private static final String WRITE_ONLY = "{\n" +
            "    "Version": "2012-10-17",\n" +
            "    "Statement": [\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:GetBucketLocation",\n" +
            "                "s3:ListBucketMultipartUploads"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
            "            ]\n" +
            "        },\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:AbortMultipartUpload",\n" +
            "                "s3:DeleteObject",\n" +
            "                "s3:ListMultipartUploadParts",\n" +
            "                "s3:PutObject"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +"/**"\n" +
            "            ]\n" +
            "        }\n" +
            "    ]\n" +
            "}";

    @Override
    public boolean createBucketPolicy(MinioClient client, String bucket) {
        // TODO Auto-generated method stub
        try {
            client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(WRITE_ONLY.replace(BUCKET_PARAM, bucket)).build());
            return true;
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                 | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                 | IllegalArgumentException | IOException e) {
            log.error("error: {}", e.getMessage(), e);
        }
        return false;
    }

}

桶读写策略

@Slf4j
public class BuckerReadWriterPolicy implements IBucketPolicy {
    /**
     * 桶占位符
     */
    private static final String BUCKET_PARAM = "${bucket}";

    /**
     * bucket权限-读写
     */
    private static final String READ_WRITE = "{\n" +
            "    "Version": "2012-10-17",\n" +
            "    "Statement": [\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:GetBucketLocation",\n" +
            "                "s3:ListBucketMultipartUploads"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
            "            ]\n" +
            "        },\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:ListBucket"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
            "            ],\n" +
            "            "Condition": {\n" +
            "                "StringEquals": {\n" +
            "                    "s3:prefix": [\n" +
            "                        "*"\n" +
            "                    ]\n" +
            "                }\n" +
            "            }\n" +
            "        },\n" +
            "        {\n" +
            "            "Effect": "Allow",\n" +
            "            "Principal": {\n" +
            "                "AWS": [\n" +
            "                    "*"\n" +
            "                ]\n" +
            "            },\n" +
            "            "Action": [\n" +
            "                "s3:AbortMultipartUpload",\n" +
            "                "s3:DeleteObject",\n" +
            "                "s3:GetObject",\n" +
            "                "s3:ListMultipartUploadParts",\n" +
            "                "s3:PutObject"\n" +
            "            ],\n" +
            "            "Resource": [\n" +
            "                "arn:aws:s3:::"+ BUCKET_PARAM +"/**"\n" +
            "            ]\n" +
            "        }\n" +
            "    ]\n" +
            "}";

    @Override
    public boolean createBucketPolicy(MinioClient client, String bucket) {
        // TODO Auto-generated method stub
        try {
            client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(READ_WRITE.replace(BUCKET_PARAM, bucket)).build());
            return true;
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                 | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                 | IllegalArgumentException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            log.error("error: {}", e.getMessage(), e);
        }
        return false;
    }

}

桶策略枚举类

public enum BuckerPolicyEnum {
    READ("read","只读"),
    WRITE("write","只写"),
    READ_WRITE("read-write","可读写");

    private final String right;
    private final String describe;

    public String getRight() {
        return right;
    }

    public String getDescribe() {
        return describe;
    }

    BuckerPolicyEnum(String right, String describe){
        this.right = right;
        this.describe = describe;
    }

}

桶策略工厂函数

public class BuckerPolicyFactory {
    static Map<String, IBucketPolicy> operationMap = new HashMap<>();
    static {
        // 只读
        operationMap.put(BuckerPolicyEnum.READ.getRight(), new BuckerReadPolicy());
        // 只写
        operationMap.put(BuckerPolicyEnum.WRITE.getRight(), new BuckerWritePoliy());
        // 读写
        operationMap.put(BuckerPolicyEnum.READ_WRITE.getRight(), new BuckerReadWriterPolicy());
    }

    public static IBucketPolicy getBucketPolicyInterface(String poliy){
        IBucketPolicy object = operationMap.get(poliy);
        if(object == null){
            object = new BuckerDefaultPolicy();
        }
        return object;
    }

}

封装MinioUtils

@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtils {

    private final MinioClient minioClient;


    /**
     * --------------------------------------------------------------------
     * 桶操作(开始)
     * --------------------------------------------------------------------
     * */



    /**
     * 判断Bucket是否存在,true:存在,false:不存在
     *
     * @param bucketName 桶名
     * @return boolean
     */
    @SneakyThrows(Exception.class)
    public boolean bucketExists(String bucketName) {
        if (bucketName != null && !bucketName.isEmpty()) {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            //BucketExistsArgs exist = BucketExistsArgs.builder().bucket(bucketName).build();
            //boolean result = minioClient.bucketExists(exist);

        }
        return false;
    }

    /**
     * 启动SpringBoot容器的时候初始化Bucket
     * 如果没有Bucket则创建
     *
     * @param bucketName 桶名
     * @return boolean
     */
    @SneakyThrows(Exception.class)
    public boolean createBucket(String bucketName) {
        boolean flag = false;
        if (!bucketExists(bucketName)) {
            MakeBucketArgs create = MakeBucketArgs.builder().bucket(bucketName).build();
            minioClient.makeBucket(create);
            flag = true;
        }
        return flag;
    }

    /**
     * 设置桶策略
     *
     * @param bucketName 桶名
     * @param policy     策略(read,write,read-write)
     * @return boolean
     */
    @SneakyThrows(Exception.class)
    public boolean setBucketPolicy(String bucketName, String policy) {
        // 判断桶是否存在
        if (bucketExists(bucketName)) {
            IBucketPolicy bucketPolicy = BuckerPolicyFactory.getBucketPolicyInterface(policy);
            return bucketPolicy.createBucketPolicy(minioClient, bucketName);
        }
        return false;
    }

    /**
     * 删除桶
     *
     * @param bucketName 桶名
     */
    @SneakyThrows(Exception.class)
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /**
     * 获得Bucket的策略
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public String getBucketPolicy(String bucketName) {
        return minioClient.getBucketPolicy(GetBucketPolicyArgs
                .builder()
                .bucket(bucketName)
                .build());
    }

    /**
     * 获得所有Bucket列表
     *
     * @return
     */
    @SneakyThrows(Exception.class)
    public List<Bucket> getAllBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * --------------------------------------------------------------------
     * 桶操作(结束)
     * --------------------------------------------------------------------
     * */

    ////////////////////////////////////////////////////////////////////////

    /**
     * --------------------------------------------------------------------
     * 文件操作(开始)
     * --------------------------------------------------------------------
     * */

    /**
     * 根据bucketName获取其相关信息
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public Optional<Bucket> getBucket(String bucketName) {
        return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * 创建文件夹或目录
     *
     * @param bucketName 存储桶
     * @param objectName 目录路径
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse createDir(String bucketName, String objectName) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{});
        ObjectWriteResponse response = minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(byteArrayInputStream, 0, -1)
                        .build());

        byteArrayInputStream.close();
        return response;
    }

    /**
     * 判断文件是否存在
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            //log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * 判断文件夹是否存在
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isFolderExist(String bucketName, String objectName) {
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && objectName.equals(item.objectName())) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            //log.error("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * 根据文件前置查询文件
     *
     * @param bucketName 存储桶
     * @param prefix     前缀
     * @param recursive  是否使用递归查询
     * @return MinioItem 列表
     */
    @SneakyThrows(Exception.class)
    public List<Item> getAllObjectsByPrefix(String bucketName,
                                            String prefix,
                                            boolean recursive) {
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
        if (objectsIterator != null) {
            for (Result<Item> o : objectsIterator) {
                Item item = o.get();
                list.add(item);
            }
        }
        return list;
    }

    /**
     * 获取文件流
     *
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @return 二进制流
     */
    //@SneakyThrows(Exception.class)
    public GetObjectResponse getObject(String bucketName, String objectName) {
        try {
            return minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build());
        } catch (Exception e) {
            log.warn("文件获取【{objectName}】失败");
            return null;
        }
    }

    /**
     * 断点下载
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @param offset     起始字节的位置
     * @param length     要读取的长度
     * @return 二进制流
     */
    @SneakyThrows(Exception.class)
    public GetObjectResponse getObject(String bucketName, String objectName, long offset, long length) {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build());
    }

    /**
     * 获取路径下文件列表
     *
     * @param bucketName 存储桶
     * @param prefix     文件名称
     * @param recursive  是否递归查找,false:模拟文件夹结构查找
     * @return 二进制流
     */
    public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(recursive)
                        .build());
    }

    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName  存储桶
     * @param file        文件名
     * @param objectName  对象名
     * @param contentType 类型
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
        InputStream inputStream = file.getInputStream();
        ObjectWriteResponse response = minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(contentType)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());

        inputStream.close();
        return response;
    }

    /**
     * 通过md5获取分片列表  块文件必须满足 名字是 1 2 3 5....(索引从1开始)
     *
     * @param chunkBucketName 分片桶名称
     * @param fileMd5         源文件md5
     * @param chunkTotal      分片总数量
     * @return {@link List }<{@link ComposeSource }> 分片集
     */
    public List<ComposeSource> getChunkListByMd5(String chunkBucketName, String fileMd5, Integer chunkTotal) {
        // 获取所以分块
        List<Item> chunkList = getChunkList(chunkBucketName, fileMd5);
        // 获取缺失的分块
        List<Integer> missChunkIndexList = getMissChunkIndexList(chunkList, chunkTotal);
        if (!missChunkIndexList.isEmpty()) {
            throw new RuntimeException(
                    String.format(
                            "丢失分片索引:%s",
                            missChunkIndexList.toString()
                    )
            );
        }
        // 获取块资源
        List<ComposeSource> composeSourceList = new ArrayList<>();
        for (Item item : chunkList) {
            composeSourceList.add(ComposeSource.builder().bucket(chunkBucketName).object(item.objectName()).build());
        }

        return composeSourceList;
    }


    ///**
    // * 通过md5删除分块列表
    // *
    // * @param chunkBucketName 分块所在的桶名称
    // * @param fileMd5         源文件md5
    // * @return boolean
    // */
    //public void removeChunkListByMd5(String chunkBucketName, String fileMd5) {
    //    // 获取所以分块
    //    //List<Item> chunkList = getChunkList(chunkBucketName, fileMd5);
    //    removeFile(chunkBucketName, fileMd5);
    //}

    /**
     * 合并分片块
     *
     * @param composeSourceList 分片集
     * @param chunkBucketName   分片所在的桶
     * @param bucketName        合并到哪个桶
     * @param fileName          合并后的文件名称
     * @return boolean
     */
    @SneakyThrows
    public boolean mergeChunk(List<ComposeSource> composeSourceList, String chunkBucketName, String bucketName, String fileName) {

        String contentType = ViewContentTypeEnum.getContentType(fileName);
        Map<String,String> header = new HashMap<>();
        header.put("Content-Type",contentType);
        // 合并
        ObjectWriteResponse composeResponse = minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .sources(composeSourceList)
                        .headers(header)
                        .build());


        // 上传对象并设置 Content-Type
        //minioClient.putObject(
        //        PutObjectArgs.builder()
        //                .bucket(bucketName)
        //                .object(fileName)
        //                .contentType(ViewContentTypeEnum.getContentType(fileName))
        //                .build()
        //);
        return true;
    }

    /**
     * 设置合并块的ContentType => 用于某些类型的文件链接访问不触发下载而是直接预览
     *
     * @param bucketName bucket名称
     * @param fileName   文件名称
     * @return boolean
     */
    @SneakyThrows
    public boolean setMergeChunkContentType(String bucketName, String fileName) {
        GetObjectResponse object = getObject(bucketName, fileName);
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(fileName)
                .contentType(ViewContentTypeEnum.getContentType(fileName))
                // 这里需要提供一个有效的流
                //.stream(new ByteArrayInputStream(new byte[0]), 0, -1)
                .stream(object, object.available(), -1)
                .build());
        object.close();
        return true;
    }

    /**
     * 上传本地文件
     *
     * @param bucketName 存储桶
     * @param objectName 对象名称
     * @param fileName   本地文件路径
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
        return minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(fileName)
                        .build());
    }

    /**
     * 通过流上传文件
     *
     * @param bucketName  存储桶
     * @param objectName  文件对象
     * @param inputStream 文件流
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(ViewContentTypeEnum.getContentType(objectName))
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }

    /**
     * 获取文件信息, 如果抛出异常则说明文件不存在
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @return
     */
    @SneakyThrows(Exception.class)
    public Map<String, String> getFileStatusInfo(String bucketName, String objectName) {
        String dataStr = minioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()).toString();
        // 更新正则表达式以匹配带连字符的键
        String regex = "([\w-]+)=([^,}]+)";

        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(dataStr);

        Map<String, String> map = new HashMap<>();

        while (matcher.find()) {
            String key = matcher.group(1);
            String value = matcher.group(2);
            map.put(key, value);
        }
        return map;
    }

    /**
     * 拷贝文件
     *
     * @param bucketName    存储桶
     * @param objectName    文件名
     * @param srcBucketName 目标存储桶
     * @param srcObjectName 目标文件名
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
        return minioClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                        .bucket(srcBucketName)
                        .object(srcObjectName)
                        .build());
    }

    /**
     * 删除文件
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     */
    @SneakyThrows(Exception.class)
    public void removeFile(String bucketName, String objectName) {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }

    /**
     * 批量删除文件
     *
     * @param bucketName 存储桶
     * @param keys       需要删除的文件列表
     * @return
     */
    public void removeFiles(String bucketName, List<String> keys) {
        List<DeleteObject> objects = new LinkedList<>();
        keys.forEach(s -> {
            objects.add(new DeleteObject(s));
            try {
                removeFile(bucketName, s);
            } catch (Exception e) {
                log.error("[Minio工具类]>>>> 批量删除文件,异常:", e);
            }
        });
    }

    /**
     * 删除文件
     *
     * @param bucketName 存储桶
     * @param dir        分片所在的文件夹
     */
    @SneakyThrows(Exception.class)
    public void removeAllChunk(String bucketName, String dir) {
        List<Item> chunkList = getChunkList(bucketName, dir);

        //遍历所有的文件,将其加入待删除列表中
        List<String> chunkNames = new ArrayList<>();
        for (Item item : chunkList) {
            chunkNames.add(item.objectName());
        }
        removeFiles(bucketName, chunkNames);
    }

    /**
     * 获取文件外链并设置过期时间(设置过cont-type,否则都是下载链接)
     *
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @param expires    过期时间 <=7 秒 (外链有效时间(单位:秒))
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
        return getUtf8ByURLDecoder(
                minioClient.getPresignedObjectUrl(
                        GetPresignedObjectUrlArgs
                                .builder()
                                .expiry(expires)
                                .bucket(bucketName)
                                .object(objectName)
                                .build()
                )
        );
    }

    /**
     * 获得文件下载外链
     *
     * @param bucketName
     * @param objectName
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .method(Method.GET).build();
        //return minioClient.getPresignedObjectUrl(args);
        return getUtf8ByURLDecoder(minioClient.getPresignedObjectUrl(args));
    }


    /**
     * --------------------------------------------------------------------
     * 文件操作(结束)
     * --------------------------------------------------------------------
     * */


    /**
     * --------------------------------------------------------------------
     * 其他操作(开始)
     * --------------------------------------------------------------------
     * */
    /**
     * 将一个 Base64 编码的字符串转换为 InputStream 对象
     *
     * @param base64 Base64 编码的字符串
     * @return InputStream
     */
    public static InputStream base64ToInputStream(String base64) {
        ByteArrayInputStream stream = null;
        try {
            byte[] bytes = Base64.decodeBase64(base64.trim());
            stream = new ByteArrayInputStream(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return stream;
    }

    /**
     * 获取分片列表
     *
     * @param chunkBucketName 块桶名称
     * @param md5             源文件md5 => 用于生成唯一文件夹和读取文件夹下的块
     * @return {@link List }<{@link Item }>
     */
    private List<Item> getChunkList(String chunkBucketName, String md5) {
        Iterable<Result<Item>> resultIterable = listObjects(chunkBucketName, md5, true);
        // 分块
        List<Item> result = new ArrayList<>();
        for (Result<Item> itemResult : resultIterable) {
            try {
                result.add(itemResult.get());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        // 分片文件排序
        result.sort((o1, o2) -> {
            String o1Name = o1.objectName();
            String o2Name = o2.objectName();
            int o1Index = Integer.parseInt(o1Name.substring(o1Name.lastIndexOf("/") + 1));
            int o2Index = Integer.parseInt(o2Name.substring(o2Name.lastIndexOf("/") + 1));
            return o1Index - o2Index;
        });

        return result;
    }

    /**
     * 获取缺失的分片
     *
     * @param chunkList  分片列表
     * @param chunkTotal 分片总数
     * @return 缺失的分片列表
     */
    private List<Integer> getMissChunkIndexList(List<Item> chunkList, Integer chunkTotal) {
        // 缺失的分片文件
        List<Integer> missChunkIndexList = new ArrayList<>();

        // 列出已经存在的分片存在set里
        HashSet<Integer> chunkIndexSet = new HashSet<>();
        for (Item item : chunkList) {
            String chunkName = item.objectName();
            int chunkIndex = Integer.parseInt(chunkName.substring(chunkName.lastIndexOf("/") + 1));
            chunkIndexSet.add(chunkIndex);
        }

        // 分片索引应该从1开始
        for (int i = 1; i <= chunkTotal; i++) {
            // 是否缺少当前分片
            if (!chunkIndexSet.contains(i)) {
                missChunkIndexList.add(i);
            }
        }

        return missChunkIndexList;
    }

    /**
     * 将URLDecoder编码转成UTF8
     *
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
        String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
        return URLDecoder.decode(url, "UTF-8");
    }
    /**
     * --------------------------------------------------------------------
     * 其他操作(结束)
     * --------------------------------------------------------------------
     * */
}

这里我们可以看见MinioUtils工具类setBucketPolicy方法就需要用到桶策略生成的工厂夯实去实现配置桶的策略,其次我们在InitConfig预检MinIO服务中将新建的桶都设置为只读

minioUtils.setBucketPolicy(s, BuckerPolicyEnum.READ.getRight());

你也可以根据自己的需求设置

其他需要用到的类(由业务决定)

这些类不涉及MinIO的核心操作,根据个人业务需求确定是否需要

文件比较类

public class FileComparer {
    static {
        // 注册 BouncyCastle 提供者
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 比较文件
     *
     * @param multipartFile 多部分文件
     * @param inputStream   输入流
     * @return boolean
     * @throws IOException ioexception
     */
    public static boolean compareFiles(MultipartFile multipartFile, InputStream inputStream) {
        String sm3FromMultipartFile = null;
        String sm3FromInputStream = null;
        try {
            sm3FromMultipartFile = calculateSm3(multipartFile.getInputStream());
            sm3FromInputStream = calculateSm3(inputStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return sm3FromMultipartFile.equals(sm3FromInputStream);
    }

    /**
     * 计算sm3
     *
     * @param inputStream 输入流
     * @return {@link String }
     * @throws IOException ioexception
     */
    private static String calculateSm3(InputStream inputStream) throws IOException {
        SM3Digest digest = new SM3Digest();
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            digest.update(buffer, 0, bytesRead);
        }
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);
        return bytesToHex(hash);
    }

    /**
     * 字节到十六进制
     *
     * @param bytes 字节
     * @return {@link String }
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

}

由于封装的MininUtil判断文件是否存在是通过文件名来的,同一个文件 不同名会判断false,即文件不存在

这里我需要用它来判断上传的文件是否存在,逻辑线是:

  1. 先根据文件名判断文件是否在桶中存在
  2. 不存在正常上传
  3. 存在则通过该类比较两个文件是否为一个文件
  4. 是同一个文件直接返回MinIO里的文件
  5. 不是同一个文件则重新生成一个唯一的文件名(UUID),再进行上传操作

注意:若是考虑的实际个人建议是通过文件的 【md5值_文件名】来设置文件名,然后通过数据库来存储文件的原始信息,例如文件的原始名,文件访问地址,文件md5值等,验证的时候直接走数据库,我这里由于是测试所以就用的UUID

统一返回结果

@Data
@Schema(name = "统一返回的结果对象➱Result<T>")
public class Result<T> {

    //@Schema(description = "返回状态")
    //private Boolean flag;

    @Schema(description = "状态码")
    private int code;

    @Schema(description = "返回信息")
    private String msg;

    @Schema(description = "返回数据")
    private T data;

    /**
     * 私有静态方法,用于构建一个 Result 对象。
     * 接受 flag、data、code 和 message 四个参数,并返回一个根据这些参数构建的 Result 对象。
     */
    private static <T> Result<T> buildResult(T data, Integer code, String msg) {
        Result<T> r = new Result<>();
        r.setData(data);
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    /**
     * 成功---------------------------------------------
     */
    public static <T> Result<T> success() {
        ResultCodeEnum success = ResultCodeEnum.SUCCESS;
        return success((T) Collections.emptyMap(), success.getCode(), success.getZhMsg());
    }

    public static <T> Result<T> success(T data) {
        ResultCodeEnum success = ResultCodeEnum.SUCCESS;
        return success(data, success.getCode(), success.getZhMsg());
    }

    public static <T> Result<T> success(T data, String msg) {
        ResultCodeEnum success = ResultCodeEnum.SUCCESS;
        return success(data, success.getCode(), msg);
    }

    public static <T> Result<T> success(T data, Integer code, String msg) {
        return buildResult( data, code, msg);
    }

    /**
     * 失败---------------------------------------------
     */
    public static <T> Result<T> fail() {
        return fail(ResultCodeEnum.FAIL.getCode(), ResultCodeEnum.FAIL.getZhMsg());
    }

    public static <T> Result<T> fail(String msg) {
        return fail(ResultCodeEnum.FAIL.getCode(), msg);
    }

    public static <T> Result<T> fail(Integer code, String msg) {
        return buildResult( (T) Collections.emptyMap(), code, msg);
    }

    /**
     * 服务器错误---------------------------------------------
     */
    public static <T> Result<T> error() {
        ResultCodeEnum errorCode = ResultCodeEnum.INTERNAL_SERVER_ERROR;
        return error(errorCode.getCode(),errorCode.getZhMsg());
    }

    public static <T> Result<T> error(String msg) {
        return error( ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(),msg);
    }

    public static <T> Result<T> error(Integer code, String msg) {
        return buildResult( (T) Collections.emptyMap(), code, msg);
    }

    /**
     * 设置数据 => 如果 data 为空,则设置为空Map
     */
    public void setData(T data) {
        //if(data == null || data.equals("")){
        //    //this.data = (T) new HashMap<String,Object>();
        //}else{
        //    this.data = data;
        //}
        this.data = Optional.ofNullable(data).orElse((T) new HashMap<String, Object>());
    }
}

全局统一异常处理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理系统异常
     */
    @ExceptionHandler(value = Exception.class)
    public Result<?> handleSystemException(Exception e) {
        log.error("系统异常:{}",e.getMessage());
        return Result.fail(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
    }

    /**
     * 处理运行时异常
     */
    @ExceptionHandler(value = RuntimeException.class)
    public Result<?> handleRuntimeException(RuntimeException e) {
        log.error("程序发生错误:{}", e.getMessage());
        return Result.fail(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
    }

    /**
     * 处理SpringBoot Validation 参数验证抛出的异常
     */
    //处理MethodArgumentNotValidException,该异常会在请求参数验证失败时抛出
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        log.error("请求参数验证异常:{}",e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(), e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
    }

    //处理ConstraintViolationException,该异常会在方法级别的参数验证失败时抛出
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
        log.error("方法参数验证失败");
        return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(), e.getConstraintViolations().iterator().next().getMessage());
    }

    /**
     * 处理Assert异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Result<?> handleIllegalArgumentException(IllegalArgumentException e) {
        log.error("服务器内部执行出行非法参数");
        return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(),e.getMessage());
    }

    /**
     * 文件上传大小超出限制
     */
    @ExceptionHandler(MultipartException.class)
    public Result<?> handleBusinessException(MaxUploadSizeExceededException ex) {
        String msg;
        Throwable rootCase = ex.getRootCause();
        if (rootCase instanceof org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException) {
            msg = "上传文件过大[单文件大小不得超过50M]";
        } else if (rootCase instanceof org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException) {
            msg = "上传文件过大[总上传文件大小不得超过50M]";
        } else {
            msg = "上传文件失败";
        }
        log.error(msg);
        //return Result.fail(ResultCodeEnum.UPLOAD_FILE_FAILED.getCode(), msg );
        return Result.success(new StateAndMsgVo(false, msg));
    }

    /**
     * 文件不存在
     */
    @ExceptionHandler(value = MissingServletRequestPartException.class)
    public Result<?> handleMissingServletRequestPartException() {
        return Result.fail(ResultCodeEnum.NOT_FOUND.getCode(), ResultCodeEnum.NOT_FOUND.getZhMsg());
    }

    /**
     * 连接异常
     */
    @ExceptionHandler(value = ConnectException.class)
    public Result<?> handleConnectException(ConnectException e) {
        return Result.fail(ResultCodeEnum.REQUEST_TIMEOUT.getCode(), "MinIO服务器不在线,请联系管理员进行检查");
    }
}

跨域处理

@Configuration
public class MyCorsFilter {
    @Bean
    public CorsFilter corsFilter() {
        // 1.创建 CORS 配置对象
        CorsConfiguration config = new CorsConfiguration();
        // 支持域
        config.addAllowedOriginPattern("*");
        // 是否发送 Cookie
        config.setAllowCredentials(true);
        // 支持请求方式
        config.addAllowedMethod("*");
        // 允许的原始请求头部信息
        config.addAllowedHeader("*");
        // 暴露的头部信息
        config.addExposedHeader("*");
        // 2.添加地址映射
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**", config);
        // 3.返回 CorsFilter 对象
        return new CorsFilter(corsConfigurationSource);
    }
}

VO

文件信息

@Data
@Schema(name = "文件信息➱FileInfoVo")
public class FileInfoVo {

    @Schema(description = "桶名")
    private String bucket;

    @Schema(description = "文件名")
    private String name;

    @Schema(description = "文件大小")
    private String size;

    @Schema(description = "文件安全地址(长)")
    private String safeUrl;

    @Schema(description = "文件不安全地址(短)")
    private String url;

    @Schema(description = "文件更新时间")
    //private LocalDateTime updateTime;
    private String updateTime;

    @Schema(description = "获取文件信息是否成功")
    private Boolean state = true;

    public FileInfoVo(String bucket, String name, String size, String safeUrl, String url, String updateTime, Boolean state) {
        this.bucket = bucket;
        this.name = name;
        this.size = size;
        this.safeUrl = safeUrl;
        this.url = url;
        this.updateTime = updateTime;
        this.state = state;
    }

    public FileInfoVo(String bucket, String name, String size, String safeUrl, String url, String updateTime) {
        this.bucket = bucket;
        this.name = name;
        this.size = size;
        this.safeUrl = safeUrl;
        this.url = url;
        this.updateTime = updateTime;
        state = false;
    }

    public FileInfoVo() {
    }

    /* minio工具类返回的Map数据进行相应的转换 */
    public FileInfoVo(Map<String, String> minioMap) {
        //文件名称
        this.name = minioMap.get("object");
        // 桶
        this.bucket = minioMap.get("bucket");
        //获取文件尺寸
        long fileSize = Long.parseLong(minioMap.get("size"));
        this.size = new AyoUtils().formatFileSize(fileSize);
        // 服务器文件上传时间 => 该时间受MinIO所在服务器时间的影响 => 前端请求时间和服务器实际时间相差过大会被MinIO拒绝请求
        Date date = DateUtil.parse(minioMap.get("last-modified"));
        this.updateTime = DateUtil.formatDateTime(date);
    }
}

状态信息

@Data
@Schema(name = "状态和信息➱StateAndMsg")
public class StateAndMsgVo {

    @Schema(description = "操作状态")
    private Boolean state;

    @Schema(description = "信息")
    private String msg;

    public StateAndMsgVo() {
    }

    public StateAndMsgVo(Boolean state, String msg) {
        this.state = state;
        this.msg = msg;
    }

    public void setStateAndMsg(Boolean state, String msg) {
        this.state = state;
        this.msg = msg;
    }
}

service层

接口

/**
 * 文件上传服务
 *
 * @author 山居
 * @date 2024/09/04  15:15
 */
public interface IFileService {

    /**
     * Minio是否在线
     *
     * @return {@link Result }
     */
    Boolean minioIsOk();


    /**
     * 文件是否存在
     *
     * @param bucketName bucket名称
     * @param fileName   文件名称
     * @return {@link Boolean }
     */
    Boolean fileIsExist(String bucketName, String fileName);


    /**
     * 文件是否存在
     *
     * @param fileName 文件名称
     * @return {@link Boolean }
     */
    Boolean fileIsExist(String fileName);

    /**
     * 文件上传
     *
     * @param file 文件
     * @return {@link String }
     */
    FileInfoVo fileUpload(MultipartFile file);

    /**
     * 获取文件信息
     *
     * @param bucketName bucket名称
     * @param fileName   文件名称
     * @return {@link FileInfoVo }
     */
    FileInfoVo getFileInfo(String bucketName, String fileName);

    /**
     * 获取文件信息
     *
     * @param fileName 文件名称
     * @return {@link FileInfoVo }
     */
    FileInfoVo getFileInfo(String fileName);


    /**
     * 文件删除
     *
     * @param bucketName bucket名称
     * @param fileName   文件名称
     * @return {@link Boolean }
     */
    Boolean fileDelete(String bucketName, String fileName);

    /**
     * 文件删除
     *
     * @param fileName 文件名称
     * @return {@link Boolean }
     */
    Boolean fileDelete(String fileName);
}

实现类

@Slf4j
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements IFileService {

    private final MinioUtils minioUtils;
    private final MinioConfig minioConfig;
    private final AyoUtils ayoUtils;

    // MinIO服务是否正常
    @Override
    public Boolean minioIsOk() {
        try {
            return ayoUtils.checkMinioHealth(minioConfig.getUrl());
        } catch (IOException e) {
            return false;
        }
    }

    // 文件是否存在
    @Override
    public Boolean fileIsExist(String bucketName, String fileName) {
        return minioUtils.isObjectExist(bucketName, fileName);
    }

    // 文件是否存在
    @Override
    public Boolean fileIsExist(String fileName) {
        return fileIsExist(minioConfig.getBucketName(), fileName);
    }

    // 文件上传
    @Override
    public FileInfoVo fileUpload(MultipartFile file) {
        /* 获取上传文件信息 => 先上传再获取 */
        //{bucket=upload-center, object=ic_tree_1.png, last-modified=2024-09-04T08:50:02Z, size=5490}
        Map<String, String> servicFileInfo = uploadGetServicFileInfo(file);
        return mapFormatterFileInfo(minioConfig.getBucketName(),servicFileInfo);
    }

    //获取文件信息
    @Override
    public FileInfoVo getFileInfo(String bucketName, String fileName) {
        if (fileIsExist(fileName)){
            Map<String, String> fileStatusInfo = minioUtils.getFileStatusInfo(bucketName, fileName);
            return  mapFormatterFileInfo(bucketName,fileStatusInfo);
        }
        return null;
    }

    //获取文件信息
    @Override
    public FileInfoVo getFileInfo(String fileName) {
        return getFileInfo(minioConfig.getBucketName(),fileName);
    }

    // 文件删除
    @Override
    public Boolean fileDelete(String bucketName, String fileName) {
        if (!fileIsExist(bucketName, fileName)) {
            return false;
        }
        minioUtils.removeFile(bucketName, fileName);
        return true;
    }

    @Override
    public Boolean fileDelete(String fileName) {
        return fileDelete(minioConfig.getBucketName(), fileName);
    }

    /**
     * ==========================================================================
     * 其他方法封装
     * ==========================================================================
     * */
    // 获取服务器文件信息 => 存在直接获取信息,不存在上传再获取信息
    private Map<String, String> uploadGetServicFileInfo(MultipartFile file) {
        //文件名
        String fileName = file.getOriginalFilename();
        // 验证MinIO服务器上是否存在同名文件
        if (fileIsExist(fileName)) {
            //比较文件的hash值是否为相同文件
            InputStream servicFile = minioUtils.getObject(minioConfig.getBucketName(), fileName);
            //文件是否一致(同名,且文件md5值一致)
            boolean isEqual = compareFiles(file, servicFile);
            if (!isEqual) {
                // 同名但是文件md5不一致,取个新的文件名
                String newFileName = IdUtil.simpleUUID() + "." + StringUtils.substringAfterLast(fileName, ".");
                minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, file.getContentType());
                log.warn("{} 存在同名文件,但是文件不一致,文件名被修改为 {}", fileName, newFileName);
                fileName = newFileName;
            } else {
                log.warn("{} 存在完全一致的文件,不再重复上传操作!!!", fileName);
            }
        } else {
            minioUtils.uploadFile(minioConfig.getBucketName(), file, fileName, file.getContentType());
            log.info("文件 {} 不存在,正常上传", fileName);
        }

        return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
    }

    // minio工具生成的Map数据转成FileInfo类型的vo数据
    FileInfoVo mapFormatterFileInfo(String bucketName, Map<String,String> servicFileInfo){
        /* 返回数据 */
        FileInfoVo fileInfoVo = new FileInfoVo(servicFileInfo);
        /* 获取文件URL地址 */
        String fileUrl = minioUtils.getPresignedObjectUrl(bucketName, fileInfoVo.getName());
        // 文件访问地址
        fileInfoVo.setSafeUrl(fileUrl);
        fileInfoVo.setUrl(ayoUtils.removeUrlParameters(fileUrl));
        return fileInfoVo;
    }

}

controller层

@ApiSort(1)
@Tag(name = "MinIO文件上传", description = "MinIO文件上传相关API接口")
@Slf4j
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {

    private final IFileService fileService;

    @ApiOperationSupport(order = 1)
    @Operation(summary = "验证服务器是否在线")
    @GetMapping("minioIsOk")
    Result<?> minioIsOk() {
        StateAndMsgVo stateAndMsgVo = new StateAndMsgVo(false, "服务未验证");
        if (fileService.minioIsOk()) {
            stateAndMsgVo.setState(true);
            stateAndMsgVo.setMsg("MinIO服务器为在线状态");
        } else {
            stateAndMsgVo.setMsg("MinIO服务器不在线");
        }
        return Result.success(stateAndMsgVo);
    }


    /**
     * 文件上传操作应该使用 @PostMapping@GetMapping 不支持处理 multipart/form-data 请求。
     */
    @ApiOperationSupport(order = 2)
    @Operation(
            summary = "文件上传",
            parameters = {
                    @Parameter(name = "file", description = "要上传的文件", required = true)
            }
    )
    @PostMapping("fileUpload")
    Result<?> fileUpload(@RequestParam MultipartFile file) {
        return Result.success(fileService.fileUpload(file));
    }

    /**
     * 根据文件名获取文件信息
     */
    @ApiOperationSupport(order = 3)
    @Operation(
            summary = "获取文件信息",
            parameters = {
                    @Parameter(name = "fileName", description = "要查询的文件名", required = true)
            }
    )
    @PostMapping("fileInfo")
    Result<?> fileUpload(@RequestParam(value = "fileName", required = true, defaultValue = "") String fileName) {
        if (fileName.isEmpty()) {
            return Result.success(new StateAndMsgVo(false, "必要参数文件名不存在"));
        }
        FileInfoVo fileInfoVo = fileService.getFileInfo(fileName);
        return Result.success(Objects.requireNonNullElseGet(fileInfoVo, () -> new StateAndMsgVo(false, "文件不存在")));
    }

    /**
     * 文件删除
     */
    @ApiOperationSupport(order = 4)
    @Operation(
            summary = "文件删除",
            parameters = {
                    @Parameter(name = "fileName", description = "要删除的文件名", required = true)
            }
    )
    @DeleteMapping("fileDelete")
    Result<?> fileDelete(@RequestParam String fileName) {
        if (fileName.isEmpty()) {
            return Result.success(new StateAndMsgVo(false, "必要参数文件名不存在"));
        }
        StateAndMsgVo stateAndMsgVo = new StateAndMsgVo(false, "删除操作失败,“" + fileName + "”文件不存在");
        if (fileService.fileDelete(fileName)) {
            stateAndMsgVo.setStateAndMsg(true, "文件“" + fileName + "”删除成功");
        }
        return Result.success(stateAndMsgVo);
    }
}

测试

这里就不展示了,直接去knife4j的发布页面自己测测

PS

若有不足之处欢迎👏指正