Spring Boot 封装 MinIO 工具

Spring Boot 封装 MinIO 工具

MinIO 简介

​ MinIO 是一款高性能、开源、兼容Amazon S3 API的分布式对象存储系统,专为云原生架构和大规模非结构化数据场景设计。其核心定位是成为私有云/混合云环境中的标准存储方案,适用于从数据湖到AI/ML、容器化部署等多样化需求。

背景

​ 为解决业务开时对接 MinIO繁琐接口对工作

目录结构

  • cdkj-minio MinIO工具
    • annotation 注解
      • EnableAutoMinio 启用自动MinIO
    • config 配置
      • MinioAutoConfiguration MinIO 自动配置
      • MinioMarkerConfiguration MinIO 标记配置
      • MinioProperties MinIO 配置读取
    • connectivity 连接库
      • MinioConfiguration MinIO 配置
    • enums 枚举库
      • ContentTypeEnums 内容类型枚举
    • MinioUtils MinIO工具库

项目介绍

POM引入包

  <dependencies>
        <!-- MinIO -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.squareup.okhttp3</groupId>
                    <artifactId>okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>
    </dependencies>

EnableAutoMinio 启用自动MinIO

​ spring boot 启动加载工具包

package com.cdkjframework.minio.annotation;

import com.cdkjframework.minio.config.MinioMarkerConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.annotation
 * @ClassName: EnableAutoMinio
 * @Description: java类作用描述
 * @Author: xiaLin
 * @Date: 2024/9/2 11:27
 * @Version: 1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MinioMarkerConfiguration.class})
public @interface EnableAutoMinio {
}

Spring Boot 项目引入

package cn.qjwxlcps.ims.annotation;

import com.cdkjframework.cloud.job.core.handler.annotation.EnableAutoCdkjJob;
import com.cdkjframework.datasource.mongodb.annotation.EnableAutoMongo;
import com.cdkjframework.minio.annotation.EnableAutoMinio;
import com.cdkjframework.swagger.annotation.EnableAutoSwagger;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @ProjectName: com.lesmarthome.interface
 * @Package: com.lesmarthome.interfaces.annotation
 * @ClassName: EnableAutoApi
 * @Description: java类作用描述
 * @Version: 1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableRetry
@EnableAsync
@Configuration
@EnableAutoMinio
@EnableAutoMongo
@EnableAutoCdkjJob
//@EnableAutoSwagger
@EnableDiscoveryClient
@EnableTransactionManagement
@EnableFeignClients(basePackages = {"com.*.*.client"})
public @interface EnableAutoApi {
}

MinIO 配置读取

​ 该类主要读取用户配置信息

package com.cdkjframework.minio.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.config
 * @ClassName: MinioProperties
 * @Description: mini配置
 * @Version: 1.0
 */
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "spring.minio")
public class MinioProperties {

    /**
     * 访问域名
     */
    private String domain;

    /**
     * 存储端点
     */
    private String endpoint;

    /**
     * 端口
     */
    private Integer port;

    /**
     * 访问密钥
     */
    private String accessKey;

    /**
     * 密钥
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String bucketName;

    /**
     * 分片对象过期时间 单位(天)
     */
    private Integer expiry;

    /**
     * 断点续传有效时间,在redis存储任务的时间 单位(天)
     */
    private Integer breakpointTime;
}

内容类型枚举

package com.cdkjframework.minio.enums;

import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.util.tool.StringUtils;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.enums
 * @ClassName: ContentTypeEnums
 * @Description: 内容类型枚举
 * @Version: 1.0
 */
public enum ContentTypeEnums {
    /**
     * 默认类型
     */
    DEFAULT("default", "application/octet-stream"),
    JPG("jpg", "image/jpeg"),
    TIFF("tiff", "image/tiff"),
    GIF("gif", "image/gif"),
    JFIF("jfif", "image/jpeg"),
    PNG("png", "image/png"),
    TIF("tif", "image/tiff"),
    ICO("ico", "image/x-icon"),
    JPEG("jpeg", "image/jpeg"),
    WBMP("wbmp", "image/vnd.wap.wbmp"),
    FAX("fax", "image/fax"),
    NET("net", "image/pnetvue"),
    JPE("jpe", "image/jpeg"),
    RP("rp", "image/vnd.rn-realpix"),
    MP4("mp4", "video/mp4");

    /**
     * 文件名后缀
     */
    private final String suffix;

    /**
     * 返回前端请求头中,Content-Type具体的值
     */
    private final String value;

