简介
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。目前支持JavaScript 、Java、Python、Golang、.NET。
MinIO 是一款高性能、分布式的对象存储系统. 它是一款软件产品, 可以100%的运行在标准硬件。即X86等低成本机器也能够很好的运行MinIO。
MinIO与传统的存储和其他的对象存储不同的是:它一开始就针对性能要求更高的私有云标准进行软件架构设计。因为MinIO一开始就只为对象存储而设计。所以他采用了更易用的方式进行设计,它能实现对象存储所需要的全部功能,在性能上也更加强劲,它不会为了更多的业务功能而妥协,失去MinIO的易用性、高效性。 这样的结果所带来的好处是:它能够更简单的实现局有弹性伸缩能力的原生对象存储服务。
MinIO在传统对象存储用例(例如辅助存储,灾难恢复和归档)方面表现出色。同时,它在机器学习、大数据、私有云、混合云等方面的存储技术上也独树一帜。当然,也不排除数据分析、高性能应用负载、原生云的支持。
在中国:阿里巴巴、腾讯、百度、中国联通、华为、中国移动等等9000多家企业也都在使用MinIO产品
MinIO现在也是CNCF成员,在云原生存储部分和ceph等一起作为目前的解决方案之一。
Windows/Linux单机部署Minio
下载
根据系统下载安装包
Windows
下载安装包
在minio.exe目录打开cmd
配置参数并启动
# 设置用户名
set MINIO_ACCESS_KEY=admin
# 设置密码(8位)
set MINIO_SECRET_KEY=admin123
# 指定启动端口(未指定默认9000)及存储位置
minio.exe server --address 0.0.0.0:9999 D:/data
登录地址IP+9999,输入用户名及密码,搭建完成
Linux
下载安装包,因为我用的是华为云鲲鹏Centos7服务器,所以下载的ARM64版本
创建相关目录
# 创建文件存储目录
mkdir -p /data/minio
# 创建程序存放目录,并上传minio至此目录
mkdir -p /usr/local/minio
cd /usr/local/minio
# 修改可读权限
chmod +x minio
# 设置用户名
export MINIO_ACCESS_KEY=admin
# 设置密码
export MINIO_SECRET_KEY=admin123
启动并访问首页
# 启动,此处只是演示,实际使用需nohup或注册为服务启动
./minio server --address 0.0.0.0:9998 /data/minio
Spring Boot集成Minio
环境搭建
首先我们搭建一个spring boot基础工程,引入以下依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.3.4</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
配置文件设置
在application.yml中设置
# Tomcat
server:
port: 8888
spring:
# 配置文件上传大小限制
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# Minio配置
minio:
# 访问的url
endpoint: http://127.0.0.1:9000
# API的端口
port: 9000
# 秘钥
accessKey: minioadmin
secretKey: minioadmin
bucketName: miniohjl # 桶名 我这是给出了一个默认桶名
image-size: 10485760 # 我在这里设定了 图片文件的最大大小
file-size: 1073741824 # 此处是设定了文件的最大大小
allowFileType: jpg,png,jpeg,zip,rar,doc,docx,xls,xlsx,img,iso
# 分片上传有效期: 秒
chunkUploadExpirySecond: 86400
配置类设置
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class ParamConfig {
private String endpoint;
private String port;
private String accessKey;
private String secretKey;
private String bucketName;
/**
* 运行上传的文件类型
*/
private String allowFileType;
/**
* 分片上传有效期(单位:秒)
*/
private Integer chunkUploadExpirySecond;
}
工具类整合
package com.example.minio.common.utils;
import cn.hutool.core.date.DateUtil;
import com.example.minio.common.config.ParamConfig;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
/**
* @Author: HOUJL
* @Date: 2023/2/14
* @Description:
*/
@Slf4j
@Component
public class MinIoUtil {
public static MinioClient minioClient;
public final ParamConfig paramConfig;
public MinIoUtil(ParamConfig paramConfig) {
this.paramConfig = paramConfig;
}
/**
* 初始化minio配置
*/
@PostConstruct
public void init() {
try {
log.info("Minio Initialize........................");
minioClient = MinioClient.builder().endpoint(paramConfig.getEndpoint()).credentials(paramConfig.getAccessKey(), paramConfig.getSecretKey()).build();
createBucket(paramConfig.getBucketName());
log.info("Minio Initialize........................successful");
} catch (Exception e) {
log.error("初始化minio配置异常: 【{}】", e.fillInStackTrace());
}
}
/****************************** Operate Bucket Start ******************************/
/**
* 判断bucket是否存在
*/
@SneakyThrows(Exception.class)
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建bucket
*/
@SneakyThrows(Exception.class)
public void createBucket(String bucketName) {
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!isExist) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* 获取全部bucket
*/
@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();
}
/**
* 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在
*
* @param bucketName
* @throws Exception
*/
@SneakyThrows(Exception.class)
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/****************************** Operate Bucket End ******************************/
/**
* 上传本地文件
*
* @param bucketName 存储桶
* @param fileName 对象名称
* @param filePath 本地文件路径
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String fileName, String filePath) {
return minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.filename(filePath)
.build());
}
/**
* 上传文件
* 返回可以直接预览文件的URL
*/
public String uploadFile(MultipartFile file) {
try {
//如果存储桶不存在则创建
if (!bucketExists(paramConfig.getBucketName())) {
createBucket(paramConfig.getBucketName());
}
String originalFilename = file.getOriginalFilename();
if ("".equals(originalFilename)) {
originalFilename = file.getName();
}
//保证文件不重名(并且没有特殊字符)
String fileName = DateUtil.format(new Date(), "yyyyMMddHHmmss") + "_" + originalFilename;
minioClient.putObject(PutObjectArgs.builder()
.bucket(paramConfig.getBucketName())
.object(fileName)
.stream(file.getInputStream(), file.getInputStream().available(), -1)
.contentType(file.getContentType())
.build());
return getPreviewFileUrl(paramConfig.getBucketName(), fileName);
} catch (Exception e) {
log.error(e.getMessage(), e);
return "";
}
}
@SneakyThrows(Exception.class)
public String putFile(String bucketName, String fileName, InputStream stream) {
return upload(bucketName, fileName, stream, "application/octet-stream");
}
/**
* 文件上传
* 返回下载文件url地址 和下面upload方法仅传参不同
* bucketName 也可以直接从ParamConfig对象中获取
*/
@SneakyThrows(Exception.class)
public String upload(String bucketName, String fileName, InputStream stream, String contentType) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(stream, stream.available(), -1)
.contentType(contentType)
.build());
return getPreviewFileUrl(bucketName, fileName);
}
/**
* 文件上传
* 返回下载文件url地址 和上面upload方法仅传参不同
*/
@SneakyThrows(Exception.class)
public String upload(String bucketName, MultipartFile file) {
final InputStream is = file.getInputStream();
final String fileName = file.getOriginalFilename();
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(is, is.available(), -1)
.contentType(file.getContentType())
.build());
is.close();
return getPreviewFileUrl(bucketName, fileName);
}
/**
* 删除文件
*
* @param bucketName: 桶名
* @param fileName: 文件名
*/
@SneakyThrows(Exception.class)
public void deleteFile(String bucketName, String fileName) {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build());
}
/**
* 批量删除
*
* @param fileNames
*/
@SneakyThrows(Exception.class)
public void removeFiles(List<String> fileNames) {
removeFiles(paramConfig.getBucketName(), fileNames);
}
/**
* @param bucketName
* @param fileNames
*/
@SneakyThrows(Exception.class)
public void removeFiles(String bucketName, List<String> fileNames) {
Stream<DeleteObject> stream = fileNames.stream().map(DeleteObject::new);
minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(stream::iterator).build());
}
/**
* 获取minio文件的下载或者预览地址
* 取决于调用本方法的方法中的PutObjectOptions对象有没有设置contentType
*
* @param bucketName: 桶名
* @param fileName: 文件名
*/
@SneakyThrows(Exception.class)
public String getPreviewFileUrl(String bucketName, String fileName) {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(fileName)
.method(Method.GET).build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* 获取文件外链
*
* @param bucketName 存储桶
* @param fileName 文件名
* @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String fileName, Integer expires) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(fileName)
.expiry(expires)
.build()
);
}
/**
* 获取文件信息, 如果抛出异常则说明文件不存在
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @return
*/
@SneakyThrows(Exception.class)
public String getFileStatusInfo(String bucketName, String objectName) {
return minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()).toString();
}
/**
* 断点下载
*
* @param bucketName 存储桶
* @param objectName 文件名称
* @param offset 起始字节的位置
* @param length 要读取的长度
* @return 二进制流
*/
@SneakyThrows(Exception.class)
public InputStream 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 objectName 文件名称
* @return 二进制流
*/
@SneakyThrows(Exception.class)
public InputStream dowload(String bucketName, String objectName) {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
}
Minio对象存储服务接口调用
package com.example.minio.controller;
import com.example.minio.common.config.ParamConfig;
import com.example.minio.common.utils.MinIoUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
/**
* @Author: HOUJL
* @Date: 2023/2/14
* @Description:
*/
@RestController
@RequestMapping("/minio")
@Api(value = "Minio对象存储服务", tags = "Minio对象存储服务")
public class MinioController {
private final static Logger LOGGER = LoggerFactory.getLogger(MinioController.class);
public final MinIoUtil minIoUtil;
private final ParamConfig paramConfig;
public MinioController(MinIoUtil minIoUtil, ParamConfig paramConfig) {
this.minIoUtil = minIoUtil;
this.paramConfig = paramConfig;
}
@ApiOperation(value = "文件上传")
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
try {
String fileUrl = minIoUtil.uploadFile(file);
return fileUrl;
} catch (Exception e) {
LOGGER.error("上传失败");
return "上传失败";
}
}
@ApiOperation(value = "删除文件")
@DeleteMapping("/delete")
public void delete(@RequestParam("fileName") String fileName) {
minIoUtil.deleteFile(paramConfig.getBucketName(), fileName);
}
@ApiOperation(value = "获取文件详情")
@GetMapping("/info")
public String getFileStatusInfo(@RequestParam("fileName") String fileName) {
return minIoUtil.getFileStatusInfo(paramConfig.getBucketName(), fileName);
}
@ApiOperation(value = "获取文件外链")
@GetMapping("/url")
public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
return minIoUtil.getPresignedObjectUrl(paramConfig.getBucketName(), fileName, 7);
}
@ApiOperation(value = "文件下载")
@GetMapping("/download")
public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
try {
InputStream fileInputStream = minIoUtil.dowload(paramConfig.getBucketName(), fileName);
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setContentType("application/force-download");
response.setCharacterEncoding("UTF-8");
IOUtils.copy(fileInputStream, response.getOutputStream());
} catch (Exception e) {
LOGGER.error("下载失败");
}
}
}
SpringBoot 分片上传、断点续传、秒传、直传Minio
参考
本文为CSDN博主「单人可khalil」的原创文章。
原文链接:https://blog.csdn.net/m0_46493080/article/details/128303690
大致的流程如下:
前端获取文件MD5,发送至后台判断是否有该文件,有则直接转存;
前端调用初始化接口,后端调用 minio 初始化,返回分片上传地址和 uploadId;
前端上传分片文件;
上传完成后,前端发送请求至后台服务,后台服务调用 minio 合并文件;
前端代码地址:https://github.com/lanweihong/vue-minio-upload-sample
流程图如下:
实现过程
基本配置见Spring Boot集成Minio
获取ParamConfig默认属性
package com.example.minio.common.config;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: HOUJL
* @Date: 2023/2/21
* @Description:
*/
@Configuration
@EnableConfigurationProperties(ParamConfig.class)
@Slf4j
public class MinioConfig {
private ParamConfig paramConfig;
@Autowired
public void setMinioPropertiesConfig(ParamConfig paramConfig) {
this.paramConfig = paramConfig;
}
@Bean
public CustomMinioClient customMinioClient() {
MinioClient minioClient;
try {
minioClient = MinioClient.builder()
.endpoint(paramConfig.getEndpoint())
.credentials(paramConfig.getAccessKey(), paramConfig.getSecretKey())
.build();
} catch (Exception e) {
log.error("初始化 Minio 客户端失败:" + e.getMessage());
throw e;
}
return new CustomMinioClient(minioClient);
}
}
自定义Minio继承 MinioClient
package com.example.minio.common.config;
import com.google.common.collect.Multimap;
import io.minio.CreateMultipartUploadResponse;
import io.minio.ListPartsResponse;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.errors.*;
import io.minio.messages.Part;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @Author: HOUJL
* @Date: 2023/2/21
* @Description: SpringBoot 分片上传、断点续传、秒传、直传Minio
* 自定义Minio继承 MinioClient
*/
public class CustomMinioClient extends MinioClient {
protected CustomMinioClient(MinioClient client) {
super(client);
}
/**
* 初始化分片上传、获取 uploadId
*
* @param bucketName bucketName 存储桶名称
* @param region region
* @param objectName objectName 文件名称
* @param headers headers 请求头
* @param extraQueryParams extraQueryParams
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InvalidKeyException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws InternalException
*/
public String getUploadId(String bucketName, String region, String objectName,
Multimap<String, String> headers, Multimap<String, String> extraQueryParams)
throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
CreateMultipartUploadResponse response = this.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
return response.result().uploadId();
}
/**
* 合并分片
*
* @param bucketName 桶名称
* @param region
* @param objectName 文件名称
* @param uploadId 上传的 uploadId
* @param parts 分片集合
* @param extraHeaders
* @param extraQueryParams
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InvalidKeyException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws InternalException
*/
public ObjectWriteResponse mergeMultipart(String bucketName, String region, String objectName, String uploadId,
Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams)
throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
}
/**
* 查询当前上传后的分片信息
*
* @param bucketName 桶名称
* @param region
* @param objectName 文件名称
* @param maxParts 分片数量
* @param partNumberMaker 分片起始值
* @param uploadId 上传的 uploadId
* @param extraHeaders
* @param extraQueryParams
* @return
* @throws ServerException
* @throws InsufficientDataException
* @throws ErrorResponseException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws InvalidKeyException
* @throws XmlParserException
* @throws InvalidResponseException
* @throws InternalException
*/
public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMaker,
String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
return this.listParts(bucketName, region, objectName, maxParts, partNumberMaker, uploadId, extraHeaders, extraQueryParams);
}
}
minio、上传、合并分片的核心方法
package com.example.minio.common.helper;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.example.minio.common.config.CustomMinioClient;
import com.example.minio.common.config.ParamConfig;
import com.example.minio.common.exception.BusinessException;
import com.example.minio.dto.MinioUploadInfo;
import com.google.common.collect.HashMultimap;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListPartsResponse;
import io.minio.ObjectWriteResponse;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Component
@Slf4j
public class MinioHelper {
private final ParamConfig paramConfig;
private final CustomMinioClient customMinioClient;
@Autowired
public MinioHelper(ParamConfig paramConfig, CustomMinioClient customMinioClient) {
this.paramConfig = paramConfig;
this.customMinioClient = customMinioClient;
}
/**
* 初始化获取 uploadId
*
* @param objectName 文件名
* @param partCount 分片总数
* @param contentType contentType
* @return
*/
public MinioUploadInfo initMultiPartUpload(String objectName, int partCount, String contentType) {
HashMultimap<String, String> headers = HashMultimap.create();
headers.put("Content-Type", contentType);
String uploadId = "";
List<String> partUrlList = new ArrayList<>();
try {
// 获取 uploadId
uploadId = customMinioClient.getUploadId(paramConfig.getBucketName(),
null,
objectName,
headers,
null);
Map<String, String> paramsMap = new HashMap<>(2);
paramsMap.put("uploadId", uploadId);
for (int i = 1; i <= partCount; i++) {
paramsMap.put("partNumber", String.valueOf(i));
// 获取上传 url
String uploadUrl = customMinioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
// 注意此处指定请求方法为 PUT,前端需对应,否则会报 `SignatureDoesNotMatch` 错误
.method(Method.PUT)
.bucket(paramConfig.getBucketName())
.object(objectName)
// 指定上传连接有效期
.expiry(paramConfig.getChunkUploadExpirySecond(), TimeUnit.SECONDS)
.extraQueryParams(paramsMap).build());
partUrlList.add(uploadUrl);
}
} catch (Exception e) {
log.error("initMultiPartUpload Error:" + e);
return null;
}
// 过期时间
LocalDateTime expireTime = LocalDateTimeUtil.offset(LocalDateTime.now(), paramConfig.getChunkUploadExpirySecond(), ChronoUnit.SECONDS);
MinioUploadInfo result = new MinioUploadInfo();
result.setUploadId(uploadId);
result.setExpiryTime(expireTime);
result.setUploadUrls(partUrlList);
return result;
}
/**
* 分片合并
*
* @param objectName 文件名
* @param uploadId uploadId
* @return
*/
public String mergeMultiPartUpload(String objectName, String uploadId) {
// todo 最大1000分片 这里好像可以改吧
Part[] parts = new Part[1000];
int partIndex = 0;
ListPartsResponse partsResponse = listUploadPartsBase(objectName, uploadId);
if (null == partsResponse) {
log.error("查询文件分片列表为空");
throw new BusinessException("分片列表为空");
}
for (Part partItem : partsResponse.result().partList()) {
parts[partIndex] = new Part(partIndex + 1, partItem.etag());
partIndex++;
}
ObjectWriteResponse objectWriteResponse;
try {
objectWriteResponse = customMinioClient.mergeMultipart(paramConfig.getBucketName(), null, objectName, uploadId, parts, null, null);
} catch (Exception e) {
log.error("分片合并失败:" + e);
throw new BusinessException("分片合并失败:" + e.getMessage());
}
if (null == objectWriteResponse) {
log.error("合并失败,合并结果为空");
throw new BusinessException("分片合并失败");
}
return objectWriteResponse.region();
}
/**
* 获取已上传的分片列表
*
* @param objectName 文件名
* @param uploadId uploadId
* @return
*/
public List<Integer> listUploadChunkList(String objectName, String uploadId) {
ListPartsResponse partsResponse = listUploadPartsBase(objectName, uploadId);
if (null == partsResponse) {
return Collections.emptyList();
}
return partsResponse.result().partList().stream().map(Part::partNumber).collect(Collectors.toList());
}
private ListPartsResponse listUploadPartsBase(String objectName, String uploadId) {
int maxParts = 1000;
ListPartsResponse partsResponse;
try {
partsResponse = customMinioClient.listMultipart(paramConfig.getBucketName(), null, objectName, maxParts, 0, uploadId, null, null);
} catch (ServerException | InsufficientDataException | ErrorResponseException | NoSuchAlgorithmException | IOException | XmlParserException | InvalidKeyException | InternalException | InvalidResponseException e) {
log.error("查询文件分片列表错误:{},uploadId:{}", e, uploadId);
return null;
}
return partsResponse;
}
}
基本工具类
package com.example.minio.common.exception;
import com.example.minio.common.enums.MessageEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseErrorException extends RuntimeException {
private static final long serialVersionUID = 6386720492655133851L;
private int code;
private String error;
public BaseErrorException(MessageEnum messageEnum) {
this.code = messageEnum.getCode();
this.error = messageEnum.getMessage();
}
}
package com.example.minio.common.exception;
import com.example.minio.common.enums.MessageEnum;
import lombok.Data;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Data
public class BusinessException extends BaseErrorException {
private static final long serialVersionUID = 2369773524406947262L;
public BusinessException(MessageEnum messageEnum) {
super(messageEnum);
}
public BusinessException(String error) {
super.setCode(-1);
super.setError(error);
}
}
package com.example.minio.common.utils;
import org.springframework.beans.BeanUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
public class HongBeanUtils {
public static <DTO> List<DTO> doListToDtoList(List<?> doList, Class<DTO> dtoClass) {
return pojoListConvert(doList, dtoClass);
}
public static <VO> List<VO> dtoListToVoList(List<?> dtoList, Class<VO> voClass) {
return pojoListConvert(dtoList, voClass);
}
public static <DTO> DTO doToDto(Object doEntity, Class<DTO> clazz) {
return doToDto(doEntity, clazz, "");
}
public static <DTO> DTO doToDto(Object doEntity, Class<DTO> clazz, String... ignoreProperties) {
if (null == doEntity || null == clazz) {
return null;
}
return pojoConvert(doEntity, clazz, ignoreProperties);
}
public static <VO> VO dtoToVo(Object dtoEntity, Class<VO> voClass, String... ignoreProperties) {
if (null == dtoEntity || null == voClass) {
return null;
}
return pojoConvert(dtoEntity, voClass, ignoreProperties);
}
public static <T> T copy(Object source, Class<T> targetClass) {
return pojoConvert(source, targetClass);
}
private static <T> List<T> pojoListConvert(List<?> sourceList, Class<T> targetClass) {
if (sourceList.size() == 0 || null == targetClass) {
return Collections.emptyList();
}
return sourceList.stream().map(item -> pojoConvert(item, targetClass)).collect(Collectors.toList());
}
private static <T> T pojoConvert(Object sourceEntity, Class<T> targetClass, String... ignoreProperties) {
T targetInstance;
try {
targetInstance = targetClass.newInstance();
} catch (IllegalAccessException | InstantiationException exception) {
return null;
}
if (ignoreProperties.length > 0) {
BeanUtils.copyProperties(sourceEntity, targetInstance, ignoreProperties);
} else {
BeanUtils.copyProperties(sourceEntity, targetInstance);
}
return targetInstance;
}
}
package com.example.minio.common.enums;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
public class CommonEnums {
public enum MinioFileStatusEnum {
/**
* 待上传
* 已上传
* 上传中
*/
UN_UPLOADED,
UPLOADED,
UPLOADING
}
}
package com.example.minio.common.enums;
import lombok.Getter;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description: 消息枚举
*/
public enum MessageEnum {
/**
* 消息枚举
*/
FAIL(-1, "操作失败"),
SUCCESS(200, "操作成功"),
RECORD_NOT_EXISTED(1001, "记录不存在"),
PARAM_NOT_NULL(1002, "参数不能为空"),
PARAM_INVALID(1003, "参数错误"),
UPLOAD_FILE_NOT_NULL(1004, "上传文件不能为空");
MessageEnum(int value, String text) {
this.code = value;
this.message = text;
}
@Getter
private final int code;
@Getter
private final String message;
public static MessageEnum valueOf(int value) {
MessageEnum[] enums = values();
for (MessageEnum enumItem : enums) {
if (value == enumItem.getCode()) {
return enumItem;
}
}
return null;
}
}
Minio文件切片上传接口调用
package com.example.minio.controller;
import cn.hutool.json.JSONObject;
import com.alibaba.druid.util.StringUtils;
import com.example.minio.dto.MinioOperationResult;
import com.example.minio.dto.MinioUploadInfo;
import com.example.minio.param.GetMinioUploadInfoParam;
import com.example.minio.param.MergeMinioMultipartParam;
import com.example.minio.service.IFileUploadInfoService;
import com.example.minio.vo.JsonResult;
import io.swagger.annotations.Api;
import org.simpleframework.xml.core.Validate;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@RestController
@RequestMapping("/file-upload")
@Api(value = "Minio文件切片上传", tags = "Minio文件切片上传")
public class SliceUploadController {
private final IFileUploadInfoService fileService;
public SliceUploadController(IFileUploadInfoService fileService) {
this.fileService = fileService;
}
/**
* 获取上传 url
*
* @param param 参数
* @return
*/
@PostMapping("/upload")
public JsonResult<MinioUploadInfo> getUploadId(@Validate @RequestBody GetMinioUploadInfoParam param) {
MinioUploadInfo minioUploadId = fileService.getUploadId(param);
return JsonResult.ok(minioUploadId);
}
/**
* 校验文件是否存在
*
* @param md5 文件 md5
* @return
*/
@GetMapping("/upload/check")
public JsonResult<MinioOperationResult> checkFileUploadedByMd5(@RequestParam("md5") String md5) {
MinioOperationResult result = fileService.checkFileExistsByMd5(md5);
return JsonResult.ok(result);
}
/**
* 合并文件
*
* @param param
* @return
*/
@PostMapping("/upload/merge")
public JsonResult<JSONObject> mergeUploadFile(@Valid MergeMinioMultipartParam param) {
String result = fileService.mergeMultipartUpload(param);
if (StringUtils.isEmpty(result)) {
return JsonResult.error("合并失败");
}
JSONObject object = new JSONObject();
object.set("url", result);
return JsonResult.ok(object);
}
}
package com.example.minio.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.minio.dto.MinioOperationResult;
import com.example.minio.dto.MinioUploadInfo;
import com.example.minio.entity.FileUploadInfo;
import com.example.minio.param.GetMinioUploadInfoParam;
import com.example.minio.param.MergeMinioMultipartParam;
import java.util.List;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
public interface IFileUploadInfoService extends IService<FileUploadInfo> {
/**
* 获取分片上传信息
*
* @param param 参数
* @return
*/
MinioUploadInfo getUploadId(GetMinioUploadInfoParam param);
/**
* 检查文件是否存在
*
* @param md5 md5
* @return
*/
MinioOperationResult checkFileExistsByMd5(String md5);
/**
* 查询已上传的分片序号
*
* @param objectName 文件名
* @param uploadId uploadId
* @return
*/
List<Integer> listUploadParts(String objectName, String uploadId);
/**
* 分片合并
*
* @param param 参数
* @return
*/
String mergeMultipartUpload(MergeMinioMultipartParam param);
}
package com.example.minio.service.impl;
import com.alibaba.druid.util.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.minio.common.enums.CommonEnums;
import com.example.minio.common.helper.MinioHelper;
import com.example.minio.dao.FileUploadInfoMapper;
import com.example.minio.dto.MinioFileChunkUploadInfoDTO;
import com.example.minio.dto.MinioFileUploadInfoDTO;
import com.example.minio.dto.MinioOperationResult;
import com.example.minio.dto.MinioUploadInfo;
import com.example.minio.entity.FileUploadInfo;
import com.example.minio.param.GetMinioUploadInfoParam;
import com.example.minio.param.MergeMinioMultipartParam;
import com.example.minio.param.MinioFileChunkUploadInfoParam;
import com.example.minio.param.MinioFileUploadInfoParam;
import com.example.minio.service.IFileUploadInfoService;
import com.example.minio.service.IMinioFileChunkUploadInfoService;
import com.example.minio.service.IMinioFileUploadInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Service
@Slf4j
public class FileUploadInfoServiceImpl extends ServiceImpl<FileUploadInfoMapper, FileUploadInfo> implements IFileUploadInfoService {
private final MinioHelper minioHelper;
private final IMinioFileUploadInfoService minioFileUploadInfoService;
private final IMinioFileChunkUploadInfoService minioFileChunkUploadInfoService;
public FileUploadInfoServiceImpl(MinioHelper minioHelper, IMinioFileUploadInfoService minioFileUploadInfoService, IMinioFileChunkUploadInfoService minioFileChunkUploadInfoService) {
this.minioHelper = minioHelper;
this.minioFileUploadInfoService = minioFileUploadInfoService;
this.minioFileChunkUploadInfoService = minioFileChunkUploadInfoService;
}
@Override
public MinioUploadInfo getUploadId(GetMinioUploadInfoParam param) {
MinioUploadInfo uploadInfo;
MinioFileUploadInfoDTO minioFileUploadInfo = this.minioFileUploadInfoService.getByFileMd5(param.getFileMd5());
if (null == minioFileUploadInfo) {
// 计算分片数量
double partCount = Math.ceil(param.getFileSize() / param.getChunkSize());
log.info("总分片数:" + partCount);
uploadInfo = minioHelper.initMultiPartUpload(param.getFileName(), (int) partCount, param.getContentType());
if (null != uploadInfo) {
MinioFileUploadInfoParam saveParam = new MinioFileUploadInfoParam();
saveParam.setUploadId(uploadInfo.getUploadId());
saveParam.setFileMd5(param.getFileMd5());
saveParam.setFileName(param.getFileName());
saveParam.setTotalChunk((int) partCount);
saveParam.setFileStatus(CommonEnums.MinioFileStatusEnum.UN_UPLOADED.ordinal());
// 保存文件上传信息
minioFileUploadInfoService.saveMinioFileUploadInfo(saveParam);
MinioFileChunkUploadInfoParam chunkUploadInfoParam = new MinioFileChunkUploadInfoParam();
chunkUploadInfoParam.setUploadUrls(uploadInfo.getUploadUrls());
chunkUploadInfoParam.setUploadId(uploadInfo.getUploadId());
chunkUploadInfoParam.setExpiryTime(uploadInfo.getExpiryTime());
chunkUploadInfoParam.setFileMd5(param.getFileMd5());
chunkUploadInfoParam.setFileName(param.getFileName());
// 保存分片上传信息
minioFileChunkUploadInfoService.saveMinioFileChunkUploadInfo(chunkUploadInfoParam);
}
return uploadInfo;
}
// 查询分片上传地址
List<MinioFileChunkUploadInfoDTO> list = minioFileChunkUploadInfoService.listByFileMd5AndUploadId(minioFileUploadInfo.getFileMd5(), minioFileUploadInfo.getUploadId());
List<String> uploadUrlList = list.stream().map(MinioFileChunkUploadInfoDTO::getChunkUploadUrl).collect(Collectors.toList());
uploadInfo = new MinioUploadInfo();
uploadInfo.setUploadUrls(uploadUrlList);
uploadInfo.setUploadId(minioFileUploadInfo.getUploadId());
return uploadInfo;
}
@Override
public MinioOperationResult checkFileExistsByMd5(String md5) {
MinioOperationResult result = new MinioOperationResult();
MinioFileUploadInfoDTO minioFileUploadInfo = this.minioFileUploadInfoService.getByFileMd5(md5);
if (null == minioFileUploadInfo) {
result.setStatus(CommonEnums.MinioFileStatusEnum.UN_UPLOADED.ordinal());
return result;
}
// 已上传
if (minioFileUploadInfo.getFileStatus() == CommonEnums.MinioFileStatusEnum.UPLOADED.ordinal()) {
result.setStatus(CommonEnums.MinioFileStatusEnum.UPLOADED.ordinal());
result.setUrl(minioFileUploadInfo.getFileUrl());
return result;
}
// 查询已上传分片列表并返回已上传列表
List<Integer> chunkUploadedList = listUploadParts(minioFileUploadInfo.getFileName(), minioFileUploadInfo.getUploadId());
result.setStatus(CommonEnums.MinioFileStatusEnum.UPLOADING.ordinal());
result.setChunkUploadedList(chunkUploadedList);
return result;
}
@Override
public List<Integer> listUploadParts(String objectName, String uploadId) {
return minioHelper.listUploadChunkList(objectName, uploadId);
}
@Override
public String mergeMultipartUpload(MergeMinioMultipartParam param) {
String result = minioHelper.mergeMultiPartUpload(param.getFileName(), param.getUploadId());
if (!StringUtils.isEmpty(result)) {
MinioFileUploadInfoParam fileUploadInfoParam = new MinioFileUploadInfoParam();
fileUploadInfoParam.setFileUrl(result);
fileUploadInfoParam.setFileMd5(param.getMd5());
fileUploadInfoParam.setFileStatus(CommonEnums.MinioFileStatusEnum.UPLOADED.ordinal());
// 更新状态
minioFileUploadInfoService.updateFileStatusByFileMd5(fileUploadInfoParam);
}
return result;
}
}
IMinioFileUploadInfoService和MinioFileUploadInfoServiceImpl
package com.example.minio.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.minio.dto.MinioFileUploadInfoDTO;
import com.example.minio.entity.FileUploadInfo;
import com.example.minio.param.MinioFileUploadInfoParam;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
public interface IMinioFileUploadInfoService extends IService<FileUploadInfo> {
/**
* 保存
*
* @param param 参数对象
* @return
*/
MinioFileUploadInfoDTO saveMinioFileUploadInfo(MinioFileUploadInfoParam param);
/**
* 修改文件状态
*
* @param param 参数对象
* @return
*/
int updateFileStatusByFileMd5(MinioFileUploadInfoParam param);
/**
* 根据文件 md5 查询
*
* @param fileMd5 文件 md5
* @return
*/
MinioFileUploadInfoDTO getByFileMd5(String fileMd5);
}
package com.example.minio.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.minio.common.enums.MessageEnum;
import com.example.minio.common.exception.BusinessException;
import com.example.minio.common.utils.HongBeanUtils;
import com.example.minio.dao.IMinioFileUploadInfoMapper;
import com.example.minio.dto.MinioFileUploadInfoDTO;
import com.example.minio.entity.FileUploadInfo;
import com.example.minio.param.MinioFileUploadInfoParam;
import com.example.minio.service.IMinioFileUploadInfoService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Service
public class MinioFileUploadInfoServiceImpl extends ServiceImpl<IMinioFileUploadInfoMapper, FileUploadInfo> implements IMinioFileUploadInfoService {
private final IMinioFileUploadInfoMapper minioFileUploadInfoMapper;
public MinioFileUploadInfoServiceImpl(IMinioFileUploadInfoMapper minioFileUploadInfoMapper) {
this.minioFileUploadInfoMapper = minioFileUploadInfoMapper;
}
@Override
public MinioFileUploadInfoDTO saveMinioFileUploadInfo(MinioFileUploadInfoParam param) {
FileUploadInfo minioFileUploadInfo = null;
if (null == param.getId()) {
minioFileUploadInfo = new FileUploadInfo();
} else {
minioFileUploadInfo = this.minioFileUploadInfoMapper.selectById(param.getId());
if (null == minioFileUploadInfo) {
throw new BusinessException(MessageEnum.RECORD_NOT_EXISTED);
}
minioFileUploadInfo.setUpdateTime(new Date());
}
BeanUtils.copyProperties(param, minioFileUploadInfo, "id");
int result;
if (null == param.getId()) {
result = this.minioFileUploadInfoMapper.insert(minioFileUploadInfo);
} else {
result = this.minioFileUploadInfoMapper.updateById(minioFileUploadInfo);
}
if (result == 0) {
throw new BusinessException(MessageEnum.FAIL);
}
return HongBeanUtils.doToDto(minioFileUploadInfo, MinioFileUploadInfoDTO.class);
}
@Override
public int updateFileStatusByFileMd5(MinioFileUploadInfoParam param) {
FileUploadInfo minioFileUploadInfo = this.minioFileUploadInfoMapper.getByFileMd5(param.getFileMd5());
if (null == minioFileUploadInfo) {
throw new BusinessException(MessageEnum.RECORD_NOT_EXISTED);
}
minioFileUploadInfo.setFileStatus(param.getFileStatus());
minioFileUploadInfo.setFileUrl(param.getFileUrl());
return this.minioFileUploadInfoMapper.updateById(minioFileUploadInfo);
}
@Override
public MinioFileUploadInfoDTO getByFileMd5(String fileMd5) {
FileUploadInfo minioFileUploadInfo = this.minioFileUploadInfoMapper.getByFileMd5(fileMd5);
if (null == minioFileUploadInfo) {
return null;
}
return HongBeanUtils.doToDto(minioFileUploadInfo, MinioFileUploadInfoDTO.class);
}
}
IMinioFileChunkUploadInfoService和MinioFileChunkUploadInfoServiceImpl
package com.example.minio.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.minio.dto.MinioFileChunkUploadInfoDTO;
import com.example.minio.entity.MinioFileChunkUploadInfoDO;
import com.example.minio.param.MinioFileChunkUploadInfoParam;
import java.util.List;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
public interface IMinioFileChunkUploadInfoService extends IService<MinioFileChunkUploadInfoDO> {
/**
* 保存
*
* @param param
* @return
*/
boolean saveMinioFileChunkUploadInfo(MinioFileChunkUploadInfoParam param);
List<MinioFileChunkUploadInfoDO> listByFileMd5(String fileMd5);
List<MinioFileChunkUploadInfoDTO> listByFileMd5AndUploadId(String fileMd5, String uploadId);
}
package com.example.minio.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.minio.common.utils.HongBeanUtils;
import com.example.minio.dao.IMinioFileChunkUploadInfoMapper;
import com.example.minio.dto.MinioFileChunkUploadInfoDTO;
import com.example.minio.entity.MinioFileChunkUploadInfoDO;
import com.example.minio.param.MinioFileChunkUploadInfoParam;
import com.example.minio.service.IMinioFileChunkUploadInfoService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: HOUJL
* @Date: 2023/2/22
* @Description:
*/
@Service
public class MinioFileChunkUploadInfoServiceImpl extends ServiceImpl<IMinioFileChunkUploadInfoMapper, MinioFileChunkUploadInfoDO> implements IMinioFileChunkUploadInfoService {
private final IMinioFileChunkUploadInfoMapper iMinioFileChunkUploadInfoMapper;
public MinioFileChunkUploadInfoServiceImpl(IMinioFileChunkUploadInfoMapper iMinioFileChunkUploadInfoMapper) {
this.iMinioFileChunkUploadInfoMapper = iMinioFileChunkUploadInfoMapper;
}
@Override
public boolean saveMinioFileChunkUploadInfo(MinioFileChunkUploadInfoParam param) {
List<MinioFileChunkUploadInfoDO> list = new ArrayList<>();
for (int i = 0; i < param.getUploadUrls().size(); i++) {
MinioFileChunkUploadInfoDO tempObj = new MinioFileChunkUploadInfoDO();
tempObj.setChunkNumber(i + 1);
tempObj.setFileMd5(param.getFileMd5());
tempObj.setUploadId(param.getUploadId());
tempObj.setExpiryTime(param.getExpiryTime());
tempObj.setChunkUploadUrl(param.getUploadUrls().get(i));
list.add(tempObj);
}
int result = this.iMinioFileChunkUploadInfoMapper.batchInsert(list);
return result != 0;
}
@Override
public List<MinioFileChunkUploadInfoDO> listByFileMd5(String fileMd5) {
return iMinioFileChunkUploadInfoMapper.listByFileMd5(fileMd5);
}
@Override
public List<MinioFileChunkUploadInfoDTO> listByFileMd5AndUploadId(String fileMd5, String uploadId) {
List<MinioFileChunkUploadInfoDO> list = iMinioFileChunkUploadInfoMapper.listByFileMd5AndUploadId(fileMd5, uploadId);
return HongBeanUtils.doListToDtoList(list, MinioFileChunkUploadInfoDTO.class);
}
}
具体实体和vo/dto/param见资源详细代码
参考
https://blog.csdn.net/lemon_TT/article/details/124675675
https://blog.csdn.net/lemon_TT/article/details/127090238
https://blog.csdn.net/qq_43437874/category_10562215.html?spm=1001.2014.3001.5482
https://blog.csdn.net/m0_46493080/article/details/128303690?spm=1001.2014.3001.5502
https://min.io/download 中文开发文档:http://docs.minio.org.cn/docs/