一个简单易用的文件上传方案

现在 OSS 服务算是一个基础服务了,很多云服务厂商都有提供这样的服务,价格也不贵,松哥自己的 www.javaboy.org 用的就是类似的服务。

不过对于中小公司来说,除了购买 OSS 服务之外,也可以自己搭建专业的文件服务器,自己搭建专门的文件服务器的话,曾经比较专业的做法是 FastDFS,松哥之前也专门为之录过视频发在 B 站上,感兴趣的小伙伴可以自行查看。不过 FastDFS 搭建比较麻烦,非常容易出错,所以对各位小伙伴来说多多少少有一点门槛。

松哥在之前的文章录制的一些项目视频中,如果涉及到文件上传,基本上都是保存在项目本地,这种方式比较省事,但是安全性不高。

所以,今天给大伙介绍一个较好的玩意 MinIO,看看这个工具带给我们什么惊喜。

1. MinIO 简介

MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 KB 到最大 5T 不等。

MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

简单来说,可以使用 MinIO 来搭建一个对象存储服务,而且 MinIO 的 Java 客户端和亚马逊的 S3 云存储服务客户端接口兼容,换句话说,你会往 MinIO 上存数据,就会往 S3 上存数据。

MinIO 的特点:

  1. 兼容 Amazon S3:可以使用 MinIO SDK,MinIO Client,AWS SDK 和 AWS CLI 访问 MinIO 服务器。
  2. 较强的数据保护能力:MinIO 使用 Minio Erasure Code 来防止硬件故障。
  3. 高度可用:MinIO 服务器可以容忍分布式设置中高达(N/2)-1 节点故障。
  4. 支持 Lambda 计算。
  5. 具有加密和防篡改功能:MinIO 为加密数据提供了机密性,完整性和真实性保证,而且性能开销微乎其微。使用 AES-256-GCM,ChaCha20-Poly1305 和 AES-CBC 支持服务器端和客户端加密。
  6. 可对接后端存储:除了 MinIO 自己的文件系统,还支持 DAS、 JBODs、NAS、Google 云存储和 Azure Blob 存储。

2. MinIO 安装

不废话了,赶紧装一个体验一把吧。

为了省事,咱们就直接用 docker 来安装吧,如果你对 docker 还不熟悉,公众号后台回复 docker 获取松哥的 docker 教程。

我们执行如下命令,安装 MinIO:

1
docker run -p 9000:9000 -p 9001:9001 -d minio/minio server /data --console-address ":9000" --address ":9001"

这个启动命令中配置了两个端口:console-address 是后台管理的网页端口;address 则是 API 通信端口。以上面的启动脚本为例,项目启动成功后,网页上的访问端口是 9000,如果我们通过 Java 代码上传文件,通信端口则是 9001。

项目启动成功后,浏览器地址栏输入 http://127.0.0.1:9000/login 即可访问到 MinIO 的后端页面:

默认的登录用户名和密码均为 minioadmin

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

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

设置完成后,接下来我们就可以往这个桶中上传资源了,如下图:

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

上传成功后,点击文件,然后点击右边的 Share 按钮会弹出来文件的访问链接,由于我们已经设置了文件可读,因此可以不用管这里的链接有效期了,直接通过路径的前面部分就可以访问到刚刚上传的图片了,如下:

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

不过前面这种安装方式其实有点小问题,因为我们没有为 docker 容器设置数据卷,所以如果你把 docker 容器不小心删除了,那么数据也就没了!

所以我们要设置数据卷。

修正后的 docker 脚本如下:

1
docker run -p 9000:9000 -p 9001:9001 -d --name minio -v /Users/sang/minio/data:/data -v /Users/sang/minio/config:/root/.minio -e "MINIO_ROOT_USER=javaboy" -e "MINIO_ROOT_PASSWORD=123@45678" minio/minio server /data --console-address ":9000" --address ":9001"

主要是加了数据卷映射功能,将 MinIO 的数据和配置文件映射到宿主机上,这样将来即使容器删除了,数据也都还在。

注意上面也自定义了登录用户名和密码。