    ContentTypeEnums(String suffix, String value) {
        this.suffix = suffix;
        this.value = value;
    }

    /**
     * 根据文件后缀,获取Content-Type
     *
     * @param suffix 文件后缀
     * @return 返回结果
     */
    public static String formContentType(String suffix) {
        if (StringUtils.isNullAndSpaceOrEmpty(suffix)) {
            return DEFAULT.getValue();
        }
        int beginIndex = suffix.lastIndexOf(StringUtils.POINT) + IntegerConsts.ONE;
        suffix = suffix.substring(beginIndex);
        for (ContentTypeEnums value : ContentTypeEnums.values()) {
            if (suffix.equalsIgnoreCase(value.getSuffix())) {
                return value.getValue();
            }
        }
        return DEFAULT.getValue();
    }

    public String getSuffix() {
        return suffix;
    }

    public String getValue() {
        return value;
    }
}

MinIO 标记配置

package com.cdkjframework.minio.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.config
 * @ClassName: MinioMarkerConfiguration
 * @Description: Minio标记配置
 * @Version: 1.0
 */
@Configuration(proxyBeanMethods = false)
public class MinioMarkerConfiguration {
    @Bean
    public Marker mybatisMarker() {
        return new Marker();
    }

    public static class Marker {

    }
}

MinIO 自动配置

注入 Bean 之前需要先加载配置 MinioProperties,且基于Bean的条件 MinioMarkerConfiguration.Marker 完成。开始调用 MinioConfiguration 中的start Bean方法。

package com.cdkjframework.minio.config;

import com.cdkjframework.minio.connectivity.MinioConfiguration;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.config
 * @ClassName: MinioAutoConfiguration
 * @Description: Minio自动配置
 * @Version: 1.0
 */
@Lazy(false)
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({
        MinioProperties.class
})
@AutoConfigureAfter({WebClientAutoConfiguration.class})
@ConditionalOnBean(MinioMarkerConfiguration.Marker.class)
public class MinioAutoConfiguration {

    /**
     * 配置信息
     */
    private final MinioProperties minioProperties;

    /**
     * minio配置
     *
     * @return 返回配置信息
     */
    @Bean(initMethod = "start")
    public MinioConfiguration minioConfiguration() {
        return new MinioConfiguration(minioProperties);
    }
}
 体验AI代码助手
 代码解读
复制代码

MinIO 配置

package com.cdkjframework.minio.connectivity;

import com.cdkjframework.minio.MinioUtils;
import com.cdkjframework.minio.config.MinioProperties;
import io.minio.MinioClient;
import org.springframework.context.annotation.Bean;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio.connectivity
 * @ClassName: MinioConfiguration
 * @Description: minio配置
 * @Version: 1.0
 */
public class MinioConfiguration {

    /**
     * 配置信息
     */
    private final MinioProperties minioProperties;

    /**
     * 构建函数
     */
    public MinioConfiguration(MinioProperties minioProperties) {
        this.minioProperties = minioProperties;
    }

    /**
     * 启动
     */
    @Bean(name = "start")
    public void start() {
        MinioClient.Builder builder = MinioClient.builder();
        if (minioProperties.getPort() == null) {
            builder.endpoint(minioProperties.getEndpoint());
        } else {
            builder.endpoint(minioProperties.getEndpoint(), minioProperties.getPort(), Boolean.FALSE);
        }
        MinioClient client = builder
                // 服务端用户名 、 服务端密码
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();

        // 实例化工具类
        new MinioUtils(client);
    }
}

MinIO工具库

该工具库主要提供静态接口方法

package com.cdkjframework.minio;

import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.exceptions.GlobalException;
import com.cdkjframework.exceptions.GlobalRuntimeException;
import com.cdkjframework.minio.enums.ContentTypeEnums;
import com.cdkjframework.util.tool.StringUtils;
import com.google.common.collect.Sets;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @ProjectName: cdkjframework
 * @Package: com.cdkjframework.minio
 * @ClassName: MinioUtils
 * @Description: minio工具类
 * @Version: 1.0
 */
public class MinioUtils {

    /**
     * 默认的临时文件存储桶
     */
    private static final String DEFAULT_TEMP_BUCKET_NAME = "temp-bucket";

    /**
     * minio客户端
     */
    private static MinioClient client = null;

    /**
     * 构造函数
     */
    public MinioUtils(MinioClient client) {
        MinioUtils.client = client;
    }

