目录

一、安装

1、在k8s中安装minio单机版

(1)创建minio名称空间
kubectl create ns minio

我们下面的yaml文件用到了minio名称空间,所以需要先把该名称空间创建出来

(2)minio单机版安装yaml

在安装minio之前,以下几点需要先说明一下

yaml文件内容修改说明:大家只用把写注释的地方改成自己的,其他地方都不用变

安装命令:大家可以把下面yaml文件内容复制到minio.yaml中,然后上传到linux中,之后通过kubectl apply -f minio.yaml命令来安装minio

端口解释:对于端口部分解释一下,5000端口是供浏览器访问UI页面的,而9000端口是供客户端连接的

访问链接:安装完成之后,可以通过http://ip:port来访问minio,其中ip就是虚拟机ip,而端口就是5000端口对应的nodePort端口,比如下面yaml文件中的就是30427

登录信息:登录用户名和密码需要看yaml文件中的MINIO_ROOT_USERMINIO_ROOT_PASSWORD的value值,比如我的就是adminadmin123456

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
  # 名称空间
  namespace: minio
spec:
  # 副本数量,建议1个,集群版可能存在问题
  replicas: 1
  selector:
    matchLabels:
      app: minio
  serviceName: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
        - command:
            - /bin/sh
            - -c
            - minio server /data --console-address ":5000"
          env:
            - name: MINIO_ROOT_USER
              # 登录用户名,按照自己的来设置
              value: "admin"
            - name: MINIO_ROOT_PASSWORD
              # 登录密码,按照自己的来设置
              value: "admin123456"
          image: minio/minio:RELEASE.2022-03-22T02-05-10Z
          name: minio
          ports:
            - containerPort: 9000
              name: data
              protocol: TCP
            - containerPort: 5000
              name: console
              protocol: TCP
          volumeMounts:
            - mountPath: /data
              name: data
  volumeClaimTemplates:
    - apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteMany
        resources:
          requests:
            # MioIO存储空间大小
            storage: 5Gi
        # nfs动态挂载存储类名称
        storageClassName: "managed-nfs-storage-bakckup"

---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: minio
  name: minio
  namespace: minio
spec:
  ports:
    - name: data
      port: 9000
      protocol: TCP
      targetPort: 9000
      # 暴露端口
      nodePort: 30024
    - name: console
      port: 5000
      protocol: TCP
      targetPort: 5000
      # 暴露端口
      nodePort: 30427
  selector:
    app: minio
  type: NodePort

登录之后效果如下图:

在这里插入图片描述

二、代码

1、pom.xml(说明:minio所用依赖)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>minio-study-springboot</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--minio依赖-->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.4.0</version>
        </dependency>
        <!-- 解决报错:okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>
        <!--aws 文件上传工具包-->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.263</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、application.yml(说明:放置minio连接信息、minio文件上传限制)

# minio文件上传大小限制
spring:
  servlet:
    multipart:
      max-file-size: 1GB
      max-request-size: 1GB

# minio连接配置
minio:
  # minio通信地址
  endpoint: http://192.168.139.128:30024
  # minio登录用户名
  userName: admin
  # minio登录密码
  password: admin123456

3、MinioProperties.java(说明:读取minio连接信息)

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties("minio")
public class MinioProperties {

    /**
     * 访问地址 http://localhost:9000
     */
    private String endpoint;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 密码
     */
    private String password;
}

4、MinioConfig.java(说明:配置minio连接客户端类)

import com.atguigu.miniostudyspringboot.util.ParallelMinioClient;
import com.atguigu.miniostudyspringboot.util.ReflectUtils;
import io.minio.MinioAsyncClient;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * 传统连接MinIO方式
 * @author 明快de玄米61
 * @date   2022/12/15 14:22
 **/
@Data
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {

    @Resource
    private MinioProperties minioProperties;

    /**
     * 注入minio 客户端
     */
    @Bean
    public MinioClient minioClient() {

        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getUserName(), minioProperties.getPassword())
                .build();
    }

    @Bean
    @ConditionalOnBean({MinioClient.class})
    @ConditionalOnMissingBean(ParallelMinioClient.class)
    public ParallelMinioClient parallelMinioClient(MinioClient minioClient) {
        MinioAsyncClient asyncClient = ReflectUtils.getFieldValue(minioClient, "asyncClient");
        return new ParallelMinioClient(asyncClient);
    }
}

5、AmazonS3Config.java(说明:配置aws连接minio客户端类)

