一. MiniO
官网:MinIO | 高性能, Kubernetes原生对象存储
文档:MinIO对象存储 Kubernetes — MinIO中文文档 | MinIO Kubernetes中文文档
MinIO是一款分布式文件系统(或者叫对象存储服务),可以做为云存储的解决方案用来保存海量的图片、视频、文档等。由于采用Golang实现,服务端可以工作在Windows、Linux、 OS X和FreeBSD上。配置简单,基本是复制可执行程序,单行命令就可以运行起来。
MinIO兼容亚马逊S3(Simple Storage Service,简单存储服务)云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而且每个对象文件可以是任意大小,从几kb到最大5T不等。
MiniO特点:
1. 高性能:作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率;
2. 可扩容:不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心;
3. SDK支持: 基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持;
4. 支持纠删码:MinIO使用纠删码、Checksum来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据;
MiniO基本概念:
- bucket(桶) :类似文件系统的目录(文件夹);
- Object : 类似文件系统的文件;
- Keys :类似文件名;
- MINIO_ACCESS_KEY:访问key,类似账号;
- MINIO_SECRET_KEY:秘钥,类似密码。
二. MiniO环境搭建
基于Docker搭建MiniO环境
1. 拉取MiniO镜像
下载指定版本的minio
docker pull minio/minio:RELEASE.2021-04-06T23-11-00Z
2. 创建容器
docker run -p 4007:9000 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /date/data/minio/data:/data -v /date/data/minio/config:/root/.minio minio/minio:RELEASE.2021-04-06T23-11-00Z server /data
- -p 4007:9000 ,端口映射
- -e,环境变量
- -d,后台运行
- –name,给容器起名字
- –restart=always,开机自启
- -e “MINIO_ACCESS_KEY=minio”,设置账号
- -e “MINIO_SECRET_KEY=minio123”,设置密码
- -v 挂载数据卷
3. 查看容器是否启动 :docker ps
4. 访问MinIO后台系统,直接浏览器访问 http://ip:port 即可,账号minio,密码minio123
5. 登录后的管理界面
6. 创建bukect桶,右下角+号点击创建桶
创建一个test桶
三. SpringBoot整合MiniO
1. 在已有的SpringBoot项目中导入MiniO相关依赖
<dependencies>
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--knife4j(swagger) -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2. 编写MiniO属性配置类
package com.baidou.dto;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Data
@Component
@ConfigurationProperties(prefix = "minio") //自动注入属性前缀为minio的配置
public class MinIOConfigProperties implements Serializable {
private String accessKey; // 访问key
private String secretKey; // 秘钥
private String bucket; // 桶
private String endpoint; // 地域节点
private String readPath; // 读取路径
}
3. 编写MinIO配置类,注册MinioClient客户端的Bean对象
package com.baidou.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO配置类
*
* @author 白豆五
* @version 2023/04/21
* @since JDK8
*/
@Configuration
public class MinIOConfig {
@Autowired
private MinIOConfigProperties minIOConfigProperties;
// 注册MinIO实例
@Bean
public MinioClient buildMinioClient(){
return MinioClient
.builder()
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.endpoint(minIOConfigProperties.getEndpoint())
.build();
}
}
4. 在application.yml文件中配置minio自定义属性和文件上传大小
minio:
accessKey: minio
secretKey: minio123
bucket: testminio
endpoint: http://219.159.22.22:4007
readPath: http://219.159.22.22:4007
servlet:
multipart:
# 单个上传文件的最大值是200mb
max-file-size: 200MB
# 单次请求的最大值
max-request-size: 200MB
5. 编写操作MiniO相关业务接口(导包根据自身项目来)
package com.geb.system.service;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
/**
* 操作minio相关业务接口
*
* @author Jx
* @version 2023-11-14
* @since JDK8
*/
public interface IFileStorageService{
/**
* 上传文件
*
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadFile(String prefix, String filename, InputStream inputStream);
/**
* 删除文件
*
* @param pathUrl 文件全路径
*/
public void delete(String pathUrl);
/**
* 下载文件
*
* @param pathUrl 文件全路径
* @return
*/
public void downLoad(HttpServletResponse response, String pathUrl);
/**
* 在线预览文件
* @param filePath
* @return
*/
public String getPreviewUrl(String filePath);
}
实现类:
package com.geb.system.service.impl;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.geb.common.config.MinIOConfigProperties;
import com.geb.system.service.IFileStorageService;
import io.minio.*;
import io.minio.http.Method;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Service
public class MinIOFileStorageServiceImpl implements IFileStorageService {
@Autowired
private MinioClient minioClient;
@Autowired
private MinIOConfigProperties minIOConfigProperties;
private final static String separator = "/"; //文件夹分隔符
private final static String endPointUrl = "http://219.159.22.22:4007";
private final static String endPointUrl1 = "http://minio:9000";
/**
* 构建文件的绝对路径
*
* @param dirPath 文件路径
* @param filename 文件名 yyyy/mm/dd/file.jpg
* @return /test
*/
public String builderFilePath(String dirPath, String filename) {
StringBuilder stringBuilder = new StringBuilder(50);
if (!StringUtils.isEmpty(dirPath)) {
stringBuilder.append(dirPath).append(separator);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String todayStr = sdf.format(new Date());
stringBuilder.append(todayStr).append(separator);
stringBuilder.append(filename);
return stringBuilder.toString();
}
/**
* 上传文件
*
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadFile(String prefix, String filename, InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath) //文件名
.contentType("text/json")//文件类型
.bucket(minIOConfigProperties.getBucket())//桶名称与minio创建的桶一致
.stream(inputStream, inputStream.available(), -1)//文件流
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator + minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString(); //文件全路径
} catch (Exception ex) {
log.error("minio put file error.", ex);
ex.printStackTrace();
throw new RuntimeException("上传文件失败");
}
}
/**
* 删除文件
*
* @param pathUrl 文件全路径
*/
@Override
public void delete(String pathUrl) {
/** 获取桶后面的删除路径 */
String delUrl = pathUrl.substring(pathUrl.lastIndexOf("/json"), pathUrl.length());
/** 重新构建根节点endpoint */
minioClient = MinioClient.builder()
.endpoint(endPointUrl1)
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.build();
/** 删除文件 */
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs
.builder()
.bucket(minIOConfigProperties.getBucket())
.object(separator + delUrl).build();
try {
minioClient.removeObject(removeObjectArgs);
} catch (Exception e) {
log.error("minio remove file error. pathUrl:{}", pathUrl);
e.printStackTrace();
}
}
/**
* 下载文件
*
* @param pathUrl 文件全路径
* @return 文件流 byte[]
*/
@Override
public void downLoad(HttpServletResponse response, String pathUrl) {/** 拼接完整的下载路径 pathUrl+minio */
/** 获取文件名称 */
String fileName = pathUrl.substring(pathUrl.lastIndexOf("/") + 1, pathUrl.length()) + ".json";
/** 获取桶后面的下载路径 */
String downUrl = pathUrl.substring(pathUrl.lastIndexOf("/json"), pathUrl.length());
/** 获取本地文件默认下载路径 */
InputStream inputStream = null;
try {
/** 重新构建根节点endpoint */
minioClient = MinioClient.builder()
.endpoint(endPointUrl1)
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.build();
inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(minIOConfigProperties.getBucket())
.object(downUrl).build());
/*** text/json */
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setCharacterEncoding("UTF-8");
/*** 用文件流的方式读取到下载文件中 */
OutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
inputStream.close();
outputStream.close();
} catch (Exception e) {
log.error("minio down file error. pathUrl:{}", pathUrl);
e.printStackTrace();
}
}
/**
* 在线预览文件
* @param pathUrl
* @return
*/
@Override
@SneakyThrows(Exception.class)
public String getPreviewUrl(String pathUrl) {
/** 获取文件名称 */
String fileName = pathUrl.substring(pathUrl.lastIndexOf("/") + 1, pathUrl.length());
/** 获取桶后面的下载路径 */
String downUrl = pathUrl.substring(pathUrl.lastIndexOf("/json"), pathUrl.length());
/** 重新构建根节点endpoint */
minioClient = MinioClient.builder()
.endpoint(endPointUrl)
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.build();
/** 获取预览的路径 */
String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(minIOConfigProperties.getBucket())
.object(downUrl)
.build());
System.out.println("previewUrl===" + url);
return url;
}
}
以上主要是MiniO业务的一些常规操作,实现类里面的具体路径以及桶需要的操作路径,由于我是docker安装的MiniO服务器,都放在同一台服务器上可能会有影响,路径可以自行处理下!
5. 编写统一返回结果类
package com.geb.common.core.domain;
import java.io.Serializable;
import com.geb.common.constant.HttpStatus;
/**
* 响应信息主体
*
* @author ruoyi
*/
public class R<T> implements Serializable
{
private static final long serialVersionUID = 1L;
/** 成功 */
public static final int SUCCESS = HttpStatus.SUCCESS;
/** 失败 */
public static final int FAIL = HttpStatus.ERROR;
private int code;
private String msg;
private T data;
public static <T> R<T> ok()
{
return restResult(null, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data)
{
return restResult(data, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data, String msg)
{
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail()
{
return restResult(null, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg)
{
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data)
{
return restResult(data, FAIL, "操作失败");
}
public static <T> R<T> fail(T data, String msg)
{
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg)
{
return restResult(null, code, msg);
}
private static <T> R<T> restResult(T data, int code, String msg)
{
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public int getCode()
{
return code;
}
public void setCode(int code)
{
this.code = code;
}
public String getMsg()
{
return msg;
}
public void setMsg(String msg)
{
this.msg = msg;
}
public T getData()
{
return data;
}
public void setData(T data)
{
this.data = data;
}
public static <T> Boolean isError(R<T> ret)
{
return !isSuccess(ret);
}
public static <T> Boolean isSuccess(R<T> ret)
{
return R.SUCCESS == ret.getCode();
}
}
6. 编写Controller,里面设计到的@Api注解是安装配置了Swagger接口文档,详情可见
IDEA配置Swagger+统一返回数据嵌套展示信息(不被拦截器拦截处理)_swagger3 统一的返回封装类-CSDN博客
package com.baidou.controller;
import com.baidou.dto.Result;
import com.baidou.service.FileStorageService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
/**
* 操作minio的控制器类
*
* @author 白豆五
* @version 2023/04/21
* @since JDK8
*/
@RestController
@RequestMapping("/minio")
@Api(tags = "minio相关接口")
public class MinioController {
@Autowired
private FileStorageService fileStorageService;
/**
* 上传图片到minio
*
* @param file
* @return
*/
@PostMapping("/uploadFile")
@ApiOperation(value = "手动上传json文件接口--给算法提供")
public R<String> uploadFile(MultipartFile file) {
// 获取文件名称
String fileName = file.getOriginalFilename();
fileName = fileName.substring(0, fileName.indexOf(".json"));
QueryWrapper<DataFormatConversion> wrapper = new QueryWrapper<>();
wrapper.eq("data_name", fileName);
List<DataFormatConversion> dataFormatConversions = dataFormatConversionService.list(wrapper);
if(!dataFormatConversions.isEmpty()){
throw new ServiceException(BaseErrorCodeEnum.CODE_ERROR_DATA_SAME_ERROR.getMessage(), BaseErrorCodeEnum.CODE_ERROR_DATA_SAME_ERROR.getCode());
}
try {
// 获取文件输入流
InputStream is = file.getInputStream();
String fileUrl = fileStorageService.uploadFile("json", fileName, is);
System.out.println("fileUrl:" + fileUrl);
/** 更新产线可以访问的文件路径 */
if(fileUrl.contains(minIOConfigProperties.getEndpoint())){
fileUrl = fileUrl.replace(minIOConfigProperties.getEndpoint(), pointIP);
}
dataFormatConversionService.insertData(fileName, fileUrl);
return R.ok(fileUrl);
} catch (IOException e) {
e.printStackTrace();
return R.fail();
}
}
}
7. 启动项目后用postman调用接口测试
{{host}}变量是在postsman里面配置的环境,可直接用IP:Port替换掉即可
具体的上传文件类型,可自行设置更改,以此记录~