    /**
     * 将URLDecoder编码转成UTF8
     *
     * @param str 待转码的字符串
     * @return 转码后的字符串
     */
    public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
        String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
        return URLDecoder.decode(url, StandardCharsets.UTF_8);
    }

    /**
     * 把路径开头的"/"去掉,并在末尾添加"/",这个是minio对象名的样子。
     *
     * @param projectPath 以"/"开头、以字母结尾的路径
     * @return 去掉开头"/"
     */
    private static String trimHead(String projectPath) {
        return projectPath.substring(IntegerConsts.ONE);
    }

    /**
     * 把路径开头的"/"去掉,并在末尾添加"/",这个是minio对象名的样子。
     *
     * @param projectPath 以"/"开头、以字母结尾的路径
     * @return 添加结尾"/"
     */
    private static String addTail(String projectPath) {
        return projectPath + StringUtils.BACKSLASH;
    }

    /**
     * 获取数值的位数(用于构造临时文件名)
     *
     * @param number
     * @return
     */
    private static int countDigits(int number) {
        if (number == IntegerConsts.ZERO) {
            // 0 本身有一位
            return IntegerConsts.ONE;
        }
        int count = IntegerConsts.ZERO;
        while (number != IntegerConsts.ZERO) {
            number /= IntegerConsts.TEN;
            count++;
        }
        return count;
    }

    /**
     * 检查是否空
     *
     * @param objects 待检查的对象
     */
    private static void checkNull(Object... objects) {
        for (Object o : objects) {
            if (o == null) {
                throw new GlobalRuntimeException("Null param");
            }
            if (o instanceof String && StringUtils.isNullAndSpaceOrEmpty(o)) {
                throw new GlobalRuntimeException("Empty string");
            }
        }
    }

    /**
     * 给定一个字符串,返回其"/"符号后面的字符串
     *
     * @param input 输入字符串
     * @return 截取后的字符串
     */
    private static String getContentAfterSlash(String input) {
        if (StringUtils.isNullAndSpaceOrEmpty(input)) {
            return StringUtils.Empty;
        }
        int slashIndex = input.indexOf(StringUtils.BACKSLASH);
        if (slashIndex != IntegerConsts.MINUS_ONE && slashIndex < input.length() - IntegerConsts.ONE) {
            return input.substring(slashIndex + IntegerConsts.ONE);
        }
        return StringUtils.Empty;
    }

    /**
     * 判断Bucket是否存在
     *
     * @return true:存在,false:不存在
     */
    private static boolean bucketExists(String bucketName) throws Exception {
        return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 如果一个桶不存在,则创建该桶
     */
    public static void createBucket(String bucketName) throws Exception {
        if (!bucketExists(bucketName)) {
            client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 获取 Bucket 的相关信息
     */
    public static Optional<Bucket> getBucketInfo(String bucketName) throws Exception {
        return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * 使用MultipartFile进行文件上传
     *
     * @param bucketName  存储桶
     * @param file        文件
     * @param fileName    对象名
     * @param contentType 类型
     * @return
     * @throws Exception
     */
    public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,
                                                                                             String fileName, ContentTypeEnums contentType) throws Exception {
        InputStream inputStream = file.getInputStream();
        return client.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .contentType(contentType.getValue())
                        .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE)
                        .build());
    }

    /**
     * 将文件进行分片上传
     * <p>有一个未处理的bug(虽然概率很低很低):</p>
     * 当两个线程同时上传md5相同的文件时,由于两者会定位到同一个桶的同一个临时目录,两个线程会相互产生影响!
     *
     * @param file        分片文件
     * @param currIndex   当前文件的分片索引
     * @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)
     * @param md5         整体文件MD5
     * @return 剩余未上传的文件索引集合
     */
    public static ResponseBuilder uploadFileFragment(MultipartFile file,
                                                                                                     Integer currIndex, Integer totalPieces, String md5) throws Exception {
        checkNull(currIndex, totalPieces, md5);
        // 把当前分片上传至临时桶
        if (!bucketExists(DEFAULT_TEMP_BUCKET_NAME)) {
            createBucket(DEFAULT_TEMP_BUCKET_NAME);
        }
        uploadFileStream(DEFAULT_TEMP_BUCKET_NAME, getFileTempPath(md5, currIndex, totalPieces), file.getInputStream());
        // 得到已上传的文件索引
        Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), Boolean.FALSE);
        Set<Integer> savedIndex = Sets.newHashSet();
        boolean fileExists = Boolean.FALSE;
        for (Result<Item> item : results) {
            Integer idx = Integer.valueOf(getContentAfterSlash(item.get().objectName()));
            if (currIndex.equals(idx)) {
                fileExists = Boolean.TRUE;
            }
            savedIndex.add(idx);
        }
        // 得到未上传的文件索引
        Set<Integer> remainIndex = Sets.newTreeSet();
        for (int i = IntegerConsts.ZERO; i < totalPieces; i++) {
            if (!savedIndex.contains(i)) {
                remainIndex.add(i);
            }
        }
        if (fileExists) {
            return ResponseBuilder.failBuilder("index [" + currIndex + "] exists");
        }
        // 还剩一个索引未上传,当前上传索引刚好是未上传索引,上传完当前索引后就完全结束了。
        if (remainIndex.size() == IntegerConsts.ONE && remainIndex.contains(currIndex)) {
            return ResponseBuilder.successBuilder("completed");
        }
        return ResponseBuilder.failBuilder("index [" + currIndex + "] has been uploaded");
    }

    /**
     * 合并分片文件,并放到指定目录
     * 前提是之前已把所有分片上传完毕。
     *
     * @param bucketName  目标文件桶名
     * @param targetName  目标文件名(含完整路径)
     * @param totalPieces 切片总数(对于同一个文件,请确保切片总数始终不变)
     * @param md5         文件md5
     * @return minio原生对象,记录了文件上传信息
     */
    public static boolean composeFileFragment(String bucketName, String targetName,
                                                                                        Integer totalPieces, String md5) throws Exception {
        checkNull(bucketName, targetName, totalPieces, md5);
        // 检查文件索引是否都上传完毕
        Iterable<Result<Item>> results = getFilesByPrefix(DEFAULT_TEMP_BUCKET_NAME, md5.concat(StringUtils.BACKSLASH), false);
        Set<String> savedIndex = Sets.newTreeSet();
        for (Result<Item> item : results) {
            savedIndex.add(item.get().objectName());
        }
        if (savedIndex.size() == totalPieces) {
            // 文件路径 转 文件合并对象
            List<ComposeSource> sourceObjectList = savedIndex.stream()
                    .map(filePath -> ComposeSource.builder()
                            .bucket(DEFAULT_TEMP_BUCKET_NAME)
                            .object(filePath)
                            .build())
                    .collect(Collectors.toList());
            ObjectWriteResponse objectWriteResponse = client.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucketName)
                            .object(targetName)
                            .sources(sourceObjectList)
                            .build());
            // 上传成功,则删除所有的临时分片文件
            List<String> filePaths = Stream.iterate(IntegerConsts.ZERO, i -> ++i)
                    .limit(totalPieces)
                    .map(i -> getFileTempPath(md5, i, totalPieces))
                    .collect(Collectors.toList());
            Iterable<Result<DeleteError>> deleteResults = removeFiles(DEFAULT_TEMP_BUCKET_NAME, filePaths);
            // 遍历错误集合(无元素则成功)
            for (Result<DeleteError> result : deleteResults) {
                DeleteError error = result.get();
                System.err.printf("[Bigfile] 分片'%s'删除失败! 错误信息: %s", error.objectName(), error.message());
            }
            return true;
        }
        throw new GlobalException("The fragment index is not complete. Please check parameters [totalPieces] or [md5]");
    }

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

    /**
     * 通过流上传文件
     *
     * @param bucketName  存储桶
     * @param fileName    文件名
     * @param inputStream 文件流
     */
    public static ObjectWriteResponse uploadFileStream(String bucketName, String fileName, InputStream inputStream) throws Exception {
        return client.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .stream(inputStream, inputStream.available(), IntegerConsts.MINUS_ONE)
                        .build());
    }

    /**
     * 判断文件是否存在
     *
     * @param bucketName 存储桶
     * @param fileName   文件名
     * @return true: 存在
     */
    public static boolean isFileExist(String bucketName, String fileName) {
        boolean exist = true;
        try {
            client.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileName).build());
        } catch (Exception e) {
            exist = false;
        }
        return exist;
    }

    /**
     * 判断文件夹是否存在
     *
     * @param bucketName 存储桶
     * @param folderName 目录名称:本项目约定路径是以"/"开头,不以"/"结尾
     * @return true: 存在
     */
    public boolean isFolderExist(String bucketName, String folderName) {
        // 去掉头"/",才能搜索到相关前缀
        folderName = trimHead(folderName);
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = client.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(folderName).recursive(false).build());
            for (Result<Item> result : results) {
                Item item = result.get();
                // 增加尾"/",才能匹配到目录名字
                String objectName = addTail(folderName);
                if (item.isDir() && objectName.equals(item.objectName())) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            exist = false;
        }
        return exist;
    }

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

    }

    /**
     * 获取文件信息, 如果抛出异常则说明文件不存在
     *
     * @param bucketName 存储桶
     * @param fileName   文件名称
     */
    public StatObjectResponse getFileStatusInfo(String bucketName, String fileName) throws Exception {
        return client.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }

    /**
     * 根据文件前缀查询文件
     *
     * @param bucketName 存储桶
     * @param prefix     前缀
     * @param recursive  是否使用递归查询
     * @return MinioItem 列表
     */
    public List<Item> getAllFilesByPrefix(String bucketName,
                                                                                String prefix,
                                                                                boolean recursive) throws Exception {
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = client.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 filePaths<String> 需要删除的文件列表
     * @return Result
     */
    public static Iterable<Result<DeleteError>> removeFiles(String bucketName, List<String> filePaths) {
        List<DeleteObject> objectPaths = filePaths.stream()
                .map(filePath -> new DeleteObject(filePath))
                .collect(Collectors.toList());
        return client.removeObjects(
                RemoveObjectsArgs.builder().bucket(bucketName).objects(objectPaths).build());
    }

    /**
     * 获取文件的二进制流
     *
     * @param bucketName 存储桶
     * @param fileName   文件名
     * @return 二进制流
     */
    public InputStream getFileStream(String bucketName, String fileName) throws Exception {
        return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
    }

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

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

    /**
     * 删除文件夹(未完成)
     *
     * @param bucketName 存储桶
     * @param fileName   路径
     */
    @Deprecated
    public void removeFolder(String bucketName, String fileName) throws Exception {
        // 加尾
        fileName = addTail(fileName);
        client.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }

    /**
     * 删除文件
     *
     * @param bucketName 存储桶
     * @param fileName   文件名称
     */
    public void removeFile(String bucketName, String fileName) throws Exception {
        // 掐头
        fileName = trimHead(fileName);
        client.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }

    /**
     * 获得文件外链
     *
     * @param bucketName 存储桶
     * @param fileName   文件名
     * @return url 返回地址
     * @throws Exception
     */
    public static String getPresignedObjectUrl(String bucketName, String fileName) throws Exception {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(fileName)
                .method(Method.GET).build();
        return client.getPresignedObjectUrl(args);
    }

    /**
     * 获取文件外链
     *
     * @param bucketName 存储桶
     * @param fileName   文件名
     * @param expires    过期时间 <=7 秒 (外链有效时间(单位:秒))
     * @return url
     * @throws Exception
     */
    public String getPresignedObjectUrl(String bucketName, String fileName, Integer expires) throws Exception {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .expiry(expires, TimeUnit.SECONDS)
                .bucket(bucketName)
                .object(fileName)
                .build();
        return client.getPresignedObjectUrl(args);
    }

    /**
     * 通过文件的md5,以及分片文件的索引,构造分片文件的临时存储路径
     *
     * @param md5         文件md5
     * @param currIndex   分片文件索引(从0开始)
     * @param totalPieces 总分片
     * @return 临时存储路径
     */
    private static String getFileTempPath(String md5, Integer currIndex, Integer totalPieces) {

        int zeroCnt = countDigits(totalPieces) - countDigits(currIndex);
        StringBuilder name = new StringBuilder(md5);
        name.append(StringUtils.BACKSLASH);
        for (int i = IntegerConsts.ZERO; i < zeroCnt; i++) {
            name.append(IntegerConsts.ZERO);
        }
        name.append(currIndex);
        return name.toString();
    }

    /**
     * 创建目录
     *
     * @param bucketName 存储桶
     * @param folderName 目录路径:本项目约定路径是以"/"开头,不以"/"结尾
     */
    public ObjectWriteResponse createFolder(String bucketName, String folderName) throws Exception {
        // 这是minio的bug,只有在路径的尾巴加上"/",才能当成文件夹。
        folderName = addTail(folderName);
        return client.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(folderName)
                        .stream(new ByteArrayInputStream(new byte[]{}), IntegerConsts.ZERO, IntegerConsts.MINUS_ONE)
                        .build());
    }

}

资源目录下新建 META-INF\spring

​ 该方式支持 Spring Boot 3.x

新建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports 内容

com.cdkjframework.minio.config.MinioAutoConfiguration
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT枫斗者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值