你的文件还躺在硬盘里?试试MinIO,打造高性能私有云盘!
目录
一 入门
1 基础概念
网址:https://minio.org.cn/docs/minio/kubernetes/upstream/
对象存储(Object Storage)是一种数据存储架构,他以对象为单位来处理、存储和检索数据,每个对象都包含了数据本身以及元数据
MinIO存储的元数据主要包括对象的描述信息,如用户(account)、桶(bucket)以及存储桶索引(bucket index)等;
对象存储系统通常通过基于HTTP或者HTTPS协议的API(应用程序编程接口)进行数据读写。
2 环境部署
存储路径:/mnt/data/
minio路径:/usr/local/minio/
环境部署对应的网址:
GitHub: https://github.com/minio
Gitee: https://gitee.com/mirrors/minio
下载地址:https://www.min.io/download?platform=linux&arch=amd64


wget https://dl.min.io/server/minio/release/linux-amd64/minio
在本地linux系统当中去进行下载

文件下载结束之后需要给文件授予一个可执行权限
chmod +x minio

软件使用对应的启动服务器
查看如何去使用

对应的方法
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /mnt/data --console-address ":9001"
启动MinIO:
- 执行命令:
- MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /mnt/data --console-address ":9001"
- 或者:
- ./minio server /mnt/data --console-address ":9001"
- MINIO_ROOT_USER 是指定minio的用户名;
- MINIO_ROOT_PASSWORD 是指定minio的密码;
- /mnt/data 是指定MinIO服务器用于存储数据的目录;
- console-address ":9001" 指定 MinIO 控制台的监听地址和端口
展示:这里的内核版本优点低

现在去访问对应的客户端,可能会出现超时的情况,是因为防火墙关闭的原因。
也可以先看看防火墙是否开启
systemctl status firewalld
如果说其防火墙开启了可以临时将这个端口的防火墙给关闭
firewall-cmd --add-port=9001/tcp
本地访问成功


查看进程程序

使用Docker的方式进行启动部署(可以根据实际情况进行实现)
https://anxian.blog.csdn.net/article/details/148144715


Docker启动命令
docker run -d --name minio -p 9000:9000 -p 9001:9001 -e "MINIO_ROOT_USER=admin" -e "MINIO_ROOT_PASSWORD=password" -v /opt/minio/data:/mnt/data minio/minio server /mnt/data --console-address ":9001"
docker run -d \
--name minio \ # 容器名称(自定义)
-p 9000:9000 \ # 映射API端口(容器内9000 -> 宿主机9000)
-p 9001:9001 \ # 映射控制台端口(容器内9001 -> 宿主机9001)
-e "MINIO_ROOT_USER=admin" \ # 管理员账号(对应你的配置)
-e "MINIO_ROOT_PASSWORD=password" \ # 管理员密码(对应你的配置)
-v /opt/minio/data:/mnt/data \ # 挂载数据目录(宿主机/opt/minio/data -> 容器内/mnt/data,确保数据持久化)
minio/minio \ # 社区版镜像(无需许可证)
server /mnt/data --console-address ":9001" # 启动命令(指定数据目录和控制台地址,对应你的参数)
最终结果

界面也是一样可以访问

二 开发集成
1 首先在SpringBoot项目当中添加对应的依赖
https://mvnrepository.com/artifact/io.minio/minio/8.5.17
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
介绍一下两个基础概念:
Bucket:是存储Object的逻辑空间,每个Bucket之间的数据是相互隔离的,对用户而言,相当于存放文件的顶层文件夹。
Object:是存储到MinIO的基本对象,对用户而言,相当于文件。
2 常见API的使用
minio:
url: http://192.168.100.100:9000
access-key: admin
secret-key: password
servlet:
multipart:
max-file-size: 1000MB # 单个文件最大100MB
max-request-size: 1000MB # 整个请求最大100MB
enabled: true
package com.ax.demo123.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ax
*/
@Configuration
public class MinioConfig {
@Value("${minio.url}")
private String url;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
// 链式编程创建MinIO客户端
return MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.build();
}
}
对于存储桶常见API

putObject:用于上传已知大小的数据流,或者来自文件、字节数组等的数据。它要求指定对象的大小(除非使用-1表示未知大小,但通常建议已知大小)。它更通用,可以上传任何输入流。
uploadObject:专门用于上传大型文件,并且支持分片上传( multipart upload )。它通常用于大文件,因为分片上传可以提高大文件上传的可靠性和效率。使用
uploadObject时,MinIO客户端会自动将大文件分成多个部分进行上传。