import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * AWS连接MioIO方式
 * @author 明快de玄米61
 * @date   2022/12/15 14:23
 **/
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class AmazonS3Config {

    @Resource
    private MinioProperties minioProperties;

    @Bean(name = "amazonS3")
    public AmazonS3 amazonS3() {
        //设置连接时的参数
        ClientConfiguration config = new ClientConfiguration();
        //设置连接方式,可选参数为HTTP和HTTPS
        boolean httpsFlag = minioProperties.getEndpoint().toLowerCase().startsWith("https");
        config.setProtocol(httpsFlag ? Protocol.HTTPS : Protocol.HTTP);
        //设置网络访问超时时间
        config.setConnectionTimeout(5000);
        config.setUseExpectContinue(true);
        AWSCredentials credentials = new BasicAWSCredentials(minioProperties.getUserName(), minioProperties.getPassword());
        //设置Endpoint
        AwsClientBuilder.EndpointConfiguration end_point = new AwsClientBuilder.EndpointConfiguration(minioProperties.getEndpoint(), Regions.US_EAST_1.name());
        AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
                .withClientConfiguration(config)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(end_point)
                .withPathStyleAccessEnabled(true).build();
        return amazonS3;
    }

}

6、ObjectItem.java(说明:接收Minio返回内容)

import lombok.Data;

@Data
public class ObjectItem {

    private String objectName;
    private Long size;
}

7、PartInfo.java(说明:转换分片信息,返回给前端)

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * 分片信息
 *
 * @author 明快de玄米61
 * @date 2022/12/15 15:59
 */
@Data
@Accessors(chain = true)
public class PartInfo implements Serializable {

    private int partNumber;

    private String etag;

    private String lastModified;

    private Long size;

}

8、Task.java(说明:分片上传任务)

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * 分片上传任务
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:24
 */
@Data
@Accessors(chain = true)
public class Task implements Serializable {

    // 任务id
    private String id;

    // 桶名称
    private String bucketName;

    // 文件名称
    private String fileName;

    // 存储服务器中的文件绝对路径
    private String remoteFileUrl;

    // 分片上传的uploadId
    private String uploadId;

    // 文件大小(byte)
    private Long fileSize;

    // 分片大小(byte)
    private Long chunkSize;

    // 分片数量
    private Long chunkNum;

    // 上传状态(正在上传:0;已暂停:1;上传成功:2;上传失败:3;终止上传:4)
    // 说明:暂停、开始都是前端控制,后端只是进行状态记录
    private String status;

}

9、AjaxResult.java(说明:控制层统一返回值)

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author ruoyi
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 状态类型
     */
    public enum Type
    {
        /** 成功 */
        SUCCESS(0),
        /** 警告 */
        WARN(301),
        /** 错误 */
        ERROR(500);
        private final int value;

        Type(int value)
        {
            this.value = value;
        }

        public int value()
        {
            return this.value;
        }
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param type 状态类型
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(Type type, String msg, Object data)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
        if (data != null)
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(Type.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(Type.ERROR, msg, data);
    }

}

10、Constants.java(说明:常量类)

/**
 * 常量类
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:13
 */
public class Constants {

    public static final String BUCKET_NAME = "knowledge";

}

11、ReflectUtils.java(说明:反射工具类)

import org.apache.commons.lang3.Validate;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
 * 
 * @author ruoyi
 */
@SuppressWarnings("rawtypes")
public class ReflectUtils
{

    /**
     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
     */
    @SuppressWarnings("unchecked")
    public static <E> E getFieldValue(final Object obj, final String fieldName)
    {
        Field field = getAccessibleField(obj, fieldName);
        if (field == null)
        {
            System.out.println("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
            return null;
        }
        E result = null;
        try
        {
            result = (E) field.get(obj);
        }
        catch (IllegalAccessException e)
        {
            System.out.printf("不可能抛出的异常%s\n", e.getMessage());
        }
        return result;
    }

    /**
     * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
     * 如向上转型到Object仍无法找到, 返回null.
     */
    public static Field getAccessibleField(final Object obj, final String fieldName)
    {
        // 为空不报错。直接返回 null
        if (obj == null)
        {
            return null;
        }
        Validate.notBlank(fieldName, "fieldName can't be blank");
        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass())
        {
            try
            {
                Field field = superClass.getDeclaredField(fieldName);
                makeAccessible(field);
                return field;
            }
            catch (NoSuchFieldException e)
            {
                continue;
            }
        }
        return null;
    }

    /**
     * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
     */
    public static void makeAccessible(Field field)
    {
        if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
                || Modifier.isFinal(field.getModifiers())) && !field.isAccessible())
        {
            field.setAccessible(true);
        }
    }

}

12、ParallelMinioClient.java(说明:将分片方法暴露出来)

import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.XmlParserException;
import io.minio.messages.Part;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;


/**
 * Created by TD on 2021/10/26
 * 扩展 MinioClient <很多protected 修饰符的分片方法,MinioClient实例对象无法使用,只能自定义类继承使用>
 * minio 大文件分片上传思路:
 * 1. 前端访问文件服务,请求上传文件,后端返回签名数据及uploadId
 * 2. 前端分片文件,携带签名数据及uploadId并发上传分片数据
 * 3. 分片上传完成后,访问合并文件接口,后台负责合并文件。
 */
public class ParallelMinioClient extends MinioAsyncClient {

    public ParallelMinioClient(MinioAsyncClient client) {
        super(client);
    }

