MinIO 基于 Apache License v2.0 开源协议的对象存储服务,兼容亚马逊 S3( Simple Storage Service 简单存储服务)云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、静态页面等,一个对象文件可以是任意大小,文件大小最大支持 5T 。由于采用Golang实现,服务端可以工作在绝大多数主流操作系统上,而且配置和启动服务都非常简单。
本博客主要介绍单机版 Minio 的部署,分别介绍简单部署方式和纠删码部署方式,以及使用 Springboot 程序访问 Minio 实现文件的上传、下载和删除操作,在本篇博客的最后提供源代码下载。有关 Minio 的详细介绍请参考官方文档。
官网地址:https://min.io
中文地址:https://www.minio.org.cn
一、简单部署方式
我的 CentOS7 虚拟机 ip 地址为:192.168.136.128,已经安装好了 docker 和 docker-compose
首先在虚拟机上创建目录 /app/minio,然后创建相关子目录 data 和 docker-compose.yml 文件,具体结构如下:
编写 docker-compose.yml 文件内容如下:
version: "3.5"
services:
minio:
image: minio/minio
container_name: minio
privileged: true
restart: always
ports:
# 对外提供的 api 访问端口
- 9000:9000
# 对外提供的 web 管理后台访问端口
- 9001:9001
environment:
# web管理后台用户名
MINIO_ROOT_USER: jobs
# web管理后台密码
MINIO_ROOT_PASSWORD: jobs@123
volumes:
# 文件存储目录映射
- /app/minio/data:/data
# 运行 minio 服务启动命令,/data 参数是 docker 容器内部的数据目录
# 由于 web 管理后台是动态端口,因此必须指定为固定的端口
command: server --console-address ":9001" /data
然后在 docker-compose.yml 文件所在目录下,运行 docker-compose up -d
启动服务即可。
二、纠删码部署方式
Minio 可以使用至少 4 块磁盘,采用 Minio Erasure Code(纠删码)的部署方式,对上传的文件进行保护。即便损坏一半磁盘,仍然可以恢复全部文件。单机版 Minio 纠删码的部署方式,相比简单部署方式,非常简单,只不过是多挂载一些磁盘而已。但是磁盘的总数量必须 2 的 n 次幂(n >= 2),比如 4 块硬盘,8 块硬盘,16 块硬盘等等。我这边没有那么多磁盘,我使用 4 个目录代表 4 个磁盘,实现纠删码的部署方式。
首先在虚拟机上创建目录 /app/minio-erasure ,然后在其内部创建相关的子目录和文件,具体结构如下:
编写 docker-compose.yml 文件内容如下:
version: "3.5"
services:
minio:
image: minio/minio
container_name: minio
privileged: true
restart: always
ports:
# 对外提供的 api 访问端口
- 9000:9000
# 对外提供的 web 管理后台访问端口
- 9001:9001
environment:
# web管理后台用户名
MINIO_ROOT_USER: jobs
# web管理后台密码
MINIO_ROOT_PASSWORD: jobs@123
volumes:
# 文件存储目录映射
- /app/minio-erasure/data1:/data1
- /app/minio-erasure/data2:/data2
- /app/minio-erasure/data3:/data3
- /app/minio-erasure/data4:/data4
# 运行 minio 服务启动命令,/data 参数是 docker 容器内部的数据目录
# 由于 web 管理后台是动态端口,因此必须指定为固定的端口
command: server --console-address ":9001" /data{1...4}
然后在 docker-compose.yml 文件所在目录下,运行 docker-compose up -d
启动服务即可。
三、Web 界面简单操作
我们部署的 Minio 的 web 界面访问端口是 9001 ,因此访问 http://192.168.136.128:9001
即可:
我们部署时设置的账号是 jobs ,密码是 jobs@123,输入后登录即可。在左侧菜单中选择 Buckets ,在右侧的界面中创建一个 jobstest 的 Bucket 。Bucket 实际上相当于一个目录,用于对不同系统或项目的文件进行隔离。我们可以继续在 Bucket 内部创建文件夹,对文件进行分类管理。
点击 Browse 按钮,我们在 jobstest 的 Bucket 下,上传一个图片(注意:上传的文件名称,尽量不要使用包含中文的名称):
默认创建的 Bucket 是 private 权限,无法被外部访问。在上面的 Minio 操作界面第二张图片中,点击 Manage 按钮,进入 Bucket 管理界面:
然后点击 Access Policy 的编辑按钮,从弹出的框的下拉列表中,选择 Public ,这样就可以从外部访问文件了
访问文件的 url 地址格式为:http://minio 的 api 地址/bucket名称/文件夹及文件名称
因此访问刚刚上传的 qrcode2.jpg 的 url 地址为:http://192.168.136.128:9000/jobstest/qrcode2.jpg
需要注意的是:
- 需要通过 api 的端口访问文件,部署时指定的 api 访问端口是 9000。(web管理界面的端口是 9001)
- 由于没有在 jobstest 下面创建文件夹,直接在 jobstest 下面上传的图片,因此 qrcode2.jpg 前面没有一级或多级文件夹路径。
上传的 qrcode2.jpg 图片,是我在博客园的博客首页地址的二维码图片,在浏览器访问如下:
四、代码访问 Minio 实现文件上传、下载、删除
新建一个 springboot 工程,名称为 springboot_minio,具体结构如下所示:
首先看一下 pom 文件中引入的依赖包:
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jobs</groupId>
<artifactId>springboot_minio</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 minio 的依赖包-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!--引入 minio 依赖后,必须要引入 okhttp 依赖,版本必须大于等于 4.11.0-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!--使用 knife4j 功能,为了可以使用 web 界面测试接口-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
需要引入 minio 和 okhttp 这两个主要依赖包,而且有个坑必须注意:okhttp 的版本必须大于等于 4.11.0
由于我们使用 knife4j 文档对所开发的 web 接口进行测试,因此引入了 knife4j-spring-boot-starter 依赖包。
然后看一下 application.yml 配置文件的内容:
server:
port: 8080
minio:
accesskey: jobs
secretkey: jobs@123
bucket: jobstest
endpoint: http://192.168.136.128:9000
knife4j:
# 是否启用增强版功能
enable: true
# 如果是生产环境,将此设置为 true,然后就能够禁用了 knife4j 的页面
production: false
Spring:
servlet:
multipart:
# 单个文件上传大小限制
max-file-size: 100MB
# 如果同时上传多个文件,上传的总大小限制
max-request-size: 100MB
在 WebMvcConfig 中,配置一下 knife4j 文档:
package com.jobs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
//启用 knife4j 需要添加这个注解 @EnableOpenApi
@EnableOpenApi
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//需要配置 knife4j 的静态资源请求映射地址
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public Docket createDocket() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.jobs.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("我的 Minio 测试")
.version("1.0")
.description("Minio 测试接口文档")
.build();
}
}
我们使用 MinioClient 访问 Minio 服务,因此需要在 Springboot 中将 MinioClient 添加到容器中:
package com.jobs.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MinioConfig {
@Value("${minio.accesskey}")
private String accessKey;
@Value("${minio.secretkey}")
private String secretKey;
@Value("${minio.endpoint}")
private String endPoint;
@Bean
public MinioClient buildMinioClient() {
return MinioClient.builder()
.credentials(accessKey, secretKey).endpoint(endPoint).build();
}
}
然后创建一个 MinioService 实现对 Minio 服务的文件上传、下载、删除的操作:
package com.jobs.service;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
@Slf4j
@Service
public class MinioService {
@Autowired
private MinioClient minioClient;
@Value("${minio.endpoint}")
private String endPoint;
@Value("${minio.bucket}")
private String bucket;
/**
* 获取文件在 minio server 上的完整访问路径 url 地址
*
* @param minioPath 相对于 bucket 根目录的文件全路径
*/
public String getFileFullUrl(String minioPath) {
StringBuilder sb = new StringBuilder();
sb.append(endPoint);
sb.append(endPoint.endsWith("/") ? "" : "/");
sb.append(bucket).append("/");
sb.append(minioPath);
return sb.toString();
}
/**
* @param path 相对于 bucket 根目录的目录路径
* 比如 /book/chinese 表示上传到 book 目录下的 chinese 目录中
* @param file 上传的文件
* @return 相对于 bucket 目录的文件全路径
*/
public String uploadFile(String path, MultipartFile file) {
try {
return uploadFile(path, file.getOriginalFilename(), file.getInputStream(), file.getContentType());
} catch (Exception ex) {
log.error("minio 上传文件失败:", ex);
return null;
}
}
//上传文件
public String uploadFile(String path, File file, String contentType) {
try {
FileInputStream inputStream = new FileInputStream(file);
return uploadFile(path, file.getName(), inputStream, contentType);
} catch (Exception ex) {
log.error("minio 上传文件失败:", ex.getMessage());
return null;
}
}
//上传文件
private String uploadFile(String path, String fileName,
InputStream inputStream, String contentType) throws Exception {
//拼接上传文件的目录存储路径,如果目录不存在,则自动创建(支持多级目录的自动创建)
String filePath = fileName;
if (StringUtils.isNotBlank(path)) {
StringBuilder sb = new StringBuilder();
sb.append(path);
sb.append(path.endsWith("/") ? "" : "/");
sb.append(fileName);
filePath = sb.toString();
}
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath).contentType(contentType).bucket(bucket)
//inputStream.available() 表示文件的总字节数大小,-1 表示上传所有字节数
.stream(inputStream, inputStream.available(), -1).build();
minioClient.putObject(putObjectArgs);
return filePath;
}
/**
* 下载文件
*
* @param minioPath 相对于 bucket 根目录的文件全路径
*/
public byte[] downloadFile(String minioPath) {
if (StringUtils.isBlank(minioPath)) {
return null;
}
try {
GetObjectArgs getObjectArgs = GetObjectArgs.builder()
.bucket(bucket).object(minioPath).build();
InputStream inputStream = minioClient.getObject(getObjectArgs);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(inputStream, outputStream);
return outputStream.toByteArray();
} catch (Exception ex) {
log.error("minio 下载文件失败,pathUrl:{}/{},失败信息:{}",
bucket, minioPath, ex.getMessage());
return null;
}
}
/**
* 删除文件
*
* @param minioPath 相对于 bucket 根目录的文件全路径
*/
public boolean deleteFile(String minioPath) {
if (StringUtils.isBlank(minioPath)) {
return true;
}
try {
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
.bucket(bucket).object(minioPath).build();
minioClient.removeObject(removeObjectArgs);
return true;
} catch (Exception ex) {
log.error("minio 删除文件失败,pathUrl:{}/{},失败信息:{}",
bucket, minioPath, ex.getMessage());
return false;
}
}
}
然后创建 MinioController 对外提供文件的上传、下载、删除接口:
package com.jobs.controller;
import com.jobs.service.MinioService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Api(tags = "Minio 操作接口")
@RestController
@RequestMapping("/minio")
public class MinioController {
@Autowired
private MinioService minioService;
@ApiOperation("上传文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "path", value = "上传目录路径", required = false),
@ApiImplicitParam(name = "file", value = "上传的目标文件",
dataType = "java.io.File", paramType = "query", required = true)})
@PostMapping("/upload")
//注意:针对 MultipartFile 需要增加 @RequestPart 注解,否则 knife4j 接口无法显示文件上传操作。
public Map uploadFile(String path, @RequestPart("file") MultipartFile file) {
String minioPath = minioService.uploadFile(path, file);
Map<String, String> map = new HashMap<>();
if (StringUtils.isNotBlank(minioPath)) {
map.put("minio_path", minioPath);
map.put("file_url", minioService.getFileFullUrl(minioPath));
}
return map;
}
//必须要加上 produces = "application/octet-stream" ,否则 knife4j 接口无法下载文件
@ApiOperation(value = "下载文件", produces = "application/octet-stream")
@ApiImplicitParam(name = "minioPath",
value = "minio文件路径(不包含web域名和bucket名称)", required = true)
@GetMapping("/download")
public void downloadFile(String minioPath, HttpServletResponse response) {
//获取文件名(包含后缀名)
String fileName = FilenameUtils.getName(minioPath);
byte[] bytes = minioService.downloadFile(minioPath);
if (bytes != null) {
try {
//下载文件的响应类型,这里统一设置成了文件流
//你可以根据自己所提供下载的文件类型,使用不同的响应 mime 类型
response.setContentType("application/octet-stream;charset=utf-8");
//设置下载弹出框中默认显示的文件名称,如果指定中文名称的话,需要转成 iso8859-1 编码,解决乱码问题
fileName = new String(fileName.getBytes(), "iso8859-1");
response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
IOUtils.write(bytes, response.getOutputStream());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
@ApiOperation("删除文件")
@ApiImplicitParam(name = "minioPath",
value = "minio文件路径(不包含web域名和bucket名称)", required = true)
@PostMapping("/delete")
public String deleteFile(String minioPath) {
Boolean flag = minioService.deleteFile(minioPath);
return flag ? "delete success" : "delete fail";
}
}
最后启动 springboot 程序,访问 http://localhost:8080/doc.html
即可通过图形化界面调用接口进行测试:
有关具体的测试细节,这里就不展示了,我已经测试没问题了。
大家可以下载源代码,自己进行测试体验,结合 Minio 的 web 界面进行验证。
本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_minio.zip