背景介绍
MinIO 是一个基于 Go 实现的轻量高性能、兼容 S3 协议的对象存储。它采用 GNU AGPL v3 开源协议,项目地址是 https://github.com/minio/minio,官网是 https://min.io。下载地址 https://min.io/download
它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据。 例如图片、音频、视频、日志文件等常见文件,备份数据、容器、虚拟机镜像等等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。
国内阿里巴巴、腾讯、百度、华为、中国移动、中国联通等企业在使用 MinIO,甚至不少商业公司二次开发 MinIO 来提供商业化的云存储产品。
安装使用
基于docker进行安装并启动MinIo容器
1.拉取MinIo镜像,学习用的所以拉取镜像时不指定版本直接体验下最新版本
docker pull minio/minio
2.启动容器
docker run -p 9000:9000 -p 9090:9090 \
--name minio \
--privileged=true \
-it -d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \ #创建用户:minioadmin
-e "MINIO_SECRET_KEY=minioadmin" \ #设置密码:minioadmin
-v /dataFile/minio/data:/data \ #挂载数据目录
minio/minio server \
/data --console-address ":9090" -address ":9000"
访问9090端口测试,出现以下界面代表启动成功,启动失败可以使用docker的logs查看日志排查解决
使用启动容器时添加的账号和密码进行登录,登录成功后需要做以下两件事
1.创建存放文件的存储桶(类似于电脑的盘符)
2.创建用户并且为该用户生成 accessKey、secretKey
整合使用
使用SpringBoot整合MinIo完成文件的上传、下载、删除操作
1.引入MinIo依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.5</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
2. 定义配置类、工具类
yml文件:
########### MinIO 服务配置 #############
minio:
#服务器的地址和端口号
endpoint: 192.168.56.103
port: 9000
#上传的密钥
accessKey: hPe2dUdIYoevY2n7
secretKey: RjPjKe9UVXp1bP2cjcSsElTDkkIe3max
#需要注意的一点是 secure 参数是设置 oss 服务请求 minio 是否使用 https 请求默认false使用 http 请求
#但在 new MinioClient() 构造方法中默认是 true
secure: false
#文件存储桶名称
bucketName: systembucket
spring:
servlet:
#设置文件上传大小限制
multipart:
max-file-size: 500MB
max-request-size: 500MB
自定义配置类 MinioProperty:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author: YUF
* @Date: 2023/4/21 14:19
*/
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinIoProperty {
private String endpoint = "localhost";
private int port = 9000;
private String accessKey = "minioadmin";
private String secretKey = "minioadmin";
private boolean secure = false;
}
创建工具类:
package com.hp.utils;
import com.alibaba.fastjson.JSON;
import com.hp.config.MinIoProperty;
import com.hp.dto.DelFilesDto;
import com.hp.entity.FileInfoRes;
import com.hp.entity.MinIoItem;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 8.x.x版
*
* @author: YUF
* @Date: 2023/4/21 14:20
*/
@Component
public class MinIoUtils8 {
@Autowired
private MinIoProperty minIoProperty;
/**
* 存储桶名称
*/
@Value("${minio.bucketName}")
private String bucketName;
/**
* minio客户端
*/
private MinioClient minioClient;
/**
* 文件的ContentType映射map
* key-》文件类型 value-》ContentType
*/
private Map<String, String> map = new HashMap<>(16);
/**
* 创建 MinioClient 客户端
*/
@SuppressWarnings("all")
@PostConstruct
public void createMinIoClient() throws Exception {
// 拼接地址
String adders = "http://" + minIoProperty.getEndpoint() + ":" + minIoProperty.getPort();
this.minioClient = MinioClient.builder().endpoint(adders)
.credentials(minIoProperty.getAccessKey(), minIoProperty.getSecretKey())
.build();
if (null == bucketName) {
// 配置文件中未设置时赋予默认值
bucketName = "asiatrip";
}
/**
* 读取文件类型映射JSON文件,内容如下展示:
* {"jpe":"image/jpeg","jpeg":"image/jpeg","jpg":"image/jpeg","jpgm":"video/jpm","mp4":"video/mp4",......}
*/
String jsonStr = readJsonFile("json", "fileType.json");
if (null != jsonStr) {
// 反序列化文件类型映射JSON
this.map = JSON.parseObject(jsonStr, Map.class);
}
}
/**
* 上传文件方法
*
* @param file 上传的文件
* @return 封装后返回的文件信息实体类
*/
public FileInfoRes uploadFile(MultipartFile file, String prefix) throws Exception {
//获取文件的输入流
try (InputStream is = file.getInputStream()) {
// 创建文件信息返回实体类
FileInfoRes fileInfo = new FileInfoRes();
// 检查存储桶是否已经存在
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!isExist) {
// 创建一个名为asiatrip的默认存储桶
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
// 创建日期格式类
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
// 获取当前日期
Date date = new Date();
// 文件信息设置上传时间
fileInfo.setCreateDate(date);
//格式化日期为字符串
String dateStr = sdf.format(date);
if (StringUtils.isNotBlank(prefix)) {
dateStr = prefix.concat("/").concat(dateStr);
}
//获取原文件名称
String fileName = file.getOriginalFilename();
//文件信息设置原文件名称
fileInfo.setOriginalFilename(fileName);
/**
* 将待上传的文件的路径设置成 prefix/年/月/日/uuid.png
* 你也可以自定义上传文件的路径,这样设置是为了更好的管理
*/
String uuid = UUID.randomUUID()
.toString()
.replace("-", "")
.substring(0, 6);
assert fileName != null;
//生成保存文件时的uid文件名
String uIdName = uuid + fileName.substring(fileName.lastIndexOf("."));
//保存属性
fileInfo.setUuIdFileName(uIdName);
//拼接文件存储的完整路径
String fileUploadPath = dateStr + uIdName;
//保存属性
fileInfo.setFileUploadPath(fileUploadPath);
// 使用分片的形式上传文件,设置每个分片大小为5MB
PutObjectArgs put = PutObjectArgs.builder()
.contentType(getContentTypeByFileName(fileName))
.stream(is, file.getSize(), 10485760)
.bucket(bucketName).object(fileUploadPath).build();
minioClient.putObject(put);
// 获取永久的访问地址
GetPresignedObjectUrlArgs foreverUrlArg = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(fileUploadPath)
.method(Method.GET)
.build();
String webPath = minioClient.getPresignedObjectUrl(foreverUrlArg);
//获取带有效期的访问地址
HashMap<String, String> reqMap = new HashMap<>(16);
// 添加请求参数,生成路径时会拼接在其中
reqMap.put("uid", "1174847034");
reqMap.put("sessionId", "14718103074");
GetPresignedObjectUrlArgs temporaryUrlArg = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.expiry(1, TimeUnit.DAYS)
.extraQueryParams(reqMap)
.object(fileUploadPath)
.method(Method.GET)
.build();
/**
* 获取临时路径: expires有效期秒为单位,可以看到下面生成的路径中是包含了设置的reqMap值的
* http://192.168.56.103:9000/systembucket/2023/04/21/d9a98a.bat?
* sessionId=1174847034&uid=14718103074&X-Amz-Algorithm=......
*/
String temporaryUrl = minioClient.getPresignedObjectUrl(temporaryUrlArg);
// 保存属性
fileInfo.setWebPath(webPath);
fileInfo.setTemporaryUrl(temporaryUrl);
fileInfo.setBucketName(bucketName);
return fileInfo;
} catch (Exception e) {
throw new Exception("MinIo文件上传失败");
}
}
/**
* 根据文件名称获取ContentType类型
*
* @param fileName 文件名称
* @return ContentType类型
*/
private String getContentTypeByFileName(String fileName) {
// 根据文件名获取类型
String fileExtension = "";
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex > 0) {
fileExtension = fileName.substring(dotIndex + 1);
}
// 根据文件类型获取ContentType
String contentType = map.get(fileExtension);
/**
* 为空给个默认值
* application/octet-stream: 二进制流数据(如常见的文件下载,不知道下载文件类型)
*/
if (null == contentType) {
contentType = "application/octet-stream";
}
return contentType;
}
/**
* 读取某个目录下的json文件,并返回内容字符串
*
* @param catalogue 目录
* @param fileName 文件名
* @return
*/
public String readJsonFile(String catalogue, String fileName) {
String jsonStr = "";
try {
ClassPathResource classPathResource = new ClassPathResource(catalogue + "/" + fileName);
InputStream inputStream = classPathResource.getInputStream();
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
int ch;
StringBuilder sb = new StringBuilder();
while ((ch = reader.read()) != -1) {
sb.append((char) ch);
}
reader.close();
jsonStr = sb.toString();
return jsonStr;
} catch (IOException e) {
return null;
}
}
/**
* 单个删除文件
*
* @param bucketName 存储桶名称
* @param objectName 文件路径
* @return
* @throws Exception
*/
public boolean delFile(String bucketName, String objectName) throws Exception {
try {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
return Boolean.TRUE;
} catch (Exception e) {
return Boolean.FALSE;
}
}
/**
* 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
*
* @param delFilesDto 删除dto,两个属性 存储桶名称:bucketName 文件路径列表:filePaths
* @return eg: 删除失败的文件名称
* @throws Exception
*/
public List<String> delFiles(DelFilesDto delFilesDto) throws Exception {
List<String> deleteErrorNames = new ArrayList<>();
List<DeleteObject> objects = new ArrayList<>();
for (String filePath : delFilesDto.getFilePaths()) {
objects.add(new DeleteObject(filePath));
}
RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(delFilesDto.getBucketName())
.objects(objects).build();
Iterable<Result<DeleteError>> results = minioClient
.removeObjects(removeObjectsArgs);
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
deleteErrorNames.add(error.objectName());
}
return deleteErrorNames;
}
/**
* 获取对象的元数据
* 该实体类无法直接作为接口返回值,因为其没有get&set方法会导致报406-Not Acceptable的错误
*
* @param bucketName 存储桶名称
* @param objectName 文件路径
* @return
*/
public StatObjectResponse getStatObject(String bucketName, String objectName) {
StatObjectResponse statObject;
try {
statObject = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
return null;
}
return statObject;
}
/**
* 下载文件
*
* @param bucketName 存储桶名称
* @param objectName 文件路径
* @param response 响应流
*/
public void downloadFile(String bucketName, String objectName, HttpServletResponse response) {
try (InputStream in = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
OutputStream out = response.getOutputStream()) {
downloadFile(objectName, response, in, out);
} catch (Exception ignored) {
}
}
/**
* 下载文件
*
* @param bucketName 存储桶名称
* @param objectName 文件路径
* @param offSet 开始读取的位置
* @param length 需要读取的字节
* @param response 响应流
*/
public void downloadFile(String bucketName, String objectName, long offSet, Long length, HttpServletResponse response) {
GetObjectArgs build = GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offSet).length(length).build();
try (InputStream in = minioClient.getObject(build);
OutputStream out = response.getOutputStream()) {
downloadFile(objectName, response, in, out);
} catch (Exception ignored) {
}
}
private void downloadFile(String objectName, HttpServletResponse response, InputStream in, OutputStream out) throws IOException {
int len;
byte[] buffer = new byte[1024];
//重置
response.reset();
//设置编码
response.setCharacterEncoding("UTF-8");
//设置Mime-Type
String contentType = getContentTypeByFileName(objectName);
if (StringUtils.isBlank(contentType)) {
contentType = "application/octet-stream";
}
response.setContentType(contentType);
//设置下载默认文件名
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder
.encode(objectName, "UTF-8"));
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
}
/**
* 根据前缀查询文件列表或文件夹列表
*
* @param bucketName bucket名称
* @param prefix 前缀
* @param recursive 是否递归查询
* @return MinioItem 列表
*/
@SneakyThrows
public List<MinIoItem> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
List<MinIoItem> objectList = new ArrayList<>();
ListObjectsArgs build = ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build();
Iterable<Result<Item>> objectsIterator = minioClient
.listObjects(build);
for (Result<Item> itemResult : objectsIterator) {
Item item = itemResult.get();
String path = item.objectName();
String objectName = path.replace(prefix, "").replace("/", "");
objectList.add(new MinIoItem(objectName, item.isDir(), path));
}
return objectList;
}
}
3.调用测试
接口调用测试文档:https://documenter.getpostman.com/view/15741720/2s93Y5Nz4Q#9a8e3ac7-5725-4d1a-8ec1-8209172e3778
import com.hp.dto.DelFilesDto;
import com.hp.entity.FileInfoRes;
import com.hp.entity.MinIoItem;
import com.hp.utils.MinIoUtils;
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.List;
/**
* @author: YUF
* @Date: 2023/4/21 14:36
*/
@RequestMapping("/file")
@RestController
public class MinIoController {
@Autowired
private MinIoUtils minIoUtils;
/**
* 文件上传
*
* @param file 文件对象
* @param prefix 路径前缀,用于业务操作
* @return
*/
@PostMapping("uploadFile")
public FileInfoRes uploadFile(MultipartFile file, String prefix) throws Exception {
return minIoUtils.uploadFile(file, prefix);
}
/**
* 单个删除文件
* 文件删除之后,如果该文件夹下没有文件存在,MinIO会自动删除该空文件夹及上级空文件夹。
* @param bucketName 存储桶名称
* @param objectName 文件路径
* @return
*/
@PostMapping("delFile")
public boolean delFile(String bucketName, String objectName) throws Exception {
return minIoUtils.delFile(bucketName, objectName);
}
/**
* 批量删除文件
* 文件删除之后,如果该文件夹下没有文件存在,MinIO会自动删除该空文件夹及上级空文件夹。
* @param delFilesDto 删除dto
* @return
*/
@PostMapping("delFiles")
public List<String> delFiles(@RequestBody DelFilesDto delFilesDto) throws Exception {
return minIoUtils.delFiles(delFilesDto);
}
/**
* 普通文件下载
*
* @param bucketName 存储桶名称
* @param objectName 文件路径
*/
@PostMapping("downloadFile")
public void downloadFile(String bucketName, String objectName, HttpServletResponse response) {
minIoUtils.downloadFile(bucketName, objectName, response);
}
/**
* 获取指定前缀的文件或文件夹列表
*
* @param bucketName 存储桶名称
* @param prefix 路径前缀
* @param recursive 是否递归查询
* @return
*/
@PostMapping("getAllObjectsByPrefix")
public List<MinIoItem> getAllObjectsByPrefix(@RequestParam String bucketName, @RequestParam String prefix, @RequestParam boolean recursive) {
return minIoUtils.getAllObjectsByPrefix(bucketName, prefix, recursive);
}
}
注:
1.想查看文件上传后分片情况可以找到启动容器时挂载的目录
分成6个分片的原因是在上传时代码中设置了以10MB为单个分片大小,原文件为54.36MB