Minio 是一个用 Golang 开发的开源的对象存储服务器,它基于 Amazon S3 协议,提供了简单而强大的存储解决方案。可以在本地部署或云环境中使用。也支持分布式部署,并具有高可用性和容错性。
本文将会带你了解如何在 Linux 中通过 Docker 的方式来安装、配置 Minio,以及如何在 Spring Boot 应用中通过 Minio 官方 SDK 上传文件资源到 Minio 服务器。
安装 Minio {#安装-minio}
在 Linux 下,使用 Docker
的方式安装 Minio 最简单。首先确保你在服务器上安装了 Docker,并且需要 root
用户来执行下面的安装过程。
首先,创建存放文件资源的目录:
mkdir -p ~/minio/data
上述命令在 $HOME
目录下创建了 /minio/data
文件夹,用于存放资源。
接着,使用 Docker 运行 Minio 容器:
docker run \
-d \
-p 9000:9000 \
-p 9090:9090 \
--name minio-server \
-v ~/minio/data:/data \
-e "MINIO_ROOT_USER=admin" \
-e "MINIO_ROOT_PASSWORD=minio858896" \
quay.io/minio/minio server /data --console-address ":9090"
-d
:以守护进程的形式启动容器。-p 9000:9000
:指定了 Minio 资源的访问端口(也是 API 端口)。-p 9090:9090
:指定了管理控制台的访问端口。--name minio-server
:指定了容器的名称。-v ~/minio/data:/data
:挂载主机上的资源目录到容器的/data
目录。-e "MINIO_ROOT_USER=admin"
:指定了管理控制台的用户名。-e "MINIO_ROOT_PASSWORD=minio858896"
:指定了管理控制台的密码(这里为了演示,故意设置得很简单)。
注意,用户名和密码有安全要求,用户名最低 3 个字符长度,密码最低 8 个字符串长度。否则会提示异常:
ERROR Unable to validate credentials inherited from the shell environment: Invalid credentials > Please provide correct credentials HINT: Access key length should be at least 3, and secret key length at least 8 characters
为了更接近实际应用,本文专门解析了一个域名到 Minio 服务器:oss.springboot.io
。本文接下来就会使用这个域名来进行资源访问、后台管理和 API 调用!
你如果没有域名,直接使用 ip 也是没任何问题的。
登录控制台 {#登录控制台}
安装就绪后,使用浏览器访问登录页:http://oss.springboot.io:9090/login
。然后使用安装时设置的用户名和密码进行登录。
创建 Bucket {#创建-bucket}
进入管理页面后,点击左侧 "Buckets" 按钮,进入 Bucket 管理面板。
创建一个名为 "images" 的 Bucket。
Bucket 就是存储对象资源的基本单位,你可以简单理解为系统中的 "文件夹"。
为匿名用户设置只读权限 {#为匿名用户设置只读权限}
接着,点击刚创建的 Bucket,进入 "Anonymous" 配置。为匿名用户添加 "readonly" 权限。
Prefix 是资源的前缀匹配。
默认情况下,Bucket 是私有的,匿名用户无法访问其中的资源。
创建 Access Key {#创建-access-key}
点击左侧 "Access Keys" 菜单,生成一个新的 Access Key。
生成后,点击 "Create" 保存 Access Key。
千万要注意保存这俩 Key,因为这是你最后一次可以看到 Secret Key 的值了。
示例应用 {#示例应用}
创建 Spring Boot 项目 {#创建-spring-boot-项目}
添加 Minio 官方的 SDK 依赖 minio
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
配置上传信息 {#配置上传信息}
在 application.yaml
中配置 Access Key 等信息:
app:
minio:
# 访问资源的 URL
base-url: "http://oss.springboot.io:9000/"
# API 端点
endpoint: "http://oss.springboot.io:9000/"
# 上传的 Bucket
bucket: images
# Access Key
access-key: Umt2UtK5vp7njhM4BFjP
# Secret Key
secret-key: S3ZJayIxxv3AZfkyCitmrksugzrABbYGJQ4v8OGB
如上,在配置文件中指定了访问文件的URL、API 端点地址、要上传到哪个 Bucket 以及在 Minio 控制台生成的 Access Key 和 Secret Key。
UploadController {#uploadcontroller}
创建 UploadController
,实现 /upload
API。
接收客户端上传的资源文件,进行基本的校验后通过 Minio SDK 上传到 Minio 服务器,最后返回访问地址。
package cn.springdoc.demo.web.controller;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
@RestController
@RequestMapping("/upload")
public class UploadController {
static final Logger log = LoggerFactory.getLogger(UploadController.class);
// 日期格式化
static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("/yyy/MM/dd/");
// 资源的 访问 URL
@Value("${app.minio.base-url}")
private String baseUrl;
// API 端点
@Value("${app.minio.endpoint}")
private String endpoint;
// Bucket 存储桶
@Value("${app.minio.bucket}")
private String bucket;
// Acess Key
@Value("${app.minio.access-key}")
private String accessKey;
// Secret Key
@Value("${app.minio.secret-key}")
private String secretKey;
/**
* 上传文件到 Minio 服务器,返回访问地址
* @param file
* @return
* @throws Exception
*/
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) throws Exception{
// 文件大小
long size = file.getSize();
if (size == 0) {
return ResponseEntity.badRequest().body("禁止上传空文件");
}
// 文件名称
String fileName = file.getOriginalFilename();
// 文件后缀
String ext = "";
int index = fileName.lastIndexOf(".");
if (index ==-1) {
return ResponseEntity.badRequest().body("禁止上传无后缀的文件");
}
ext = fileName.substring(index);
// 文件类型
String contentType = file.getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
// 根据日期打散目录,使用 UUID 重命名文件
String filePath = formatter.format(LocalDate.now()) +
UUID.randomUUID().toString().replace("-", "") +
ext;
log.info("文件名称:{}", fileName);
log.info("文件大小:{}", size);
log.info("文件类型:{}", contentType);
log.info("文件路径:{}", filePath);
// 实例化客户端
MinioClient client = MinioClient.builder()
.endpoint(this.endpoint)
.credentials(this.accessKey, this.secretKey)
.build();
// 上传文件到客户端
try (InputStream inputStream = file.getInputStream()){
client.putObject(PutObjectArgs.builder()
.bucket(this.bucket) // 指定 Bucket
.contentType(contentType) // 指定 Content Type
.object(filePath) // 指定文件的路径
.stream(inputStream, size, -1) // 文件的 Inputstream 流
.build());
}
// 返回最终的访问路径
return ResponseEntity.ok(this.baseUrl + this.bucket + filePath);
}
}
通过 @Value
注解,把配置文件中的属性值注入到 Controller
成员变量中。
在上传方法中,首先校验了上传的文件。不允许上大小为 0 和无后缀的文件。然后根据日期 /yyy/MM/dd/
格式打散目录,并且使用 UUID
重命名文件防止同名文件覆盖。
然后使用端点 API 地址、accessKey 和 secretKey 参数构建 MinioClient
实例。
调用 MinioClient
的 putObject
方法进行上传,通过 PutObjectArgs
Builder 构建上传参数,其中指定了要上传的 Bucket、文件的媒体类型、文件的保存路径以及文件的 InputStream
。
如果没有发生异常,则上传成功。最后,拼接完整的访问路径,返回给客户端(文件的访问路径包含了 Bucket 名称)。
测试 {#测试}
启动服务器,使用 Postman 上传图片文件(图片文件是 "Spring 中文网" 的 Logo - 512 x 512):
上传成功,返回资源的访问地址如下:
http://oss.springboot.io:9000/images/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png
接着,尝试在浏览器中访问上传的资源。
一切 OK。
日志 {#日志}
最后附上客户端的请求日志:
POST /upload HTTP/1.1
Accept-Language: en_US
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 7f213edc-7506-4652-bdc5-f783823fc978
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------815963622984394675083411
Content-Length: 20029
----------------------------815963622984394675083411
Content-Disposition: form-data; name="file"; filename="512.png"
<512.png>
----------------------------815963622984394675083411--
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 84
Date: Mon, 13 Nov 2023 10:30:09 GMT
Keep-Alive: timeout=60
Connection: keep-alive
http://oss.springboot.io:9000/images/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png
以及服务端的日志:
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController : 文件名称:512.png
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController : 文件大小:19825
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController : 文件类型:image/png
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController : 文件路径:/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png
最后 {#最后}
Minio 的功能十分强大,不仅仅是基本的对象存储,还包括数据加密、访问控制、版本管理等等功能。是代替 FastDFS
的理想选择。
得益于官方提供的 SDK,可以很轻松地在应用中完成整合,只需要几行代码就能实现文件的上传。当然,SDK 提供的功能不仅限于此,还包括 Bucket 管理、存储对象管理等等。
对于 Minio 和 SDK 的更多细节,推荐你阅读 官方文档 进行了解。