按照上面的命令,重新创建容器之后,我们也创建一个桶并上传文件,上传成功之后,我们就可以在本地对应的文件夹看到我们上传的文件,如下:

3. 整合 Spring Boot

接下来我们再来看看在 Spring Boot 中如何玩 MinIO。

首先我们创建一个 Spring Boot 项目,引入 Web 依赖,如下:

项目创建成功之后,我们再来手动添加一下 MinIO 的依赖,如下:

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

这里我尝试用了最新的版本,但是似乎有一些 BUG,我也没有深究,就换了 8.2.1 这个版本,这个版本是 OK 的。

接下来我们来配置一下 application.yaml,配置一下文件上传所需要的基本信息:

1
2
3
4
5
minio:
endpoint: http://localhost:9001
accessKey: javaboy
secretKey: 123@45678
nginxHost: http://local.javaboy.org:9001

这里四个属性:

  1. endpoint:这是 MinIO 的 API 通信地址。
  2. accessKey 和 secretKey 是通信的用户名和密码,这跟网页上登录时候的用户名密码一致。
  3. nginxHost:这个配置用来生成上传文件的访问路径。对于这个路径,有的小伙伴可能会有疑问,nginxHost 不就是 endpoint 吗?为什么还要单独配置?因为对于文件服务器而言,我们上传文件是通过 MinIO,但是访问的时候不一定通过 MinIO,我们可能会自己搭建一个 Nginx 服务器,通过 Nginx 服务器来访问上传后的资源,大家知道 Nginx 非常擅长于做这个事情,效率非常高。所以这里的 nginxHost 其实是指 Nginx 的访问路径。

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

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
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
/**
* 连接地址
*/
private String endpoint;
/**
* 用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;
/**
* 域名
*/
private String nginxHost;

public String getEndpoint() {
return endpoint;
}

public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}

public String getAccessKey() {
return accessKey;
}

public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}

public String getSecretKey() {
return secretKey;
}

public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}

public String getNginxHost() {
return nginxHost;
}

public void setNginxHost(String nginxHost) {
this.nginxHost = nginxHost;
}
}

将 application.yaml 中相关的配置注入到这个配置类中来。

接下来我们需要提供一个 MinIOClient,通过这个客户端工具可以操作 MinIO,如下:

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

@Autowired
private MinioProperties minioProperties;

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

}