    @Override
    public CompletableFuture<CreateMultipartUploadResponse> createMultipartUploadAsync(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams);
    }

    @Override
    public CompletableFuture<UploadPartResponse> uploadPartAsync(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<ListPartsResponse> listPartsAsync(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.listPartsAsync(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<ObjectWriteResponse> completeMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<AbortMultipartUploadResponse> abortMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.abortMultipartUploadAsync(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
    }
}

13、MinioUtil.java(说明:Minio连接客户端)

import com.atguigu.miniostudyspringboot.entity.ObjectItem;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Component
@Slf4j
public class MinioUtil {

    @Resource
    private MinioClient minioClient;

    @Resource
    private ParallelMinioClient parallelMinioClient;

    /**
     * 判断存储桶是否存在,不存在则创建
     *
     * @param bucketName 存储桶名称
     */
    public void existBucket(String bucketName) {
        try {
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 查看存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 桶是否存在
     */
    public boolean bucketExists(String bucketName) {
        boolean found;
        try {
            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            found = false;
            e.printStackTrace();
        }
        return found;
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称
     * @return 是否创建成功
     */
    public Boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 删除存储桶
     *
     * @param bucketName 存储桶名称
     * @return 是否删除成功
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断对象是否存在
     *
     * @param bucketName 存储桶名称
     * @param objectName MinIO中存储对象全路径
     * @return 对象是否存在
     */
    public boolean existObject(String bucketName, String objectName) {
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 文件上传
     *
     * @param bucketName 存储桶名称
     * @param file       文件
     * @return 桶中位置
     */
    public String upload(String bucketName, MultipartFile file) {
        MultipartFile[] fileArr = {file};
        List<String> fileNames = upload(bucketName, fileArr);
        return fileNames.size() == 0 ? null : fileNames.get(0);
    }

    /**
     * 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileList   文件列表
     * @return 桶中位置列表
     */
    public List<String> upload(String bucketName, List<MultipartFile> fileList) {
        MultipartFile[] fileArr = fileList.toArray(new MultipartFile[0]);
        return upload(bucketName, fileArr);
    }

    /**
     * description: 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileArr    文件列表
     * @return 桶中位置列表
     */
    public List<String> upload(String bucketName, MultipartFile[] fileArr) {
        // 保证桶一定存在
        existBucket(bucketName);
        // 执行正常操作
        List<String> bucketFileNames = new ArrayList<>(fileArr.length);
        for (MultipartFile file : fileArr) {
            // 获取桶中文件名称
            // 获取原始文件名称
            String originalFileName = file.getOriginalFilename();
            // 获取当前日期,格式例如:2020/11/04
            String datePath = new SimpleDateFormat("yyyy/MM-dd/HH").format(new Date());
            // 文件名称
            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            // 获取文件后缀
            String type;
            int index = originalFileName.lastIndexOf(46);
            if (index != -1) {
                type = originalFileName.substring(index + 1);
            } else {
                type = "unkown";
            }
            String bucketFileName = datePath + "/" + uuid + "." + type;

            // 推送文件到MinIO
            try (InputStream in = file.getInputStream()) {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(bucketFileName)
                        .stream(in, in.available(), -1)
                        .contentType(file.getContentType())
                        .build()
                );
            } catch (Exception e) {
                e.printStackTrace();
            }
            bucketFileNames.add(bucketFileName);
        }
        return bucketFileNames;
    }

    /**
     * 文件下载
     *
     * @param bucketName       存储桶名称
     * @param bucketFileName   桶中文件名称
     * @param originalFileName 原始文件名称
     * @param response         response对象
     */
    public void download(String bucketName, String bucketFileName, String originalFileName, HttpServletResponse response) {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
        try (GetObjectResponse objResponse = minioClient.getObject(objectArgs)) {
            byte[] buf = new byte[1024];
            int len;
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
                while ((len = objResponse.read(buf)) != -1) {
                    os.write(buf, 0, len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                response.setCharacterEncoding("utf-8");
                //设置强制下载不打开
                response.setContentType("application/force-download");
                // 设置附件名称编码
                originalFileName = new String(originalFileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
                // 设置附件名称
                response.addHeader("Content-Disposition", "attachment;fileName=" + originalFileName);
                // 写入文件
                try (ServletOutputStream stream = response.getOutputStream()) {
                    stream.write(bytes);
                    stream.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取已上传对象的文件流
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 文件流
     */
    public InputStream getFileStream(String bucketName, String bucketFileName) throws Exception {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
        return minioClient.getObject(objectArgs);
    }

    /**
     * 批量删除文件对象结果
     *
     * @param bucketName      存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 删除结果
     */
    public DeleteError removeObjectsResult(String bucketName, String bucketFileName) {
        List<DeleteError> results = removeObjectsResult(bucketName, Collections.singletonList(bucketFileName));
        return results.size() > 0 ? results.get(0) : null;
    }

    /**
     * 批量删除文件对象结果
     *
     * @param bucketName      存储桶名称
     * @param bucketFileNames 桶中文件名称集合
     * @return 删除结果
     */
    public List<DeleteError> removeObjectsResult(String bucketName, List<String> bucketFileNames) {
        Iterable<Result<DeleteError>> results = removeObjects(bucketName, bucketFileNames);
        List<DeleteError> res = new ArrayList<>();
        for (Result<DeleteError> result : results) {
            try {
                res.add(result.get());
            } catch (Exception e) {
                e.printStackTrace();
                log.error("遍历删除结果出现错误:" + e.getMessage());
            }
        }
        return res;
    }

    /**
     * 批量删除文件对象
     *
     * @param bucketName      存储桶名称
     * @param bucketFileNames 桶中文件名称集合
     */
    private Iterable<Result<DeleteError>> removeObjects(String bucketName, List<String> bucketFileNames) {
        List<DeleteObject> dos = bucketFileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
        return minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build());
    }

    /**
     * 查看文件对象
     *
     * @param bucketName 存储桶名称
     * @return 文件对象集合
     */
    public List<ObjectItem> listObjects(String bucketName) {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).build());
        List<ObjectItem> objectItems = new ArrayList<>();
        try {
            for (Result<Item> result : results) {
                Item item = result.get();
                ObjectItem objectItem = new ObjectItem();
                objectItem.setObjectName(item.objectName());
                objectItem.setSize(item.size());
                objectItems.add(objectItem);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return objectItems;
    }

    /**
     * 获取桶(桶类型:public)中文件访问url
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 访问url
     */
    public String getUploadedObjectUrlForPublicBucket(String bucketName, String bucketFileName) {
        return bucketName + "/" + bucketFileName;
    }

    /**
     * 获取桶(不限制桶类型)中文件访问url
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @param expiry         过期时间数量
     * @param timeUnit       过期时间单位
     * @return 访问url
     */
    public String getUploadedObjectUrl(String bucketName, String bucketFileName, Integer expiry, TimeUnit timeUnit) {
        GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucketName)
                .object(bucketFileName)
                .expiry(expiry, timeUnit)
                .build();
        try {
            return minioClient.getPresignedObjectUrl(urlArgs);
        } catch (Exception e) {
            log.error("获取已上传文件的 Url 失败:" + e.getMessage());
            return "";
        }
    }

    /**
     * 创建分片上传请求
     * @author 明快de玄米61
     * @date   2022/12/15 12:47
     * @param  bucketName 桶名称
     * @param  region 一般填null就行
     * @param  objectName MinIO中文件全路径
     * @param  headers 一般只需要设置“Content-Type”
     * @return CreateMultipartUploadResponse对象
     **/
    public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
        // 保证桶一定存在
        existBucket(bucketName);
        // 创建分片上传任务
        try {
            return parallelMinioClient.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 前端通过后端上传分片到MinIO
     * @author 明快de玄米61
     * @date   2022/12/15 12:51
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  objectName MinIO中文件全路径
     * @param  data 分片文件,只能接收RandomAccessFile、InputStream类型的,一般使用InputStream类型
     * @param  length 文件大小
     * @param  uploadId 文件上传uploadId
     * @param  partNumber 分片编号
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return UploadPartResponse对象
     **/
    public UploadPartResponse uploadPart(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取分片上传地址,前端直接上传分片到MinIO
     * @author 明快de玄米61
     * @date   2022/12/15 12:54
     * @param  bucketName MinIO桶名称
     * @param  ossFilePath MinIO中文件全路径
     * @param  queryParams 查询参数,一般只需要设置“uploadId”和“partNumber”
     * @return 分片上传地址
     **/
    public String getPreSignUploadUrl(String bucketName, String ossFilePath, Map<String, String> queryParams) {
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)
                            .bucket(bucketName)
                            .object(ossFilePath)
                            .expiry(60 * 60 * 24)
                            .extraQueryParams(queryParams)
                            .build());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取已上传的所有分片列表,可以为前端和completeMultipartUpload方法服务
     * @author 明快de玄米61
     * @date   2022/12/15 12:57
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  maxParts 最大分片数,一般填写10000即可
     * @param  partNumberMarker 直接填0即可
     * @param  uploadId 文件上传uploadId
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return ListPartsResponse对象
     **/
    public ListPartsResponse listParts(String bucketName, String region, String ossFilePath, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.listPartsAsync(bucketName, region, ossFilePath, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 12:50
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  uploadId 文件上传uploadId
     * @param  parts 分片信息
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return ObjectWriteResponse对象
     **/
    public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.completeMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, parts, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 删除MinIO中已有分片
     * @author 明快de玄米61
     * @date   2022/12/15 13:04
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  uploadId 文件上传uploadId
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return AbortMultipartUploadResponse对象
     **/
    public AbortMultipartUploadResponse abortMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.abortMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

14、SimpleController.java(说明:测试Minio的普通操作)

import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import io.minio.messages.DeleteError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 普通操作
 * @author 明快de玄米61
 * @date   2022/12/15 14:16
 * @param
 * @return
 **/
@RequestMapping("/simple")
@RestController
public class SimpleController {

    @Resource
    private MinioUtil minIoUtil;

    // 上传文件
    @PostMapping("/uploadFile")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        return minIoUtil.upload(Constants.BUCKET_NAME, file);
    }

    // 删除文件
    @GetMapping("/deleteFile")
    public DeleteError deleteFile(@RequestParam String bucketFileName) {
        return minIoUtil.removeObjectsResult(Constants.BUCKET_NAME, bucketFileName);
    }

    // 下载文件
    @GetMapping("/downloadFile")
    public void downloadFile(@RequestParam String bucketFileName, @RequestParam String originalFilename, HttpServletResponse response) {
        minIoUtil.download(Constants.BUCKET_NAME, bucketFileName, originalFilename, response);
    }

    // 获取文件临时分享地址
    @GetMapping("/shareUrl")
    public String shareUrl(@RequestParam String bucketFileName) {
        return minIoUtil.getUploadedObjectUrl(Constants.BUCKET_NAME, bucketFileName, 7, TimeUnit.DAYS);
    }
}

15、MinioShardingController.java(说明:测试原生Minio的分片上传操作)

import com.atguigu.miniostudyspringboot.entity.PartInfo;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.messages.Part;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * MioIO工具类分片上传
 * 说明:
 * 1、使用MinIO传统依赖即可,不用新增依赖
 * 2、配置MinioConfig即可
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:15
 */
@RequestMapping("/minio")
@RestController
public class MinioShardingController {

    /** 分片上传任务集合 **/
    private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();

    @Resource
    private MinioUtil minIoUtil;

    /**
     * 获取任务id
     * @author 明快de玄米61
     * @date   2022/12/15 14:31
     * @param  fileName 文件原始名称
     * @param  fileSize 文件大小(byte)
     * @param  chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
     * @return 任务id
     **/
    @GetMapping("/initTask")
    public AjaxResult initTask(
            @RequestParam String fileName,
            @RequestParam Long fileSize,
            @RequestParam Long chunkSize
    ) {
        // 获取uploadId
        String suffix = FilenameUtils.getExtension(fileName);
        String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
        String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        CreateMultipartUploadResponse response = minIoUtil.createMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                remoteFileUrl,
                headers,
                null
        );
        String uploadId = response.result().uploadId();

        // 创建分片上传任务
        long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
        Task task = new Task();
        task.setId(UUID.randomUUID().toString().replace("-", ""))
                .setBucketName(Constants.BUCKET_NAME)
                .setFileName(fileName)
                .setRemoteFileUrl(remoteFileUrl)
                .setUploadId(uploadId)
                .setFileSize(fileSize)
                .setChunkSize(chunkSize)
                .setChunkNum(chunkNum)
                .setStatus("0");
        TASK_LIST.add(task);
        return AjaxResult.success("操作成功" ,task.getId());
    }

    /**
     * 上传分片
     * 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
     * @author 明快de玄米61
     * @date   2022/12/15 14:56
     * @param  file 分片文件
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     **/
    @PostMapping("/uploadPart")
    public AjaxResult uploadPart(
            @RequestPart MultipartFile file,
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 上传分片
        try {
            UploadPartResponse response = minIoUtil.uploadPart(
                    task.getBucketName(),
                    null,
                    task.getRemoteFileUrl(),
                    file.getInputStream(),
                    file.getSize(),
                    task.getUploadId(),
                    partNumber,
                    null,
                    null
            );
            return AjaxResult.success();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.error();
    }

    /**
     * 获取分片上传地址
     * 说明:前端直接上传分片到MinIO,对比:uploadPart
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     * @return
     **/
    @GetMapping("/preSignUploadUrl")
    public AjaxResult preSignUploadUrl(
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 获取分片上传地址
        Map<String, String> reqParams = new HashMap<>();
        reqParams.put("uploadId", task.getUploadId());
        reqParams.put("partNumber", partNumber.toString());
        String preSignUploadUrl = minIoUtil.getPreSignUploadUrl(
                Constants.BUCKET_NAME,
                task.getRemoteFileUrl(),
                reqParams
        );
        return AjaxResult.success("操作成功", preSignUploadUrl);
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/merge")
    public AjaxResult merge(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断已上传分片数量
        ListPartsResponse listPartsResponse = minIoUtil.listParts(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                10000,
                0,
                task.getUploadId(),
                null,
                null
        );
        List<Part> parts = listPartsResponse.result().partList();
        if (parts.size() != task.getChunkNum()) {
            return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
        }

        // 合并分片
        ObjectWriteResponse objectWriteResponse = minIoUtil.completeMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                task.getUploadId(),
                parts.toArray(new Part[0]),
                null,
                null
        );

        // 更新任务状态
        task.setStatus("1");

        return AjaxResult.success();
    }

    /**
     * 终止分片上传
     * 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
     * @author 明快de玄米61
     * @date   2022/12/15 16:08
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/abort")
    public AjaxResult abort(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 终止分片上传
        AbortMultipartUploadResponse response = minIoUtil.abortMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                task.getUploadId(),
                null,
                null
        );

        // 更新任务状态
        task.setStatus("4");

        return AjaxResult.success();
    }

    /**
     * 获取任务中以上传的分片列表,用于断点续传
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/taskInfo")
    public AjaxResult taskInfo(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断文件是否合成
        boolean exist = minIoUtil.existObject(Constants.BUCKET_NAME, task.getRemoteFileUrl());
        if (exist) {
            return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
        }

        // 查询分片集合
        ListPartsResponse listPartsResponse = minIoUtil.listParts(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                10000,
                0,
                task.getUploadId(),
                null,
                null
        );
        // 返回结果格式转换
        List<PartInfo> result = listPartsResponse.result().partList().stream().map(p -> {
            String lastModified = p.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS"));
            return new PartInfo().setSize(p.partSize()).setEtag(p.etag()).setLastModified(lastModified).setPartNumber(p.partNumber());
        }).collect(Collectors.toList());

        return AjaxResult.success(result);
    }

    /**
     * 获取上传分片任务集合
     * @author 明快de玄米61
     * @date   2022/12/15 14:54
     * @return 上传分片任务集合
     **/
    @GetMapping("/getTaskList")
    public List<Task> getTaskList(){
        return TASK_LIST;
    }

    /**
     * 根据任务id获取上传分片任务
     * @author 明快de玄米61
     * @date   2022/12/15 14:55
     * @param  id 任务id
     * @return 上传分片任务
     **/
    @GetMapping("/getTaskById")
    public Task getTaskById(@RequestParam String id){
        for (Task task : TASK_LIST) {
            if (task.getId().equals(id)) {
                return task;
            }
        }
        return null;
    }

}

16、AwsShardingController.java(说明:测试AWS分片上传操作)

import cn.hutool.core.date.DateUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * AWS分片上传
 * 说明:
 * 1、需要新增以下依赖
 * <dependency>
 *     <groupId>com.amazonaws</groupId>
 *     <artifactId>aws-java-sdk-s3</artifactId>
 *     <version>1.12.263</version>
 * </dependency>
 * 2、新增AmazonS3Config配置类
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:15
 */
@RequestMapping("/aws")
@RestController
public class AwsShardingController {

    /** 分片上传任务集合 **/
    private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();

    @Resource
    private AmazonS3 amazonS3;

    /**
     * 获取任务id
     * @author 明快de玄米61
     * @date   2022/12/15 14:31
     * @param  fileName 文件原始名称
     * @param  fileSize 文件大小(byte)
     * @param  chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
     * @return 任务id
     **/
    @GetMapping("/initTask")
    public AjaxResult initTask(
            @RequestParam String fileName,
            @RequestParam Long fileSize,
            @RequestParam Long chunkSize
    ) {
        // 获取uploadId
        String suffix = FilenameUtils.getExtension(fileName);
        String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
        String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3
                .initiateMultipartUpload(new InitiateMultipartUploadRequest(Constants.BUCKET_NAME, remoteFileUrl).withObjectMetadata(objectMetadata));
        String uploadId = initiateMultipartUploadResult.getUploadId();

        // 创建分片上传任务
        long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
        Task task = new Task();
        task.setId(UUID.randomUUID().toString().replace("-", ""))
                .setBucketName(Constants.BUCKET_NAME)
                .setFileName(fileName)
                .setRemoteFileUrl(remoteFileUrl)
                .setUploadId(uploadId)
                .setFileSize(fileSize)
                .setChunkSize(chunkSize)
                .setChunkNum(chunkNum)
                .setStatus("0");
        TASK_LIST.add(task);

        return AjaxResult.success("操作成功" ,task.getId());
    }

    /**
     * 上传分片
     * 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
     * @author 明快de玄米61
     * @date   2022/12/15 14:56
     * @param  file 分片文件
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     **/
    @PostMapping("/uploadPart")
    public AjaxResult uploadPart(
            @RequestPart MultipartFile file,
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 上传分片
        try {
            UploadPartRequest request = new UploadPartRequest();
            request.setInputStream(file.getInputStream());
            request.setBucketName(task.getBucketName());
            request.setKey(task.getRemoteFileUrl());
            request.setUploadId(task.getUploadId());
            request.setPartNumber(partNumber);
            request.setPartSize(file.getInputStream().available());
            UploadPartResult uploadPartResult = amazonS3.uploadPart(request);
            return AjaxResult.success(uploadPartResult);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.error();
    }

    /**
     * 获取分片上传地址
     * 说明:前端直接上传分片到MinIO,对比:uploadPart
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     * @return
     **/
    @GetMapping("/preSignUploadUrl")
    public AjaxResult preSignUploadUrl(
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 获取分片上传地址
        // 设置上传分片链接有效时间
        Date expireDate = DateUtil.offsetMillisecond(new Date(), 60 * 60 * 1000);
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(task.getBucketName(), task.getRemoteFileUrl())
                .withExpiration(expireDate).withMethod(HttpMethod.PUT);
        // 设置uploadId
        request.addRequestParameter("uploadId", task.getUploadId());
        // 设置分片数量
        request.addRequestParameter("partNumber", partNumber.toString());
        // 获取分片上传地址
        URL preSignUploadUrl = amazonS3.generatePresignedUrl(request);
        return AjaxResult.success("操作成功", preSignUploadUrl);
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/merge")
    public AjaxResult merge(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断已上传分片数量
        ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        PartListing partListing = amazonS3.listParts(listPartsRequest);
        List<PartSummary> parts = partListing.getParts();
        if (parts.size() != task.getChunkNum()) {
            return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
        }

        // 合并分片
        CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
                .withBucketName(task.getBucketName())
                .withKey(task.getRemoteFileUrl())
                .withUploadId(task.getUploadId())
                .withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
        CompleteMultipartUploadResult result = amazonS3.completeMultipartUpload(completeMultipartUploadRequest);

        // 更新任务状态
        task.setStatus("1");

        return AjaxResult.success(result);
    }

    /**
     * 终止分片上传
     * 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
     * @author 明快de玄米61
     * @date   2022/12/15 16:08
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/abort")
    public AjaxResult abort(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 终止分片上传
        AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        amazonS3.abortMultipartUpload(abortMultipartUploadRequest);

        // 更新任务状态
        task.setStatus("4");

        return AjaxResult.success();
    }

    /**
     * 获取任务中以上传的分片列表,用于断点续传
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/taskInfo")
    public AjaxResult taskInfo(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断文件是否合成
        boolean exist = amazonS3.doesObjectExist(task.getBucketName(), task.getRemoteFileUrl());
        if (exist) {
            return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
        }

        // 查询分片集合
        ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        PartListing partListing = amazonS3.listParts(listPartsRequest);

        return AjaxResult.success(partListing.getParts());
    }

    /**
     * 获取上传分片任务集合
     * @author 明快de玄米61
     * @date   2022/12/15 14:54
     * @return 上传分片任务集合
     **/
    @GetMapping("/getTaskList")
    public List<Task> getTaskList(){
        return TASK_LIST;
    }

    /**
     * 根据任务id获取上传分片任务
     * @author 明快de玄米61
     * @date   2022/12/15 14:55
     * @param  id 任务id
     * @return 上传分片任务
     **/
    @GetMapping("/getTaskById")
    public Task getTaskById(@RequestParam String id){
        for (Task task : TASK_LIST) {
            if (task.getId().equals(id)) {
                return task;
            }
        }
        return null;
    }

}

三、测试

1、普通操作(对应:SimpleController.java)

1.1、上传文件

在这里插入图片描述

1.2、删除文件

在这里插入图片描述

1.3、下载文件

在这里插入图片描述

1.4、获取文件临时分享地址

在这里插入图片描述

2、Minio分片上传(对应:MinioShardingController)

2.1、初始化分片上传任务

返回值中data里面是任务id
在这里插入图片描述

2.2、分片上传

有多少个分片就需要调用多少次该接口

在这里插入图片描述

2.3、查看任务中已上传分片列表

该接口可以帮助前端判断未上传分片,做到断点续传

在这里插入图片描述

2.4、合并分片

所有分片上传完毕之后,前端调用该接口进行分片合并

在这里插入图片描述

2.5、终止分片

假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口

在这里插入图片描述

3、AWS分片上传(对应:AwsShardingController)

3.1、初始化分片上传任务

返回值中data里面是任务id
在这里插入图片描述

3.2、分片上传

有多少个分片就需要调用多少次该接口

在这里插入图片描述

3.3、查看任务中已上传分片列表

该接口可以帮助前端判断未上传分片,做到断点续传

在这里插入图片描述

3.4、合并分片

所有分片上传完毕之后,前端调用该接口进行分片合并

在这里插入图片描述

3.5、终止分片

假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口

在这里插入图片描述

四、资料

  1. MinIO的Java Client API参考文档

五、拓展—分片上传

1、前端分片,后端组合分片(适用范围:本地存储)

推荐大家去看奇闻网盘qiwen-file 项目,该项目中分片上传接口方法如下所示:

类全路径名称: com.qiwenshare.file.controller.FiletransferController

方法名: uploadFile

如果你去看源码,需要拉取这几个项目:

奇文网盘项目实现了前端分片之后,后端实现了以下几种存储方式:

  • 本地存储:后端接收分片,并按照顺序在本地组合分片,组合完成后上传到本地存储位置
  • 阿里云OSS存储:后端接收分片,并上传分片到阿里云,当所有分片接收完成后,调用阿里云组合分片接口进行分片组合
  • FastDFS存储:后端接收分片,并上传分片到FastDFS,当所有分片接收完成后,那就上传完成了
  • MinIO存储:后端接收分片,并按照顺序在本地组合分片,组合完成后在上传到MinIO;这种方式我是不推荐的,原因是MinIO支持分片上传,那我们可以直接把分片上传到MinIO,最终分片上传完成后调用MinIO进行分片合并就好了,可以极大解决本地存储空间

2、前端分片,后端把分片传递给文件存储服务器,调用文件存储服务器的接口组合分片(适用范围:Minio、阿里云OSS存储等)

推荐大家去看暴走的咖喱minio-upload项目,以及明珠spring-boot-minio项目,我们下面来详细介绍一下这两个项目的区别,最后我在补充一些我在项目中用到的其他内容

2.1、minio-upload

项目: https://gitee.com/Gary2016/minio-upload

博客: https://blog.csdn.net/weixin_44359036/article/details/126514643

说明: 这个项目后端没有使用传统连接Minio的方式,而是使用AWS SDK for Java来操作MinIO Server,这种方式也是MinIO官方文档中推荐的方式之一。这个项目我是真正跑起来的,并且实现了分片上传功能,大家按照作者的README.md中的说明文档操作即可,该项目中的一个出色点是获取contentType,这可以让文件上传到文件存储服务器之后保证文件、文件格式都正确,目前没有看到其他项目做到这一点,具体代码如下:

import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;

// 作者在getMediaType方法中写的是key,而我写的是fileName,其实作用是类似的,只要参数中有后缀就好了
String contentType = MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);

另外它对流程的把控也是非常nice的,基本流程是创建上传任务、根据分片获取上传地址(上传地址有实效限制,但是这个时间点足够了)、上传分片、合并分片,另外还可以查询已上传分片列表,做到断点续传功能

2.2、spring-boot-minio

项目: https://gitee.com/pearl88/study-demo/tree/master/spring-boot-minio

博客: https://blog.csdn.net/qq_43437874/article/details/123429986

说明: 这个项目后端使用传统连接Minio的方式,虽然在新的minio依赖中,这种方式已经失效了,比如在我上面的minio版本中就已经失效了,但是这种思想依然没有改变的。不过该项目的不好的一点是对contentType的处理,仅仅是简单使用application/octet-stream来代表所有文件的类型,这种方式将会造成上传文本文件多处一些多余内容,所以建议使用上面介绍minio-upload提到的contentType获取方法;另外不好的一点是对上传地址的获取方式,在该项目中是直接在创建上传任务的时候返回所有分片的上传地址,不过这个上传地址有效时间是固定的,并且有最大值,但是用户可能将任务暂停很久很久,所以不建议使用这种方式,而建议使用上传哪个分片就先获取分片上传地址的方式

2.3、公司项目

我在项目中借鉴的minio-upload的做法,也是使用AWS SDK for Java来操作MinIO Server,主要补充两点

1、删除已上传分片

在实际使用中,大文件上传过程中肯定是允许用户中途删除文件的,这种情况下我们需要删除Minio中的分片,避免这些垃圾分片占用存储空间

// 参数解释:MinIO桶名称、文件在MinIO中存储的绝对路径、文件上传uploadId
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(record.getBucketName(), record.getRemoteFileUrl(), record.getUploadId());
amazonS3.abortMultipartUpload(abortMultipartUploadRequest);

上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#abortMultipartUploadAsync

2、前端通过后端上传分片到MinIO

我借鉴的是minio-upload项目中的思路,所以我让前端通过Minio返回的分片上传地址上传分片到MinIO,但是前端那边报跨域,另外公司前辈说不建议把MinIO的连接信息暴露给前端,另外以后K8S上部署的MinIO甚至都不会以NodePort方式暴露出来,那这种方式就无法使用了,所以最好还是前端通过后端上传分片到Minio最好,我转念一想大佬说的也有道理,那就我后端中转一下吧,具体代码如下:

Controller@PostMapping("/uploadPart")
@ApiOperation(value = "上传分片", notes = "上传分片")
public CommonRes<UploadPartResult> uploadPart(
        @ApiParam(value = "分片文件", required = true) @RequestPart("file") @Validated MultipartFile file,
        @ApiParam(value = "主键id,用于获取上传信息", required = true) @RequestParam(value = "id") @Validated String id,
        @ApiParam(value = "分片编号", required = true) @RequestParam(value = "partNumber") @Validated() Integer partNumber
) {
    return manager.uploadPart(file, id, partNumber);
}

ServiceImpl@SneakyThrows
@Override
public CommonRes<UploadPartResult> uploadPart(MultipartFile file, String id, Integer partNumber) {
    // 参数判断
    UploadRecord record = service.getById(id);
    if (record == null) {
        // 避免删除接口先调用成功,然后才接收到前端调用上传分片的接口请求,之后导致报错,因此设置状态码是200
        return CommonRes.error(HttpStatusConstants.SUCCESS, "上传记录不存在");
    }

    // 上传分片
    UploadPartRequest request = new UploadPartRequest();
    // 前端上传分片流
    request.setInputStream(file.getInputStream());
    // MioIO桶名称
    request.setBucketName(record.getBucketName());
    // MinIO中的最终合成文件全路径
    request.setKey(record.getRemoteFileUrl());
    // 文件分片上传uploadId
    request.setUploadId(record.getUploadId());
    // 分片编号
    request.setPartNumber(partNumber);
    // 分片大小
    request.setPartSize(file.getInputStream().available());
    // 上传分片到MinIO
    UploadPartResult uploadPartResult = amazonS3.uploadPart(request);

    return CommonRes.ok(uploadPartResult);
}

上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#uploadPartAsync

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