示例代码展示:
@Autowired
private MinioClient minioClient;
/**
* 判断bucket是否存在,返回值为boolean
*/
@Test
void test1() throws Exception {
BucketExistsArgs args = BucketExistsArgs.builder().bucket("bu2").build();
boolean b = minioClient.bucketExists(args);
}
/**
* 创建bucket
*/
@Test
void test2() throws Exception {
MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket("bu2").build();
minioClient.makeBucket(makeArgs);
}
/**
* 列出所有bucket
*/
@Test
void test3() throws Exception {
List<Bucket> buckets = minioClient.listBuckets();
for (Bucket bucket : buckets) {
System.out.println(bucket.name() + "创建时间" + bucket.creationDate());
}
}
/**
* 删除bucket
*/
@Test
void test4() throws Exception {
RemoveBucketArgs removeArgs = RemoveBucketArgs.builder().bucket("bu2").build();
minioClient.removeBucket(removeArgs);
}
/**
* 在指定路径下上传文件
*/
@Test
void test5() throws Exception {
//日期路径
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
//1流式上传(适用于小文件)
File file1 = new File("D:\\excel\\axminio.txt");
//创建PutObjectArgs构造器
PutObjectArgs putArgs = PutObjectArgs.builder()
.bucket("bu1")// 桶名
.object("imagfile/" + datePath + "/testminio.txt") // 包含目录路径的文件名
.stream(new FileInputStream(file1), file1.length(), -1).build();// 文件流
//上传文件
minioClient.putObject(putArgs);
//2磁盘读取(适用于大文件分片上传)
File file2 = new File("D:\\excel\\图片1.jpg");
//UploadObjectArgs构造器
UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
.bucket("bu1")
.object("imagfile/" + datePath + "/testminio.jpg")// 包含目录路径的文件名
.filename(file2.getAbsolutePath())// 临时文件路径
.build();
//上传文件
minioClient.uploadObject(uploadArgs);
}
/**
* 获取文件访问路径:作用:生成一个有时限的临时访问链接,在有效期内可以直接访问文件,无需认证。
* 前端可以直接显示或者下载文件
*/
@Test
void test6() throws Exception {
// 生成签名串
String presignedUrl = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket("jarbucket")
.object("2025/11/15/mescloud-daemon-elastic-job.jar")
.expiry(5, TimeUnit.MINUTES)
.build());
System.out.println(presignedUrl);
// 这里存在访问策略,如果说存储桶是私有的,就需要生成对应的签名串拼接进行访问
// 但是共有的存储桶,那么这里不需要设置访问策略,可以直接使用路径直接访问
}
/**
* 下载文件:这里是读取到本地指定文件夹当中
*/
@Test
void test7() {
GetObjectArgs args = GetObjectArgs.builder()
.bucket("jarbucket")// 桶名
.object("2025/11/15/mescloud-daemon-elastic-job.jar")// 文件名
.build();
try (GetObjectResponse response = minioClient.getObject(args);
FileOutputStream outputStream = new FileOutputStream("D:\\excel\\mescloud-daemon-elastic-job.jar")) {
// 读取数据并写入文件
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = response.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 遍历路径文件(完整的文件名)
*/
@Test
void test8() throws Exception {
ListObjectsArgs args = ListObjectsArgs.builder()
.bucket("jarbucket")
.prefix("2025/11/15/")
.build();
Iterable<Result<Item>> results = minioClient.listObjects(args);
// 补充获取的文件名为完整的路径加文件名,可以借助这个调价进行文件夹批量下载操作,或者说文件夹内容查询操作
for (Result<Item> result : results) {
Item item = result.get();
String objectName = item.objectName();
System.out.println("名称:" + objectName + ",大小" + item.size());
// 下载到本地D://Excel目录
try {
// 构建本地文件路径
String fileName = objectName.substring(objectName.lastIndexOf("/") + 1);
String localPath = "D://excel/" + System.currentTimeMillis() + fileName;
// 下载文件
minioClient.downloadObject(
DownloadObjectArgs.builder()
.bucket("jarbucket")
.object(objectName)
.filename(localPath)
.build()
);
System.out.println("下载成功: " + localPath);
} catch (Exception e) {
System.err.println("下载失败: " + objectName + " - " + e.getMessage());
}
}
}
/**
* 删除路径文件
*/
@Test
void test9() throws Exception {
RemoveObjectArgs args = RemoveObjectArgs.builder()
.bucket("jarbucket")
.object("2025/11/15/mescloud-daemon-elastic-job.jar")
.build();
minioClient.removeObject(args);
System.out.println("删除成功");
}
对文件访问路径访问策略的补充:修改方式有后台修改与api修改

@RequestParam(“前端传递的参数名称”)
3 文件上传
Controller层代码,前提:已经提前创建好了名称为excel的桶
/**
* 上传Excel文件到Minio的bucket存储桶
*
* @return
*/
@PostMapping("/uploadFile")
public R uploadFile(@RequestParam MultipartFile file,String bucketName) {
return R.ok(importService.uploadFile(file,bucketName));
}
Service层
String uploadFile(MultipartFile file,String bucketName);
Service实现类
整体的一个需求设计:
首先,文件上传对文件进行非空判断,然后对存储桶的存在性进行判断,不存在就创建一个,设置存储桶内部的文件结构路径(这里是根据上传的日期实践进行设置),对路径下文件是否存在进行判断,存在则返回,防止进行覆盖。然后就需要进行判断文件大小采用不同的上传策略。
第一种(小于100MB的):传递文件,日期格式路径,文件名,大小,桶名
第二种(大于100MB的):传递文件,日期格式路径,桶名
第一种采用的是流式传输,其在stream()当中需要传入对于的1 文件输入流 2 对象总大小 3 分片大小(这里使用默认即可:-1)
第二种采用的是磁盘文件上传,其内部大致思路是有一个临时文件的存储,然后读取这个临时文件,适用于大文件,在其的Filename()当中就传入指定的路径即可
临时文件会创建在本地,但是后续会对其进行删除

代码展示:
/**
* 文件上传-到excel桶的日期规范之下
*
* @param file
* @param bucketName
* @return
*/
@Override
public String uploadFile(MultipartFile file, String bucketName) {
// 非空校验
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}
// 判断桶是否存在,不存在则创建一个
ensureBucketExists(bucketName);
// 对一些参数进行设置(日期路径,文件名称,大小,尺寸限制)
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String fileName = file.getOriginalFilename();
long size = file.getSize();
long limit = 1024 * 1024 * 100;
// 判断路径下文件是否存在
boolean exists = fileExists(bucketName, datePath + "/" + fileName);
if (exists) {
log.info("该文件已存在,请勿重复上传");
throw new RuntimeException("文件已经存在请勿重复上传。");
}
try {
// 判断文件大小选择上传方式
if (size < limit) {
return uploadSmallFile(file, datePath, fileName, size, bucketName);
} else {
return uploadLargeFile(file, datePath, fileName, bucketName);
}
} catch (Exception e) {
log.error("上传文件失败:文件名:" + fileName);
throw new RuntimeException("上传文件失败" + e.getMessage());
}
}
/**
* 文件上传 (<100MB) - 内存流式传输
*/
private String uploadSmallFile(MultipartFile file, String datePath, String fileName, long size, String bucketName) {
try (InputStream inputStream = file.getInputStream()) {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(bucketName)
.object(datePath + "/" + fileName)
.stream(inputStream, size, -1)
.build();
minioClient.putObject(args);
log.info("上传成功,文件存储在Minio的" + bucketName + "/datePath/" + fileName);
return "上传成功(小于100M方式)";
} catch (Exception e) {
log.error("小文件上传失败:文件名" + fileName);
throw new RuntimeException("小文件上传失败" + e.getMessage());
}
}
/**
* 文件上传 (≥100MB) - 磁盘文件上传
*/
private String uploadLargeFile(MultipartFile file, String datePath, String fileName, String bucketName) {
File tempFile = null;
try {
// 在本地的临时文件夹路径之下创建临时文件
tempFile = File.createTempFile("minio_upload_", "_" + fileName);
// 将MultipartFile转换为临时文件
file.transferTo(tempFile);
// 使用uploadObject上传大文件
UploadObjectArgs uploadArgs = UploadObjectArgs.builder()
.bucket(bucketName)
.object(datePath + "/" + fileName)
.filename(tempFile.getAbsolutePath())
.build();
minioClient.uploadObject(uploadArgs);
log.info("上传成功,文件存储在Minio的" + bucketName + "/" + datePath + "/" + fileName);
return "上传成功(大于100M方式)";
} catch (Exception e) {
log.error("大文件上传失败: {}", fileName);
throw new RuntimeException("大文件上传失败" + e.getMessage());
} finally {
// 确保临时文件被删除
if (tempFile != null && tempFile.exists()) {
try {
tempFile.delete();
} catch (Exception e) {
log.warn("删除临时文件失败: {}", tempFile.getAbsolutePath());
}
}
}
}
/**
* 使用 statObject 判断文件是否存在
*/
public boolean fileExists(String bucketName, String objectName) {
try {
StatObjectArgs statArgs = StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build();
StatObjectResponse response = minioClient.statObject(statArgs);
// 如果能执行到这里,说明文件存在
log.info("文件存在: {}/{}, 大小: {}, 最后修改: {}",
bucketName, objectName,
response.size(), response.lastModified());
return true;
} catch (ErrorResponseException e) {
// 明确捕获 MinIO 错误响应
String errorCode = e.errorResponse().code();
if ("NoSuchKey".equals(errorCode)) {
// 文件不存在是正常情况,返回 false
log.info("文件不存在: {}/{}", bucketName, objectName);
return false;
} else {
// 其他 MinIO 错误(如权限问题、存储桶不存在等)
log.info("MinIO错误响应: {}/{}, code={}, message={}",
bucketName, objectName, errorCode, e.getMessage());
throw new RuntimeException("文件检查失败: " + e.getMessage());
}
} catch (Exception e) {
// 其他异常(如网络问题、连接超时等)
log.info("检查文件存在性时发生异常: {}/{}", bucketName, objectName, e);
throw new RuntimeException("文件检查失败: " + e.getMessage());
}
}
/**
* 确保存储桶存在
*/
private void ensureBucketExists(String bucketName) {
try {
BucketExistsArgs existsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
if (!minioClient.bucketExists(existsArgs)) {
MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket(bucketName).build();
minioClient.makeBucket(makeArgs);
log.info("创建存储桶: {}", bucketName);
}
} catch (Exception e) {
log.error("存储桶操作失败: bucket={}", bucketName, e);
throw new RuntimeException("存储桶操作失败: " + e.getMessage());
}
}
结果展示:

控制台展示:

4 文件下载
传递参数存储桶名,存储的格式化路径,这里的存储是基于年月日yyyyMMdd的格式进行传递,还有文件名称以及后缀。对文件的桶以及路径进行存在性判断,然后获取对应的存储对象将其进行读取到本地。
controller
/**
* 获取Minio的指定bucketName存储桶中的文件
*
* @return
*/
@GetMapping("/downloadFile")
public R downloadFile(@RequestParam("bucketName") String bucketName, @RequestParam("objectName") String objectName, @RequestParam("fileName") String fileName) {
return R.ok(importService.downloadFile(bucketName, objectName, fileName));
}
Service
String downloadFile(String bucketName, String objectName, String fileName);
实现类
/**
* 文件下载(指定桶名,时间路径yyyyMMdd,文件名)
*/
@Override
public String downloadFile(String bucketName, String objectName, String fileName) {
// 生成完整的对象路径
objectName = timeTransfer(objectName) + "/" + fileName;
log.info("开始下载文件: {}/{}", bucketName, objectName);
if (!bucketExists(bucketName)) {
log.error("存储桶不存在: {}", bucketName);
return "路径错误无法找到文件";
} else {
if (!fileExists(bucketName, objectName)) {
log.error("文件不存在: {}/{}", bucketName, objectName);
return "路径错误,无法找到文件";
}
}
GetObjectArgs args = GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build();
try (GetObjectResponse response = minioClient.getObject(args);
FileOutputStream outputStream = new FileOutputStream("D:\\excel\\" + fileName)) {
// 读取数据并写入文件
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = response.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("下载成功: {}/{}", bucketName, objectName);
return "下载成功" + fileName;
}
/**
* 判断存储桶是否存在
*/
public boolean bucketExists(String bucketName) {
try {
BucketExistsArgs args = BucketExistsArgs.builder().bucket(bucketName).build();
return minioClient.bucketExists(args);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 时间转换yyyyMMdd转yyyy/MM/dd
*/
public String timeTransfer(String objectName) {
try {
return LocalDate.parse(objectName, DateTimeFormatter.BASIC_ISO_DATE)
.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
} catch (Exception e) {
throw new IllegalArgumentException("日期格式错误: " + objectName);
}
}
/**
* 使用 statObject 判断文件是否存在
*/
public boolean fileExists(String bucketName, String objectName) {
try {
StatObjectArgs statArgs = StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build();
StatObjectResponse response = minioClient.statObject(statArgs);
// 如果能执行到这里,说明文件存在
log.info("文件存在: {}/{}, 大小: {}, 最后修改: {}",
bucketName, objectName,
response.size(), response.lastModified());
return true;
} catch (ErrorResponseException e) {
// 明确捕获 MinIO 错误响应
String errorCode = e.errorResponse().code();
if ("NoSuchKey".equals(errorCode)) {
// 文件不存在是正常情况,返回 false
log.info("文件不存在: {}/{}", bucketName, objectName);
return false;
} else {
// 其他 MinIO 错误(如权限问题、存储桶不存在等)
log.info("MinIO错误响应: {}/{}, code={}, message={}",
bucketName, objectName, errorCode, e.getMessage());
throw new RuntimeException("文件检查失败: " + e.getMessage());
}
} catch (Exception e) {
// 其他异常(如网络问题、连接超时等)
log.info("检查文件存在性时发生异常: {}/{}", bucketName, objectName, e);
throw new RuntimeException("文件检查失败: " + e.getMessage());
}
}
更多推荐
所有评论(0)