文章

MinIO

MinIO

OSS 服务是一个文件服务了,很多云服务厂商都有提供这样的服务。阿里云 OSS

文件服务除了购买 OSS 服务之外,也可以自己搭建专业的文件服务器。搭建文件服务器曾经比较专业的做法是 FastDFS。不过 FastDFS 搭建比较麻烦。

1. MinIO 简介

MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务,它兼容亚马逊 S3 云存储服务接口,非常适合存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据等各种文件。

MinIO 是一个非常轻量的对象存储服务,而且 MinIO 的 Java 客户端和亚马逊的 S3 云存储服务客户端接口兼容。

MinIO 的特点:

  1. 兼容 Amazon S3:可以使用 MinIO SDK,MinIO Client,AWS SDK 和 AWS CLI 访问 MinIO 服务器。

  2. 较强的数据保护能力:MinIO 使用 Minio Erasure Code(纠删码)来防止硬件故障(多个节点宕机和位衰减)。

    分布式MinIO至少需要4个盘,使用分布式MinIO自动引入了纠删码功能。

  3. 高度可用:MinIO 服务器可以容忍分布式配置中高达 (N/2)-1 节点故障。

    单机 MinIO 服务存在单点故障;但N节点的分布式 MinIO,只要有N/2节点在线,数据就是安全的。不过至少需要有 N/2+1 节点(Quorum)来创建新的对象。

    例:一个8节点的MinIO集群,每个节点一块盘,就算4个节点宕机,这个集群仍然是可读的,不过需要5个节点才能写数据。

  4. 支持 Lambda 计算。

  5. 具有加密和防篡改功能:MinIO 为加密数据提供了机密性,完整性和真实性保证,而且性能开销微乎其微。使用 AES-256-GCM,ChaCha20-Poly1305 和 AES-CBC 支持服务器端和客户端加密。

  6. 可对接后端存储:除了 MinIO 自己的文件系统,还支持 DAS、 JBODs、NAS、Google 云存储和 Azure Blob 存储。

2. MinIO 安装

Docker 安装 MinIO:

1
docker run -p 9000:9000 -p 9001:9001 -d --name minio -v /yueyazhui/minio/data:/data -v /yueyazhui/minio/config:/root/.minio -e "MINIO_ROOT_USER=minioadmin" -e "MINIO_ROOT_PASSWORD=minioadmin" minio/minio server /data --console-address ":9000" --address ":9001"

console-address 是后台管理的网页端口;address 则是 API 通信端口。

如果没有配置用户名和密码,默认的登录用户名和密码均为 minioadmin

登录成功之后,首先创建一个 bucket,将来上传的文件都处于 bucket 之中,如下:

创建成功之后,还需要设置一下桶的读取权限,确保文件上传成功之后可以读取到,点击左上角的设置按钮进行设置,如下:

设置完成后,接下来就可以往这个桶中上传文件了,如下图:

上传完成后,就可以看到刚刚上传的文件了:

上传成功后,点击文件的分享按钮会显示文件的访问链接,由于已经设置了文件可读,因此不需要链接后面拼接的有效期就可以直接访问,如下:

现在文件就可上传可访问了。是不是比 FastDFS 容易多了!

3. 整合 Spring Boot

MinIO 依赖:

1
2
3
4
5
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.8</version>
</dependency>

yaml 配置:

1
2
3
4
5
minio:
  endpoint: http://47.95.1.150:9001
  accessKey: minioadmin
  secretKey: minioadmin
  nginxHost: http://oss.yueyazhui.top

这里四个属性:

  1. endpoint:MinIO 的 API 通信地址。
  2. accessKey 和 secretKey 是通信的用户名和密码,跟网页上登录时的用户名密码一致。
  3. nginxHost:Nginx 的访问路径。

提供一个 MinioProperties 来接收这里的四个属性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
    /**
     * 连接地址
     */
    private String endpoint;
    /**
     * 用户名
     */
    private String accessKey;
    /**
     * 密码
     */
    private String secretKey;
    /**
     * 域名
     */
    private String nginxHost;
}

提供一个 MinioClient,通过这个客户端工具可以操作 MinIO,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {

    @Resource
    private MinioProperties minioProperties;

    /**
     * 获取 MinioClient
     */
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
    }
}

当文件上传成功之后,可以通过 MinIO 去访问,也可以通过 Nginx 访问,所以接下来需要提供一个类,来封装这两个地址:

1
2
3
4
5
6
7
@Data
public class UploadResponse {

    private String minioUrl;

