环境(pom.xml)
MinIO相关的maven包(核心)
<!-- MinIO相关 -->
<!-- 解决minio报错:okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.12</version>
</dependency>
其他辅助可能会用到的包
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<!-- 国密 -->
<!-- 要使用 SM 工具(中国商用密码算法)来计算并比较 InputStream 和 MultipartFile 的 MD5(实际上是 SM3 哈希值) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- hutool工具包:https://www.hutool.cn/docs/#/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<!-- Knife4j是基于springboot构建的一个文档生成工具,它可以让开发者为我们的应用生成API文档
=>
访问地址:项目根地址/doc.html => https://doc.xiaominfo.com/docs/quick-start -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
<!-- springWeb -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--去掉Jackson依赖,用fastjson -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- fastjson:https://springdoc.cn/spring-boot-fastjson2/ -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
application.yaml
MinIO配置部分(核心)
# minio配置 =》 参考:https://juejin.cn/post/7412486655862489114
minio:
# 服务地址
url: http://${dataResource.ip}:9010
# 用户名
access-key: ${dataResource.minio.accessKey}
# 密码
secret-key: ${dataResource.minio.password}
# 桶对象
bucket-name: upload-center
#临时切片存放位置
chunk-bucket-name: chunk-files
这里涉及到了我上一篇博客提到的配置,说明如下
url
:MInIO对外暴露的接口服务地址access-key
:服务通行证里的accessKey字段,若没有配置则是你的Root用户名secret-key
:服务通行证里的secretKey字段,若没有配置则是你的Root密码- bucket-name:你想将文件统一上传到MInIO的哪个桶里
- chunk-bucket-name:你想将切片文件统一上传到MInIO的哪个桶里
整个yaml配置为
server:
port: 4190
# context-path: /
spring:
mvc:
async:
# 请求响应时间设置 6s
request-timeout: 6000
application:
name: minio-upload-01
servlet:
# 限制文件上传大小
multipart:
# 最大上传大小限制为10MB。任何尝试上传超过这个大小的单个文件都会被拒绝。
max-file-size: 10MB
# 整个请求的最大大小限制为10MB。这包括所有上传文件的总和以及其他请求参数的大小
max-request-size: 10MB
enabled: true
# minio配置 =》 参考:https://juejin.cn/post/7412486655862489114
minio:
# 服务地址
url: http://${dataResource.ip}:9010
# 用户名
access-key: ${dataResource.minio.username}
# 密码
secret-key: ${dataResource.minio.password}
# 桶对象
bucket-name: upload-center
# 分片对象过期时间 单位(天)
#expiry: 1
# 断点续传有效时间,在redis存储任务的时间 单位(天)
#breakpoint-time: 1
#临时切片存放位置
bucket-name-slice: chunk-files
############## 自定义knife4j配置 ##############
# springdoc-openapi项目访问访问地址: http://127.0.0.1:8080/doc.html
springdoc:
swagger-ui:
# path: 配置swagger-ui.html/UI界面的访问路径,默认为/swagger-ui.html
path: /swagger-ui.html
# tags-sorter: 接口文档中的tags排序规则,默认为alpha,可选值为alpha(按字母顺序排序)或as-is(按照在代码中定义的顺序排序)
tags-sorter: alpha
# 该参数是swagger默认的排序规则,如果设置为alpha,那么Knife4j提供的按照order排序的增强规则不生效
# 使用增强order属性进行排序,或者不设置该参数
# operations-sorter: alpha
operations-sorter: order
api-docs:
path: /v3/api-docs
# path: 配置api-docs的访问路径,默认为/v3/api-docs
enabled: true #是否开启文档功能
group-configs:
# group-configs: 配置分组信息
- group: '默认'
# group: 分组名称
paths-to-match: '/**'
# paths-to-match: 配置要匹配的路径,默认为/**
packages-to-scan: com.ayo.controller
# packages-to-scan: 配置要扫描的包的路径,直接配置为Controller类所在的包名即可
# knife4j项目访问访问地址:http://127.0.0.1:8080/doc.html#/home
knife4j:
enable: true
# 设置为true以启用Knife4j增强功能,这将再应用程序中启用Knife4j UI
setting:
# language: 设置Knife4j UI的语言,默认为zh_cn,可选值为zh_cn或en
language: zh_cn
swagger-model-name: 数据对象 #重命名SwaggerModel名称,默认
#开启生产环境屏蔽
production: false
#是否启用登录认证
basic:
enable: true
username: root # 自己设置一个
password: 123456 # 自己设置一个
配置类
MinIO配置类(核心)
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/* 用户名 */
@NotEmpty(message = "minio认证账户不可为空")
private String accessKey;
/* 密码 */
@NotEmpty(message = "minio认证密码不可为空")
private String secretKey;
/* 服务地址 */
@NotEmpty(message = "minio服务地址不可为空")
@Pattern(regexp = "^(http|https)://.*$", message = "minio服务地址格式错误")
private String url;
/* 上传的数据桶 */
private String bucketName = "upload-files";
/* 切片存放的临时数据桶 */
private String chunkBucketName = "chunk-files";
/* 断点续传有效时间,在redis存储任务的时间 单位(天) */
//private String sliceExpiry = "1";
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.build();
}
}
Knife4j配置
@Configuration
public class Knife4jConfig {
@Bean
public OpenAPI customOpenAPI() {
//创建一个 OpenAPI 对象,用于表示整个 API 的文档信息
return new OpenAPI()
// 接口文档标题
.info(new Info().title("MinIO接口文档")
// 接口文档简介
.description("MinIO相关服务接口文档")
// 接口文档版本
.version("0.0.1-SNAPSHOT")
// 开发者的联系方式,包括姓名和电子邮件地址
.contact(new Contact().name("Ayo").email("2192475085@qq.com")));
}
}
FastJson配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
//自定义配置...
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setReaderFeatures(JSONReader.Feature.FieldBased, JSONReader.Feature.SupportArrayToBean);
config.setWriterFeatures(JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.PrettyFormat);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(StandardCharsets.UTF_8);
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
converters.add(0, converter);
}
}
其他配置相关
先准备一个自定义的工具类
@Slf4j
@Component
public class AyoUtils {
/**
* 检查服务运行状况
*
* @param urlString url字符串
* @return boolean
* @throws IOException ioexception
*/
public boolean checkMinioHealth(String urlString) throws IOException {
Socket socket = new Socket();
URL url = new URL(urlString);
String host = url.getHost();
int port = url.getPort();
try {
socket.connect(new InetSocketAddress(host, port), 2000);
return true;
} catch (IOException exception) {
return false;
} finally {
try {
socket.close();
} catch (IOException e) {
log.error("socket关闭失败");
}
}
}
/**
* 文件大小换算
*
* @param bytes 字节
* @return {@link String }
*/
public String formatFileSize(long bytes) {
if (bytes < 1024) {
return bytes + " Bytes";
} else if (bytes < 1024 * 1024) {
return String.format("%.2f KB", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024));
} else {
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
}
/**
* 移除 URL 中 ? 之后的所有部分
*
* @param url 包含查询参数的完整 URL
* @return 清理后的 URL
*/
public String removeUrlParameters(String url) {
// 查找 ? 的位置
int queryIndex = url.indexOf('?');
// 如果 ? 存在,返回 ? 之前的部分;否则返回原始 URL
if (queryIndex != -1) {
return url.substring(0, queryIndex);
} else {
return url;
}
}
}
预检MinIO服务
@Slf4j
@Component
@RequiredArgsConstructor
public class InitConfig implements InitializingBean {
private final MinioUtils minioUtils;
private final MinioConfig minioConfig;
private final AyoUtils ayoUtils;
@Override
public void afterPropertiesSet() {
//验证MinIO服务器是否在线
try {
if(!ayoUtils.checkMinioHealth(minioConfig.getUrl())){
log.error("MinIO服务器不在线");
return;
}else{
log.info("MinIO服务正常");
}
} catch (IOException e) {
log.error("验证出错,服务启动失败");
return;
}
String[] bucketNameArr = {minioConfig.getBucketName(), minioConfig.getChunkBucketName()};
for (String s : bucketNameArr) {
//验证桶是否存在
if (!minioUtils.bucketExists(s)) {
minioUtils.createBucket(s);
minioUtils.setBucketPolicy(s, BuckerPolicyEnum.READ.getRight());
}
}
}
}
我这里显示预检MinIO
服务是否在线,不在线就打印输出日志信息,具体你可以自己实现你要实现的功能
如果MInIO
服务在线,再检查对应的Yaml
配置文件,验证文件上传桶
和切片上传桶
是否存在,不存在则创建
MinioUtils
配置在下方实现
MinioUtils工具类(核心)
由于 MinIO的桶
创建需要配置一些访问策略
,所以我这里先将桶
的策略配置
提了出来
MinIO设置桶策略
桶策略接口
public interface IBucketPolicy {
boolean createBucketPolicy(MinioClient client, String bucket);
}
桶只读策略
@Slf4j
public class BuckerReadPolicy implements IBucketPolicy {
/**
* 桶占位符
*/
private static final String BUCKET_PARAM = "${bucket}";
/**
* bucket权限-只读
*/
private static final String READ_ONLY = "{\n" +
" "Version": "2012-10-17",\n" +
" "Statement": [\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:GetBucketLocation"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
" ]\n" +
" },\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:ListBucket"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
" ],\n" +
" "Condition": {\n" +
" "StringEquals": {\n" +
" "s3:prefix": [\n" +
" "*"\n" +
" ]\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:GetObject"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::" + BUCKET_PARAM + "/**"\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}";
@Override
public boolean createBucketPolicy(MinioClient client, String bucket) {
// TODO Auto-generated method stub
try {
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(READ_ONLY.replace(BUCKET_PARAM, bucket)).build());
return true;
} catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
| InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
| IllegalArgumentException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error("error: {}", e.getMessage(), e);
}
return false;
}
}
桶只写策略
@Slf4j
public class BuckerWritePoliy implements IBucketPolicy {
/**
* 桶占位符
*/
private static final String BUCKET_PARAM = "${bucket}";
/**
* bucket权限-只写
*/
private static final String WRITE_ONLY = "{\n" +
" "Version": "2012-10-17",\n" +
" "Statement": [\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:GetBucketLocation",\n" +
" "s3:ListBucketMultipartUploads"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
" ]\n" +
" },\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:AbortMultipartUpload",\n" +
" "s3:DeleteObject",\n" +
" "s3:ListMultipartUploadParts",\n" +
" "s3:PutObject"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +"/**"\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}";
@Override
public boolean createBucketPolicy(MinioClient client, String bucket) {
// TODO Auto-generated method stub
try {
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(WRITE_ONLY.replace(BUCKET_PARAM, bucket)).build());
return true;
} catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
| InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
| IllegalArgumentException | IOException e) {
log.error("error: {}", e.getMessage(), e);
}
return false;
}
}
桶读写策略
@Slf4j
public class BuckerReadWriterPolicy implements IBucketPolicy {
/**
* 桶占位符
*/
private static final String BUCKET_PARAM = "${bucket}";
/**
* bucket权限-读写
*/
private static final String READ_WRITE = "{\n" +
" "Version": "2012-10-17",\n" +
" "Statement": [\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:GetBucketLocation",\n" +
" "s3:ListBucketMultipartUploads"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
" ]\n" +
" },\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:ListBucket"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +""\n" +
" ],\n" +
" "Condition": {\n" +
" "StringEquals": {\n" +
" "s3:prefix": [\n" +
" "*"\n" +
" ]\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" "Effect": "Allow",\n" +
" "Principal": {\n" +
" "AWS": [\n" +
" "*"\n" +
" ]\n" +
" },\n" +
" "Action": [\n" +
" "s3:AbortMultipartUpload",\n" +
" "s3:DeleteObject",\n" +
" "s3:GetObject",\n" +
" "s3:ListMultipartUploadParts",\n" +
" "s3:PutObject"\n" +
" ],\n" +
" "Resource": [\n" +
" "arn:aws:s3:::"+ BUCKET_PARAM +"/**"\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}";
@Override
public boolean createBucketPolicy(MinioClient client, String bucket) {
// TODO Auto-generated method stub
try {
client.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(READ_WRITE.replace(BUCKET_PARAM, bucket)).build());
return true;
} catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
| InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
| IllegalArgumentException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error("error: {}", e.getMessage(), e);
}
return false;
}
}
桶策略枚举类
public enum BuckerPolicyEnum {
READ("read","只读"),
WRITE("write","只写"),
READ_WRITE("read-write","可读写");
private final String right;
private final String describe;
public String getRight() {
return right;
}
public String getDescribe() {
return describe;
}
BuckerPolicyEnum(String right, String describe){
this.right = right;
this.describe = describe;
}
}
桶策略工厂函数
public class BuckerPolicyFactory {
static Map<String, IBucketPolicy> operationMap = new HashMap<>();
static {
// 只读
operationMap.put(BuckerPolicyEnum.READ.getRight(), new BuckerReadPolicy());
// 只写
operationMap.put(BuckerPolicyEnum.WRITE.getRight(), new BuckerWritePoliy());
// 读写
operationMap.put(BuckerPolicyEnum.READ_WRITE.getRight(), new BuckerReadWriterPolicy());
}
public static IBucketPolicy getBucketPolicyInterface(String poliy){
IBucketPolicy object = operationMap.get(poliy);
if(object == null){
object = new BuckerDefaultPolicy();
}
return object;
}
}
封装MinioUtils
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtils {
private final MinioClient minioClient;
/**
* --------------------------------------------------------------------
* 桶操作(开始)
* --------------------------------------------------------------------
* */
/**
* 判断Bucket是否存在,true:存在,false:不存在
*
* @param bucketName 桶名
* @return boolean
*/
@SneakyThrows(Exception.class)
public boolean bucketExists(String bucketName) {
if (bucketName != null && !bucketName.isEmpty()) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
//BucketExistsArgs exist = BucketExistsArgs.builder().bucket(bucketName).build();
//boolean result = minioClient.bucketExists(exist);
}
return false;
}
/**
* 启动SpringBoot容器的时候初始化Bucket
* 如果没有Bucket则创建
*
* @param bucketName 桶名
* @return boolean
*/
@SneakyThrows(Exception.class)
public boolean createBucket(String bucketName) {
boolean flag = false;
if (!bucketExists(bucketName)) {
MakeBucketArgs create = MakeBucketArgs.builder().bucket(bucketName).build();
minioClient.makeBucket(create);
flag = true;
}
return flag;
}
/**
* 设置桶策略
*
* @param bucketName 桶名
* @param policy 策略(read,write,read-write)
* @return boolean
*/
@SneakyThrows(Exception.class)
public boolean setBucketPolicy(String bucketName, String policy) {
// 判断桶是否存在
if (bucketExists(bucketName)) {
IBucketPolicy bucketPolicy = BuckerPolicyFactory.getBucketPolicyInterface(policy);
return bucketPolicy.createBucketPolicy(minioClient, bucketName);
}
return false;
}
/**
* 删除桶
*
* @param bucketName 桶名
*/
@SneakyThrows(Exception.class)
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 获得Bucket的策略
*
* @param bucketName
* @return
*/
@SneakyThrows(Exception.class)
public String getBucketPolicy(String bucketName) {
return minioClient.getBucketPolicy(GetBucketPolicyArgs
.builder()
.bucket(bucketName)
.build());
}
/**
* 获得所有Bucket列表
*
* @return
*/
@SneakyThrows(Exception.class)
public List<Bucket> getAllBuckets() {
return minioClient.listBuckets();
}
/**
* --------------------------------------------------------------------
* 桶操作(结束)
* --------------------------------------------------------------------
* */
////////////////////////////////////////////////////////////////////////
/**
* --------------------------------------------------------------------
* 文件操作(开始)
* --------------------------------------------------------------------
* */
/**
* 根据bucketName获取其相关信息
*
* @param bucketName
* @return
*/
@SneakyThrows(Exception.class)
public Optional<Bucket> getBucket(String bucketName) {
return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* 创建文件夹或目录
*
* @param bucketName 存储桶
* @param objectName 目录路径
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse createDir(String bucketName, String objectName) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[]{});
ObjectWriteResponse response = minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(byteArrayInputStream, 0, -1)
.build());
byteArrayInputStream.close();
return response;
}
/**
* 判断文件是否存在
*
* @param bucketName
* @param objectName
* @return
*/
public boolean isObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
//log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e);
exist = false;
}
return exist;
}
/**
* 判断文件夹是否存在
*
* @param bucketName
* @param objectName
* @return
*/
public boolean isFolderExist(String bucketName, String objectName) {
boolean exist = false;
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir() && objectName.equals(item.objectName())) {
exist = true;
}
}
} catch (Exception e) {
//log.error("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e);
exist = false;
}
return exist;
}
/**
* 根据文件前置查询文件
*
* @param bucketName 存储桶
* @param prefix 前缀
* @param recursive 是否使用递归查询
* @return MinioItem 列表
*/
@SneakyThrows(Exception.class)
public List<Item> getAllObjectsByPrefix(String bucketName,
String prefix,
boolean recursive) {
List<Item> list = new ArrayList<>();
Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
if (objectsIterator != null) {
for (Result<Item> o : objectsIterator) {
Item item = o.get();
list.add(item);
}
}
return list;
}
/**
* 获取文件流
*
* @param bucketName 存储桶
* @param objectName 文件名
* @return 二进制流
*/
//@SneakyThrows(Exception.class)
public GetObjectResponse getObject(String bucketName, String objectName) {
try {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
} catch (Exception e) {
log.warn("文件获取【{objectName}】失败");
return null;
}
}
/**
* 断点下载
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @param offset 起始字节的位置
* @param length 要读取的长度
* @return 二进制流
*/
@SneakyThrows(Exception.class)
public GetObjectResponse getObject(String bucketName, String objectName, long offset, long length) {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.offset(offset)
.length(length)
.build());
}
/**
* 获取路径下文件列表
*
* @param bucketName 存储桶
* @param prefix 文件名称
* @param recursive 是否递归查找,false:模拟文件夹结构查找
* @return 二进制流
*/
public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
return minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(recursive)
.build());
}
/**
* 使用MultipartFile进行文件上传
*
* @param bucketName 存储桶
* @param file 文件名
* @param objectName 对象名
* @param contentType 类型
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
InputStream inputStream = file.getInputStream();
ObjectWriteResponse response = minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.contentType(contentType)
.stream(inputStream, inputStream.available(), -1)
.build());
inputStream.close();
return response;
}
/**
* 通过md5获取分片列表 块文件必须满足 名字是 1 2 3 5....(索引从1开始)
*
* @param chunkBucketName 分片桶名称
* @param fileMd5 源文件md5
* @param chunkTotal 分片总数量
* @return {@link List }<{@link ComposeSource }> 分片集
*/
public List<ComposeSource> getChunkListByMd5(String chunkBucketName, String fileMd5, Integer chunkTotal) {
// 获取所以分块
List<Item> chunkList = getChunkList(chunkBucketName, fileMd5);
// 获取缺失的分块
List<Integer> missChunkIndexList = getMissChunkIndexList(chunkList, chunkTotal);
if (!missChunkIndexList.isEmpty()) {
throw new RuntimeException(
String.format(
"丢失分片索引:%s",
missChunkIndexList.toString()
)
);
}
// 获取块资源
List<ComposeSource> composeSourceList = new ArrayList<>();
for (Item item : chunkList) {
composeSourceList.add(ComposeSource.builder().bucket(chunkBucketName).object(item.objectName()).build());
}
return composeSourceList;
}
///**
// * 通过md5删除分块列表
// *
// * @param chunkBucketName 分块所在的桶名称
// * @param fileMd5 源文件md5
// * @return boolean
// */
//public void removeChunkListByMd5(String chunkBucketName, String fileMd5) {
// // 获取所以分块
// //List<Item> chunkList = getChunkList(chunkBucketName, fileMd5);
// removeFile(chunkBucketName, fileMd5);
//}
/**
* 合并分片块
*
* @param composeSourceList 分片集
* @param chunkBucketName 分片所在的桶
* @param bucketName 合并到哪个桶
* @param fileName 合并后的文件名称
* @return boolean
*/
@SneakyThrows
public boolean mergeChunk(List<ComposeSource> composeSourceList, String chunkBucketName, String bucketName, String fileName) {
String contentType = ViewContentTypeEnum.getContentType(fileName);
Map<String,String> header = new HashMap<>();
header.put("Content-Type",contentType);
// 合并
ObjectWriteResponse composeResponse = minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.sources(composeSourceList)
.headers(header)
.build());
// 上传对象并设置 Content-Type
//minioClient.putObject(
// PutObjectArgs.builder()
// .bucket(bucketName)
// .object(fileName)
// .contentType(ViewContentTypeEnum.getContentType(fileName))
// .build()
//);
return true;
}
/**
* 设置合并块的ContentType => 用于某些类型的文件链接访问不触发下载而是直接预览
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return boolean
*/
@SneakyThrows
public boolean setMergeChunkContentType(String bucketName, String fileName) {
GetObjectResponse object = getObject(bucketName, fileName);
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.contentType(ViewContentTypeEnum.getContentType(fileName))
// 这里需要提供一个有效的流
//.stream(new ByteArrayInputStream(new byte[0]), 0, -1)
.stream(object, object.available(), -1)
.build());
object.close();
return true;
}
/**
* 上传本地文件
*
* @param bucketName 存储桶
* @param objectName 对象名称
* @param fileName 本地文件路径
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
return minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(fileName)
.build());
}
/**
* 通过流上传文件
*
* @param bucketName 存储桶
* @param objectName 文件对象
* @param inputStream 文件流
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.contentType(ViewContentTypeEnum.getContentType(objectName))
.stream(inputStream, inputStream.available(), -1)
.build());
}
/**
* 获取文件信息, 如果抛出异常则说明文件不存在
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @return
*/
@SneakyThrows(Exception.class)
public Map<String, String> getFileStatusInfo(String bucketName, String objectName) {
String dataStr = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()).toString();
// 更新正则表达式以匹配带连字符的键
String regex = "([\w-]+)=([^,}]+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(dataStr);
Map<String, String> map = new HashMap<>();
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
map.put(key, value);
}
return map;
}
/**
* 拷贝文件
*
* @param bucketName 存储桶
* @param objectName 文件名
* @param srcBucketName 目标存储桶
* @param srcObjectName 目标文件名
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
return minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(objectName).build())
.bucket(srcBucketName)
.object(srcObjectName)
.build());
}
/**
* 删除文件
*
* @param bucketName 存储桶
* @param objectName 文件名称
*/
@SneakyThrows(Exception.class)
public void removeFile(String bucketName, String objectName) {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 批量删除文件
*
* @param bucketName 存储桶
* @param keys 需要删除的文件列表
* @return
*/
public void removeFiles(String bucketName, List<String> keys) {
List<DeleteObject> objects = new LinkedList<>();
keys.forEach(s -> {
objects.add(new DeleteObject(s));
try {
removeFile(bucketName, s);
} catch (Exception e) {
log.error("[Minio工具类]>>>> 批量删除文件,异常:", e);
}
});
}
/**
* 删除文件
*
* @param bucketName 存储桶
* @param dir 分片所在的文件夹
*/
@SneakyThrows(Exception.class)
public void removeAllChunk(String bucketName, String dir) {
List<Item> chunkList = getChunkList(bucketName, dir);
//遍历所有的文件,将其加入待删除列表中
List<String> chunkNames = new ArrayList<>();
for (Item item : chunkList) {
chunkNames.add(item.objectName());
}
removeFiles(bucketName, chunkNames);
}
/**
* 获取文件外链并设置过期时间(设置过cont-type,否则都是下载链接)
*
* @param bucketName 存储桶
* @param objectName 文件名
* @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
return getUtf8ByURLDecoder(
minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs
.builder()
.expiry(expires)
.bucket(bucketName)
.object(objectName)
.build()
)
);
}
/**
* 获得文件下载外链
*
* @param bucketName
* @param objectName
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName) {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET).build();
//return minioClient.getPresignedObjectUrl(args);
return getUtf8ByURLDecoder(minioClient.getPresignedObjectUrl(args));
}
/**
* --------------------------------------------------------------------
* 文件操作(结束)
* --------------------------------------------------------------------
* */
/**
* --------------------------------------------------------------------
* 其他操作(开始)
* --------------------------------------------------------------------
* */
/**
* 将一个 Base64 编码的字符串转换为 InputStream 对象
*
* @param base64 Base64 编码的字符串
* @return InputStream
*/
public static InputStream base64ToInputStream(String base64) {
ByteArrayInputStream stream = null;
try {
byte[] bytes = Base64.decodeBase64(base64.trim());
stream = new ByteArrayInputStream(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return stream;
}
/**
* 获取分片列表
*
* @param chunkBucketName 块桶名称
* @param md5 源文件md5 => 用于生成唯一文件夹和读取文件夹下的块
* @return {@link List }<{@link Item }>
*/
private List<Item> getChunkList(String chunkBucketName, String md5) {
Iterable<Result<Item>> resultIterable = listObjects(chunkBucketName, md5, true);
// 分块
List<Item> result = new ArrayList<>();
for (Result<Item> itemResult : resultIterable) {
try {
result.add(itemResult.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 分片文件排序
result.sort((o1, o2) -> {
String o1Name = o1.objectName();
String o2Name = o2.objectName();
int o1Index = Integer.parseInt(o1Name.substring(o1Name.lastIndexOf("/") + 1));
int o2Index = Integer.parseInt(o2Name.substring(o2Name.lastIndexOf("/") + 1));
return o1Index - o2Index;
});
return result;
}
/**
* 获取缺失的分片
*
* @param chunkList 分片列表
* @param chunkTotal 分片总数
* @return 缺失的分片列表
*/
private List<Integer> getMissChunkIndexList(List<Item> chunkList, Integer chunkTotal) {
// 缺失的分片文件
List<Integer> missChunkIndexList = new ArrayList<>();
// 列出已经存在的分片存在set里
HashSet<Integer> chunkIndexSet = new HashSet<>();
for (Item item : chunkList) {
String chunkName = item.objectName();
int chunkIndex = Integer.parseInt(chunkName.substring(chunkName.lastIndexOf("/") + 1));
chunkIndexSet.add(chunkIndex);
}
// 分片索引应该从1开始
for (int i = 1; i <= chunkTotal; i++) {
// 是否缺少当前分片
if (!chunkIndexSet.contains(i)) {
missChunkIndexList.add(i);
}
}
return missChunkIndexList;
}
/**
* 将URLDecoder编码转成UTF8
*
* @param str
* @return
* @throws UnsupportedEncodingException
*/
public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
return URLDecoder.decode(url, "UTF-8");
}
/**
* --------------------------------------------------------------------
* 其他操作(结束)
* --------------------------------------------------------------------
* */
}
这里我们可以看见MinioUtils
工具类setBucketPolicy
方法就需要用到桶策略生成的工厂夯实去实现配置桶的策略,其次我们在InitConfig
预检MinIO服务中将新建的桶都设置为只读
minioUtils.setBucketPolicy(s, BuckerPolicyEnum.READ.getRight());
你也可以根据自己的需求设置
其他需要用到的类(由业务决定)
这些类不涉及MinIO的核心操作,根据个人业务需求确定是否需要
文件比较类
public class FileComparer {
static {
// 注册 BouncyCastle 提供者
Security.addProvider(new BouncyCastleProvider());
}
/**
* 比较文件
*
* @param multipartFile 多部分文件
* @param inputStream 输入流
* @return boolean
* @throws IOException ioexception
*/
public static boolean compareFiles(MultipartFile multipartFile, InputStream inputStream) {
String sm3FromMultipartFile = null;
String sm3FromInputStream = null;
try {
sm3FromMultipartFile = calculateSm3(multipartFile.getInputStream());
sm3FromInputStream = calculateSm3(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
return sm3FromMultipartFile.equals(sm3FromInputStream);
}
/**
* 计算sm3
*
* @param inputStream 输入流
* @return {@link String }
* @throws IOException ioexception
*/
private static String calculateSm3(InputStream inputStream) throws IOException {
SM3Digest digest = new SM3Digest();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
byte[] hash = new byte[digest.getDigestSize()];
digest.doFinal(hash, 0);
return bytesToHex(hash);
}
/**
* 字节到十六进制
*
* @param bytes 字节
* @return {@link String }
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
由于封装的MininUtil
判断文件是否存在是通过文件名
来的,同一个文件
不同名
会判断false
,即文件不存在
这里我需要用它来判断上传的文件是否存在,逻辑线是:
- 先根据文件名判断文件是否在桶中存在
- 不存在正常上传
- 存在则通过该类比较两个文件是否为一个文件
- 是同一个文件直接返回MinIO里的文件
- 不是同一个文件则重新生成一个唯一的文件名(UUID),再进行上传操作
注意:若是考虑的实际个人建议是通过文件的 【
md5值_文件名
】来设置文件名,然后通过数据库来存储文件的原始信息,例如文件的原始名,文件访问地址,文件md5值等,验证的时候直接走数据库,我这里由于是测试所以就用的UUID
统一返回结果
@Data
@Schema(name = "统一返回的结果对象➱Result<T>")
public class Result<T> {
//@Schema(description = "返回状态")
//private Boolean flag;
@Schema(description = "状态码")
private int code;
@Schema(description = "返回信息")
private String msg;
@Schema(description = "返回数据")
private T data;
/**
* 私有静态方法,用于构建一个 Result 对象。
* 接受 flag、data、code 和 message 四个参数,并返回一个根据这些参数构建的 Result 对象。
*/
private static <T> Result<T> buildResult(T data, Integer code, String msg) {
Result<T> r = new Result<>();
r.setData(data);
r.setCode(code);
r.setMsg(msg);
return r;
}
/**
* 成功---------------------------------------------
*/
public static <T> Result<T> success() {
ResultCodeEnum success = ResultCodeEnum.SUCCESS;
return success((T) Collections.emptyMap(), success.getCode(), success.getZhMsg());
}
public static <T> Result<T> success(T data) {
ResultCodeEnum success = ResultCodeEnum.SUCCESS;
return success(data, success.getCode(), success.getZhMsg());
}
public static <T> Result<T> success(T data, String msg) {
ResultCodeEnum success = ResultCodeEnum.SUCCESS;
return success(data, success.getCode(), msg);
}
public static <T> Result<T> success(T data, Integer code, String msg) {
return buildResult( data, code, msg);
}
/**
* 失败---------------------------------------------
*/
public static <T> Result<T> fail() {
return fail(ResultCodeEnum.FAIL.getCode(), ResultCodeEnum.FAIL.getZhMsg());
}
public static <T> Result<T> fail(String msg) {
return fail(ResultCodeEnum.FAIL.getCode(), msg);
}
public static <T> Result<T> fail(Integer code, String msg) {
return buildResult( (T) Collections.emptyMap(), code, msg);
}
/**
* 服务器错误---------------------------------------------
*/
public static <T> Result<T> error() {
ResultCodeEnum errorCode = ResultCodeEnum.INTERNAL_SERVER_ERROR;
return error(errorCode.getCode(),errorCode.getZhMsg());
}
public static <T> Result<T> error(String msg) {
return error( ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(),msg);
}
public static <T> Result<T> error(Integer code, String msg) {
return buildResult( (T) Collections.emptyMap(), code, msg);
}
/**
* 设置数据 => 如果 data 为空,则设置为空Map
*/
public void setData(T data) {
//if(data == null || data.equals("")){
// //this.data = (T) new HashMap<String,Object>();
//}else{
// this.data = data;
//}
this.data = Optional.ofNullable(data).orElse((T) new HashMap<String, Object>());
}
}
全局统一异常处理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理系统异常
*/
@ExceptionHandler(value = Exception.class)
public Result<?> handleSystemException(Exception e) {
log.error("系统异常:{}",e.getMessage());
return Result.fail(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
}
/**
* 处理运行时异常
*/
@ExceptionHandler(value = RuntimeException.class)
public Result<?> handleRuntimeException(RuntimeException e) {
log.error("程序发生错误:{}", e.getMessage());
return Result.fail(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
}
/**
* 处理SpringBoot Validation 参数验证抛出的异常
*/
//处理MethodArgumentNotValidException,该异常会在请求参数验证失败时抛出
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException e) {
log.error("请求参数验证异常:{}",e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(), e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}
//处理ConstraintViolationException,该异常会在方法级别的参数验证失败时抛出
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
log.error("方法参数验证失败");
return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(), e.getConstraintViolations().iterator().next().getMessage());
}
/**
* 处理Assert异常
*/
@ExceptionHandler(IllegalArgumentException.class)
public Result<?> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("服务器内部执行出行非法参数");
return Result.fail(ResultCodeEnum.BAD_REQUEST.getCode(),e.getMessage());
}
/**
* 文件上传大小超出限制
*/
@ExceptionHandler(MultipartException.class)
public Result<?> handleBusinessException(MaxUploadSizeExceededException ex) {
String msg;
Throwable rootCase = ex.getRootCause();
if (rootCase instanceof org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException) {
msg = "上传文件过大[单文件大小不得超过50M]";
} else if (rootCase instanceof org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException) {
msg = "上传文件过大[总上传文件大小不得超过50M]";
} else {
msg = "上传文件失败";
}
log.error(msg);
//return Result.fail(ResultCodeEnum.UPLOAD_FILE_FAILED.getCode(), msg );
return Result.success(new StateAndMsgVo(false, msg));
}
/**
* 文件不存在
*/
@ExceptionHandler(value = MissingServletRequestPartException.class)
public Result<?> handleMissingServletRequestPartException() {
return Result.fail(ResultCodeEnum.NOT_FOUND.getCode(), ResultCodeEnum.NOT_FOUND.getZhMsg());
}
/**
* 连接异常
*/
@ExceptionHandler(value = ConnectException.class)
public Result<?> handleConnectException(ConnectException e) {
return Result.fail(ResultCodeEnum.REQUEST_TIMEOUT.getCode(), "MinIO服务器不在线,请联系管理员进行检查");
}
}
跨域处理
@Configuration
public class MyCorsFilter {
@Bean
public CorsFilter corsFilter() {
// 1.创建 CORS 配置对象
CorsConfiguration config = new CorsConfiguration();
// 支持域
config.addAllowedOriginPattern("*");
// 是否发送 Cookie
config.setAllowCredentials(true);
// 支持请求方式
config.addAllowedMethod("*");
// 允许的原始请求头部信息
config.addAllowedHeader("*");
// 暴露的头部信息
config.addExposedHeader("*");
// 2.添加地址映射
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
// 3.返回 CorsFilter 对象
return new CorsFilter(corsConfigurationSource);
}
}
VO
文件信息
@Data
@Schema(name = "文件信息➱FileInfoVo")
public class FileInfoVo {
@Schema(description = "桶名")
private String bucket;
@Schema(description = "文件名")
private String name;
@Schema(description = "文件大小")
private String size;
@Schema(description = "文件安全地址(长)")
private String safeUrl;
@Schema(description = "文件不安全地址(短)")
private String url;
@Schema(description = "文件更新时间")
//private LocalDateTime updateTime;
private String updateTime;
@Schema(description = "获取文件信息是否成功")
private Boolean state = true;
public FileInfoVo(String bucket, String name, String size, String safeUrl, String url, String updateTime, Boolean state) {
this.bucket = bucket;
this.name = name;
this.size = size;
this.safeUrl = safeUrl;
this.url = url;
this.updateTime = updateTime;
this.state = state;
}
public FileInfoVo(String bucket, String name, String size, String safeUrl, String url, String updateTime) {
this.bucket = bucket;
this.name = name;
this.size = size;
this.safeUrl = safeUrl;
this.url = url;
this.updateTime = updateTime;
state = false;
}
public FileInfoVo() {
}
/* minio工具类返回的Map数据进行相应的转换 */
public FileInfoVo(Map<String, String> minioMap) {
//文件名称
this.name = minioMap.get("object");
// 桶
this.bucket = minioMap.get("bucket");
//获取文件尺寸
long fileSize = Long.parseLong(minioMap.get("size"));
this.size = new AyoUtils().formatFileSize(fileSize);
// 服务器文件上传时间 => 该时间受MinIO所在服务器时间的影响 => 前端请求时间和服务器实际时间相差过大会被MinIO拒绝请求
Date date = DateUtil.parse(minioMap.get("last-modified"));
this.updateTime = DateUtil.formatDateTime(date);
}
}
状态信息
@Data
@Schema(name = "状态和信息➱StateAndMsg")
public class StateAndMsgVo {
@Schema(description = "操作状态")
private Boolean state;
@Schema(description = "信息")
private String msg;
public StateAndMsgVo() {
}
public StateAndMsgVo(Boolean state, String msg) {
this.state = state;
this.msg = msg;
}
public void setStateAndMsg(Boolean state, String msg) {
this.state = state;
this.msg = msg;
}
}
service层
接口
/**
* 文件上传服务
*
* @author 山居
* @date 2024/09/04 15:15
*/
public interface IFileService {
/**
* Minio是否在线
*
* @return {@link Result }
*/
Boolean minioIsOk();
/**
* 文件是否存在
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return {@link Boolean }
*/
Boolean fileIsExist(String bucketName, String fileName);
/**
* 文件是否存在
*
* @param fileName 文件名称
* @return {@link Boolean }
*/
Boolean fileIsExist(String fileName);
/**
* 文件上传
*
* @param file 文件
* @return {@link String }
*/
FileInfoVo fileUpload(MultipartFile file);
/**
* 获取文件信息
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return {@link FileInfoVo }
*/
FileInfoVo getFileInfo(String bucketName, String fileName);
/**
* 获取文件信息
*
* @param fileName 文件名称
* @return {@link FileInfoVo }
*/
FileInfoVo getFileInfo(String fileName);
/**
* 文件删除
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return {@link Boolean }
*/
Boolean fileDelete(String bucketName, String fileName);
/**
* 文件删除
*
* @param fileName 文件名称
* @return {@link Boolean }
*/
Boolean fileDelete(String fileName);
}
实现类
@Slf4j
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements IFileService {
private final MinioUtils minioUtils;
private final MinioConfig minioConfig;
private final AyoUtils ayoUtils;
// MinIO服务是否正常
@Override
public Boolean minioIsOk() {
try {
return ayoUtils.checkMinioHealth(minioConfig.getUrl());
} catch (IOException e) {
return false;
}
}
// 文件是否存在
@Override
public Boolean fileIsExist(String bucketName, String fileName) {
return minioUtils.isObjectExist(bucketName, fileName);
}
// 文件是否存在
@Override
public Boolean fileIsExist(String fileName) {
return fileIsExist(minioConfig.getBucketName(), fileName);
}
// 文件上传
@Override
public FileInfoVo fileUpload(MultipartFile file) {
/* 获取上传文件信息 => 先上传再获取 */
//{bucket=upload-center, object=ic_tree_1.png, last-modified=2024-09-04T08:50:02Z, size=5490}
Map<String, String> servicFileInfo = uploadGetServicFileInfo(file);
return mapFormatterFileInfo(minioConfig.getBucketName(),servicFileInfo);
}
//获取文件信息
@Override
public FileInfoVo getFileInfo(String bucketName, String fileName) {
if (fileIsExist(fileName)){
Map<String, String> fileStatusInfo = minioUtils.getFileStatusInfo(bucketName, fileName);
return mapFormatterFileInfo(bucketName,fileStatusInfo);
}
return null;
}
//获取文件信息
@Override
public FileInfoVo getFileInfo(String fileName) {
return getFileInfo(minioConfig.getBucketName(),fileName);
}
// 文件删除
@Override
public Boolean fileDelete(String bucketName, String fileName) {
if (!fileIsExist(bucketName, fileName)) {
return false;
}
minioUtils.removeFile(bucketName, fileName);
return true;
}
@Override
public Boolean fileDelete(String fileName) {
return fileDelete(minioConfig.getBucketName(), fileName);
}
/**
* ==========================================================================
* 其他方法封装
* ==========================================================================
* */
// 获取服务器文件信息 => 存在直接获取信息,不存在上传再获取信息
private Map<String, String> uploadGetServicFileInfo(MultipartFile file) {
//文件名
String fileName = file.getOriginalFilename();
// 验证MinIO服务器上是否存在同名文件
if (fileIsExist(fileName)) {
//比较文件的hash值是否为相同文件
InputStream servicFile = minioUtils.getObject(minioConfig.getBucketName(), fileName);
//文件是否一致(同名,且文件md5值一致)
boolean isEqual = compareFiles(file, servicFile);
if (!isEqual) {
// 同名但是文件md5不一致,取个新的文件名
String newFileName = IdUtil.simpleUUID() + "." + StringUtils.substringAfterLast(fileName, ".");
minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, file.getContentType());
log.warn("{} 存在同名文件,但是文件不一致,文件名被修改为 {}", fileName, newFileName);
fileName = newFileName;
} else {
log.warn("{} 存在完全一致的文件,不再重复上传操作!!!", fileName);
}
} else {
minioUtils.uploadFile(minioConfig.getBucketName(), file, fileName, file.getContentType());
log.info("文件 {} 不存在,正常上传", fileName);
}
return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
}
// minio工具生成的Map数据转成FileInfo类型的vo数据
FileInfoVo mapFormatterFileInfo(String bucketName, Map<String,String> servicFileInfo){
/* 返回数据 */
FileInfoVo fileInfoVo = new FileInfoVo(servicFileInfo);
/* 获取文件URL地址 */
String fileUrl = minioUtils.getPresignedObjectUrl(bucketName, fileInfoVo.getName());
// 文件访问地址
fileInfoVo.setSafeUrl(fileUrl);
fileInfoVo.setUrl(ayoUtils.removeUrlParameters(fileUrl));
return fileInfoVo;
}
}
controller层
@ApiSort(1)
@Tag(name = "MinIO文件上传", description = "MinIO文件上传相关API接口")
@Slf4j
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {
private final IFileService fileService;
@ApiOperationSupport(order = 1)
@Operation(summary = "验证服务器是否在线")
@GetMapping("minioIsOk")
Result<?> minioIsOk() {
StateAndMsgVo stateAndMsgVo = new StateAndMsgVo(false, "服务未验证");
if (fileService.minioIsOk()) {
stateAndMsgVo.setState(true);
stateAndMsgVo.setMsg("MinIO服务器为在线状态");
} else {
stateAndMsgVo.setMsg("MinIO服务器不在线");
}
return Result.success(stateAndMsgVo);
}
/**
* 文件上传操作应该使用 @PostMapping。@GetMapping 不支持处理 multipart/form-data 请求。
*/
@ApiOperationSupport(order = 2)
@Operation(
summary = "文件上传",
parameters = {
@Parameter(name = "file", description = "要上传的文件", required = true)
}
)
@PostMapping("fileUpload")
Result<?> fileUpload(@RequestParam MultipartFile file) {
return Result.success(fileService.fileUpload(file));
}
/**
* 根据文件名获取文件信息
*/
@ApiOperationSupport(order = 3)
@Operation(
summary = "获取文件信息",
parameters = {
@Parameter(name = "fileName", description = "要查询的文件名", required = true)
}
)
@PostMapping("fileInfo")
Result<?> fileUpload(@RequestParam(value = "fileName", required = true, defaultValue = "") String fileName) {
if (fileName.isEmpty()) {
return Result.success(new StateAndMsgVo(false, "必要参数文件名不存在"));
}
FileInfoVo fileInfoVo = fileService.getFileInfo(fileName);
return Result.success(Objects.requireNonNullElseGet(fileInfoVo, () -> new StateAndMsgVo(false, "文件不存在")));
}
/**
* 文件删除
*/
@ApiOperationSupport(order = 4)
@Operation(
summary = "文件删除",
parameters = {
@Parameter(name = "fileName", description = "要删除的文件名", required = true)
}
)
@DeleteMapping("fileDelete")
Result<?> fileDelete(@RequestParam String fileName) {
if (fileName.isEmpty()) {
return Result.success(new StateAndMsgVo(false, "必要参数文件名不存在"));
}
StateAndMsgVo stateAndMsgVo = new StateAndMsgVo(false, "删除操作失败,“" + fileName + "”文件不存在");
if (fileService.fileDelete(fileName)) {
stateAndMsgVo.setStateAndMsg(true, "文件“" + fileName + "”删除成功");
}
return Result.success(stateAndMsgVo);
}
}
测试
这里就不展示了,直接去knife4j的发布页面自己测测
PS
若有不足之处欢迎👏指正