这个也没啥好说的,传入通信地址以及用户名密码,就可以构建出一个 MinioClient 出来。

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

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
public class UploadResponse {
private String minIoUrl;

private String nginxUrl;

public UploadResponse() {
}

public UploadResponse(String minIoUrl, String nginxUrl) {
this.minIoUrl = minIoUrl;
this.nginxUrl = nginxUrl;
}

public String getMinIoUrl() {
return minIoUrl;
}

public void setMinIoUrl(String minIoUrl) {
this.minIoUrl = minIoUrl;
}

public String getNginxUrl() {
return nginxUrl;
}

public void setNginxUrl(String nginxUrl) {
this.nginxUrl = 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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
@Component
public class MinioUtil {

@Autowired
private MinioProperties minioProperties;

@Autowired
private MinioClient client;

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

/**
* 上传文件
*/
public UploadResponse uploadFile(MultipartFile file, String bucketName) throws Exception {
//判断文件是否为空
if (null == file || 0 == file.getSize()) {
return null;
}
//判断存储桶是否存在 不存在则创建
createBucket(bucketName);
//文件名
String originalFilename = file.getOriginalFilename();
//新的文件名 = 存储桶文件名_时间戳.后缀名
assert originalFilename != null;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
String fileName = bucketName + "_" +
System.currentTimeMillis() + "_" + format.format(new Date()) + "_" + new Random().nextInt(1000) +
originalFilename.substring(originalFilename.lastIndexOf("."));
//开始上传
client.putObject(
PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(
file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
String url = minioProperties.getEndpoint() + "/" + bucketName + "/" + fileName;
String urlHost = minioProperties.getNginxHost() + "/" + bucketName + "/" + fileName;
return new UploadResponse(url, urlHost);
}

/**
* 获取全部bucket
*
* @return
*/
public List<Bucket> getAllBuckets() throws Exception {
return client.listBuckets();
}

/**
* 根据bucketName获取信息
*
* @param bucketName bucket名称
*/
public Optional<Bucket> getBucket(String bucketName) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, InvalidResponseException, InternalException, ErrorResponseException, ServerException, XmlParserException, ServerException {
return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}

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

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

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

/**
* 上传⽂件
*
* @param bucketName bucket名称
* @param objectName ⽂件名称
* @param stream ⽂件流
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
*/
public void putObject(String bucketName, String objectName, InputStream stream) throws
Exception {
client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(objectName.substring(objectName.lastIndexOf("."))).build());
}

/**
* 上传⽂件
*
* @param bucketName bucket名称
* @param objectName ⽂件名称
* @param stream ⽂件流
* @param size ⼤⼩
* @param contextType 类型
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
*/
public void putObject(String bucketName, String objectName, InputStream stream, long
size, String contextType) throws Exception {
client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, size, -1).contentType(contextType).build());
}

/**
* 获取⽂件信息
*
* @param bucketName bucket名称
* @param objectName ⽂件名称
* @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#statObject
*/
public StatObjectResponse getObjectInfo(String bucketName, String objectName) throws Exception {
return client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
}

/**
* 删除⽂件
*
* @param bucketName bucket名称
* @param objectName ⽂件名称
* @throws Exception https://docs.minio.io/cn/java-client-apireference.html#removeObject
*/
public void removeObject(String bucketName, String objectName) throws Exception {
client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
}

都是一些常规的 API 调用,我就不逐行解释了,接下来我们来一个文件上传接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class FileUploadController {
@Autowired
MinioUtil minioUtil;

@PostMapping("/upload")
public String fileUpload(MultipartFile file) throws Exception {
UploadResponse bucket01 = minioUtil.uploadFile(file, "bucket01");
System.out.println("bucket01.getMinIoUrl() = " + bucket01.getMinIoUrl());
System.out.println("bucket01.getNginxUrl() = " + bucket01.getNginxUrl());
return bucket01.getMinIoUrl();
}
}

好啦,大功告成。

接下来启动 Spring Boot 项目,然后调用这个接口上传文件,上传成功后,控制台会打印如下信息:

这就表示文件上传成功了。

4. 配置 nginx

前面提到了 MinIO 可以结合 Nginx 来使用,那我们这里就来配一配 Nginx 看看。

为了省事,Nginx 我也选择安装到 docker 容器中,但是前面安装 MinIO 时,我们已经做了数据卷映射,即上传到 MinIO 的文件实际上是保存在宿主机的,所以现在也得给 Nginx 配置数据卷,将来让 Nginx 也去 /Users/sang/minio/data 路径下查找文件。

Nginx 安装指令如下:

1
docker run --name nginx01 -p 8888:80 -v /Users/sang/minio/data:/usr/share/nginx/html:ro -d nginx

这里两个关键点:

  1. 设置 Nginx 端口为 8888。
  2. 将 MinIO 映射到宿主机的数据卷,再次挂载到 Nginx 上去。

大家知道,默认情况下,当我们访问 Nginx 的时候,Nginx 给我们展示出来的数据其实就是 /usr/share/nginx/html 目录下的,现在该目录其实就相当于我宿主机的 /Users/sang/minio/data 目录,所以我现在都不用修改 Nginx 的配置了,装好之后直接使用 Nginx 即可。

好啦,接下来我们修改一下 application.yaml,如下:

1
2
3
4
5
minio:
endpoint: http://localhost:9001
accessKey: javaboy
secretKey: 123@45678
nginxHost: http://local.javaboy.org:8888

改完之后,再次上传文件,此时打印出来的文件访问路径如下:

现在我们通过这个 Nginx 路径也能访问到刚刚上传的文件了。

5. 小结

好啦,今天就和小伙伴们分享一下 MinIO 的用法,并结合 Nginx 搭建了一个简单的文件服务器,感兴趣的小伙伴可以试试哦。

公众号江南一点雨后台回复 minio_demo,获取本文源码下载链接。

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×