    private String nginxUrl;
}

再提供一个 MinIO 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@Component
public class MinioUtil {

    @Resource
    private MinioProperties minioProperties;
    @Resource
    private MinioClient minioClient;

    /**
     * 获取全部桶
     */
    public List<Bucket> listBuckets() throws Exception {
        return minioClient.listBuckets();
    }

    /**
     * 根据桶名获取桶信息
     * @param bucketName 桶名
     */
    public Optional<Bucket> getBucket(String bucketName) throws Exception {
        return minioClient.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * 创建桶
     */
    public void makeBucket(String bucketName) throws Exception {
        if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

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

    /**
     * 获取⽂件外链
     * @param bucketName 桶名
     * @param objectName ⽂件名称
     * @param expires    过期时间 <= 7
     * @return ⽂件外链
     */
    public String getObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).expiry(expires).build());
    }

    /**
     * 获取⽂件流
     * @param bucketName 桶名
     * @param objectName ⽂件名称
     * @return ⼆进制流
     */
    public InputStream getObject(String bucketName, String objectName) throws Exception {
        return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 获取⽂件信息
     * @param bucketName bucket名称
     * @param objectName ⽂件名称
     */
    public StatObjectResponse statObject(String bucketName, String objectName) throws Exception {
        return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 上传文件
     */
    public UploadResponse uploadFile(MultipartFile file, String bucketName) throws Exception {
        // 判断文件是否为空
        if (ObjectUtil.isEmpty(file)) {
            return null;
        }
        // 判断桶是否存在,不存在则创建
        makeBucket(bucketName);
        // 文件名
        String originalFilename = file.getOriginalFilename();
        assert StrUtil.isBlank(originalFilename);
        // 新的文件名 = 存储桶文件名_时间戳_雪花ID.后缀名
        String fileName = bucketName + "_" + DateUtil.current() + "_" + IdUtil.getSnowflakeNextIdStr() + originalFilename.substring(originalFilename.lastIndexOf(StrPool.DOT));
        // 开始上传
        putObject(bucketName, fileName, file.getInputStream(), file.getSize(), file.getContentType());
        String minioUrl = minioProperties.getEndpoint() + StrPool.SLASH + bucketName + StrPool.SLASH + fileName;
        String nginxUrl = minioProperties.getNginxHost() + StrPool.SLASH + bucketName + StrPool.SLASH + fileName;
        return new UploadResponse(minioUrl, nginxUrl);
    }

    /**
     * 上传⽂件
     * @param bucketName 桶名称
     * @param objectName ⽂件名称
     * @param stream     ⽂件流
     */
    public void putObject(String bucketName, String objectName, InputStream stream) throws Exception {
        minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(objectName.substring(objectName.lastIndexOf(StrPool.DOT))).build());
    }

    /**
     * 上传⽂件
     * @param bucketName  桶名称
     * @param objectName  ⽂件名称
     * @param stream      ⽂件流
     * @param objectSize  ⼤⼩
     * @param contextType 类型
     */
    public void putObject(String bucketName, String objectName, InputStream stream, long objectSize, String contextType) throws Exception {
        minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, objectSize, -1).contentType(contextType).build());
    }

    /**
     * 删除⽂件
     * @param bucketName 桶名称
     * @param objectName ⽂件名称
     */
    public void removeObject(String bucketName, String objectName) throws Exception {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }
}

接口:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("minio")
public class MinioController {

    @Resource
    MinioUtil minioUtil;

    @PostMapping("/upload")
    public UploadResponse fileUpload(MultipartFile file) throws Exception {
        return minioUtil.uploadFile(file, "test");
    }
}

测试:

1
2
3
4
{
    "minioUrl": "http://47.95.1.150:9001/test/test_1709823967121_1765755816958898176.png",
    "nginxUrl": "http://oss.yueyazhui.top/test/test_1709823967121_1765755816958898176.png"
}

4. 配置 Nginx

安装 MinIO 时,做了数据卷映射,即上传到 MinIO 的文件实际上是保存在宿主机,所以也得给 Nginx 配置数据卷,让 Nginx 也去 /yueyazhui/minio/data 路径下查找文件。

Nginx 安装指令如下:

1
docker run --name minio_nginx -p 8888:80 -v /yueyazhui/minio/data:/usr/share/nginx/html:ro -d nginx

关键点:

  1. 设置 Nginx 端口为 8888。
  2. 将 MinIO 映射到宿主机的数据卷,再次挂载到 Nginx 上去。
本文由作者按照 CC BY 4.0 进行授权