MinIO
MinIO
OSS 服务是一个文件服务了,很多云服务厂商都有提供这样的服务。阿里云 OSS
文件服务除了购买 OSS 服务之外,也可以自己搭建专业的文件服务器。搭建文件服务器曾经比较专业的做法是 FastDFS。不过 FastDFS 搭建比较麻烦。
1. MinIO 简介
MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务,它兼容亚马逊 S3 云存储服务接口,非常适合存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据等各种文件。
MinIO 是一个非常轻量的对象存储服务,而且 MinIO 的 Java 客户端和亚马逊的 S3 云存储服务客户端接口兼容。
MinIO 的特点:
-
兼容 Amazon S3:可以使用 MinIO SDK,MinIO Client,AWS SDK 和 AWS CLI 访问 MinIO 服务器。
-
较强的数据保护能力:MinIO 使用 Minio Erasure Code(纠删码)来防止硬件故障(多个节点宕机和位衰减)。
分布式MinIO至少需要4个盘,使用分布式MinIO自动引入了纠删码功能。
-
高度可用:MinIO 服务器可以容忍分布式配置中高达
(N/2)-1
节点故障。单机 MinIO 服务存在单点故障;但N节点的分布式 MinIO,只要有N/2节点在线,数据就是安全的。不过至少需要有 N/2+1 节点(Quorum)来创建新的对象。
例:一个8节点的MinIO集群,每个节点一块盘,就算4个节点宕机,这个集群仍然是可读的,不过需要5个节点才能写数据。
-
支持 Lambda 计算。
-
具有加密和防篡改功能:MinIO 为加密数据提供了机密性,完整性和真实性保证,而且性能开销微乎其微。使用 AES-256-GCM,ChaCha20-Poly1305 和 AES-CBC 支持服务器端和客户端加密。
-
可对接后端存储:除了 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
这里四个属性:
- endpoint:MinIO 的 API 通信地址。
- accessKey 和 secretKey 是通信的用户名和密码,跟网页上登录时的用户名密码一致。
- 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
关键点:
- 设置 Nginx 端口为 8888。
- 将 MinIO 映射到宿主机的数据卷,再次挂载到 Nginx 上去。