概述
MinIO为对象存储服务,用来存储大容量非结构化的数据,用于分布式文件存储。与云存储不同,云储存是将数据存储在云服务器上,用户通过互联网即可获取;分布式文件存储是将文件保存在集群的磁盘上,使用网络的方式通过地址来获取。
创建服务
MinIO官网
下载服务,推荐使用Docker或Linux,此次只做简单使用,采用Windows即可,MINIO CLIENT为客户端,暂且不使用。
下载完成后,存放在bin目录下,在bin同级创建data文件夹,用来存储数据信息。
注意:直接双击.exe文件并不能启动,在bin目录下使用cmd打开,每次启动服务需注册账号和密码
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
然后通过:minio.exe server D:\Environment_Construction\MinIO\data
注意后面路径为你的data文件路径,后面可设置端口号,不指定时默认为9000
完整为:minio.exe server D:\Environment_Construction\MinIO\data --console-address "127.0.0.1:9000" --address "127.0.0.1:9005"
可以看到有两个端口号,上方为服务器地址,下方Web端管理后台地址,其中127.0.0.1为本机,无网络也可使用,其他为网络分配IP地址,都可使用。
在浏览器中输入该网址即可查看,然后输入刚才设置的账号和密码即可登录。注:服务断开后该账号失效。
登陆后,初次没有桶和数据,需添加一个默认的桶。桶:类似于文件夹,但级别更高,他是用来存放数据的。
桶不能重复,创建后即可在本地磁盘查看到该信息,所以本质上依然是硬盘介质。
在后台可上传文件,同样在磁盘中可查看到该信息,只不过已经经过序列化处理。
创建服务和Web端的了解先到此。
整合SpringBoot
在创建之前,需先给MinIO服务创建密钥,以便于访问,点击后会自动生成密钥,可自定义修改。
在下载MinIO服务界面下方可看到MINIO SDK中的Java可获取依赖信息。
在springboot项目中添加依赖
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
<!--zip打包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.7</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置application.yml信息,其中bucketName不能为空
server:
port: 8080
spring:
# 配置文件上传大小限制
servlet:
multipart:
max-file-size: 200MB
max-request-size: 200MB
#MinIO配置
minio:
endpoint: http://127.0.0.1:9000
accessKey: wanglang
secretKey: wanglang
bucketName: create-default
在config文件夹下创建MinioConfig.java进行配置服务:
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/**
* 访问api Url
*/
@Value("${minio.endpoint}")
private String endpoint;
/**
* 访问密钥
*/
@Value("${minio.accessKey}")
private String accessKey;
/**
* 密钥
*/
@Value("${minio.secretKey}")
private String secretKey;
/**
* 捅名称
*/
@Value("${minio.bucketName}")
private String bucketName;
/**
* 创建Minio客户端
* @return MinioClient
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
在utils包下面编写统一返回格式类R.java:
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 通用返回结果类,服务器响应的数据将被封装成此对象
* @param <T>
*/
@Data
@Accessors(chain = true)
public class R<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg = "成功"; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
在entity包下面创建ObjectItem.java实体类,用做文件信息遍历接收:
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class ObjectItem {
private String objectName;
private String size;
@Override
public String toString() {
return "ObjectItem{" +
"名称='" + objectName + '\'' +
", 大小='" + size + '\'' +
'}';
}
}
现在在utils包下面编写自定义的MinioUtil.java工具类,MinIO提供了MinioClient.java,是对该文件的二次封装,其中未使用数据库存储文件相关信息,下载和删除主要使用前端传递的全路径识别,可根据实际情况修改,其他API暂未使用,可根据需要添加:
import cn.hutool.core.io.FastByteArrayOutputStream;
import com.wanglang.minioexercise.entity.ObjectItem;
import io.minio.*;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Encoder;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Component
public class MinioUtil {
@Autowired
private MinioClient minioClient;
/**
* 默认桶名
*/
@Value("${minio.bucketName}")
private String bucketName;
/**
* 判断bucket是否存在
* @param bucketName
*/
@SneakyThrows
public boolean existBucket(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建存储bucket
* @param bucketName
* @return boolean
*/
public boolean makeBucket(String bucketName) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 删除存储bucket
* @param bucketName
* @return boolean
*/
public boolean removeBucket(String bucketName) {
try {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 创建文件路径
* @param fileName 原始文件名称
* @return FilePath
*/
public String createFilePath(String fileName) {
return new SimpleDateFormat("yyyy/MM/dd").format(new Date()) + "/" + fileName;
}
/**
* 根据文件路径获取文件名称
* @param filePath 文件路径
* @return 文件名
*/
public String getFileNameByPath(String filePath) {
String[] split = StringUtils.split(filePath, "/");
return split[split.length - 1];
}
/**
* 查看桶内文件信息
* @param bucketName 可为空
* @return List
*/
@SneakyThrows
public List<ObjectItem> queryFileListInBucket(String bucketName) {
// 桶选择
String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
List<ObjectItem> objectItemList = new ArrayList<>();
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(thisBucketName).build());
for (Result<Item> result : results) {
Item item = result.get();
// 不为目录时添加
if (!item.isDir()) objectItemList.add(new ObjectItem().setObjectName(item.objectName()).setSize(String.valueOf(item.size())));
}
return objectItemList;
}
/**
* 判断桶内是否包含该文件
* @param fileName
* @param bucketName 可为空
* @return boolean
*/
public boolean isContainInBucketByFile(String fileName, String bucketName) {
List<ObjectItem> objectItemList = queryFileListInBucket(bucketName);
for (ObjectItem item : objectItemList) if (item.getObjectName().equals(fileName)) return true;
return false;
}
/**
* 单文件上传
* @param file
* @param bucketName 可为空
* @return String
*/
@SneakyThrows
public String upload(MultipartFile file, String bucketName) {
// 桶名
String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
// 文件名
String fileName = file.getOriginalFilename();
// 文件流
InputStream inputStream = file.getInputStream();
// 文件大小
long size = file.getSize();
// 文件路径
String filePath = createFilePath(fileName);
System.out.println(fileName + "的文件路径为:" + filePath);
// 存储到Minio
minioClient.putObject(PutObjectArgs.builder()
.bucket(thisBucketName)
.object(filePath)
.stream(inputStream, size, -1)
.contentType(file.getContentType())
.build());
return filePath;
}
/**
* 批量文件上传
* @param file 数组
* @param bucketName 可为空
* @return Map key-文件名 value-文件路径
*/
@SneakyThrows
public Map<String, String> upload(MultipartFile[] file, String bucketName) {
Map<String, String> filePaths = new HashMap<>();
String thisBucketName = bucketName == null || bucketName.equals("") ? this.bucketName : bucketName;
for (MultipartFile item : file) {
String fileName = item.getOriginalFilename();
InputStream inputStream = item.getInputStream();
long size = item.getSize();
String filePath = createFilePath(fileName);
filePaths.put(fileName, filePath);
System.out.println(fileName + "的文件路径为:" + filePath);
minioClient.putObject(PutObjectArgs.builder()
.bucket(thisBucketName)
.object(filePath)
.stream(inputStream, size, -1)
.contentType(item.getContentType())
.build());
}
return filePaths;
}
/**
* 单文件下载(因为存在多级目录,通过文件路径)
* @param response
* @param request
* @param filePath
*/
@SneakyThrows
public void download(HttpServletResponse response, HttpServletRequest request, String filePath) {
String fileName = getFileNameByPath(filePath);
// 实际情况的文件路径中不包含桶名,可从数据库中获取
String bucketName = StringUtils.split(filePath, "/")[0];
String thisFilePath = StringUtils.split(filePath, "/")[1];
// 设置响应头信息,告知浏览器下载文件
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + fileNameEncoding(fileName, request));
// 放行Content-Disposition标头,设置权限访问
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
GetObjectResponse getObjectResponse = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(thisFilePath)
.build());
int len = -1;
byte[] buffer = new byte[1024];
FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
while ((len = getObjectResponse.read(buffer)) != -1) outputStream.write(buffer, 0, len);
outputStream.flush();
byte[] bytes = outputStream.toByteArray();
ServletOutputStream stream = response.getOutputStream();
stream.write(bytes);
stream.flush();
}
/**
* 批量文件下载,打包成压缩包(通过文件路径)
* @param response
* @param request
* @param zipName 可为空
*/
@SneakyThrows
public void download(HttpServletResponse response, HttpServletRequest request, List<String> filePaths, String zipName) {
String thisZipName = zipName.equals("") ? "default" : zipName;
// 清除首部空白行
response.reset();
// 设置响应头信息,告知浏览器下载文件
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + fileNameEncoding(thisZipName, request));
// 放行Content-Disposition标头,设置权限访问
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
BufferedOutputStream bufferedOS = new BufferedOutputStream(response.getOutputStream());
ZipOutputStream zipOS = new ZipOutputStream(bufferedOS);
response.setHeader("Access-Control-Allow-Origin", "*");
for (String path : filePaths) {
String fileName = getFileNameByPath(path);
// 实际情况的文件路径中不包含桶名,可从数据库中获取
String bucketName = StringUtils.split(path, "/")[0];
String filePath = StringUtils.split(path, "/")[1];
InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(filePath)
.build());
byte[] buffer = new byte[1024];
int len = 0;
zipOS.putNextEntry(new ZipEntry(fileName));
while ((len = inputStream.read(buffer)) > 0) zipOS.write(buffer, 0, len);
}
bufferedOS.close();
zipOS.close();
}
/**
* 单文件删除(多级目录,使用文件路径)
* @param filePath
* @return boolean
*/
public boolean removeFile(String filePath) {
String bucketName = StringUtils.split(filePath, "/")[0];
String thisFilePath = StringUtils.split(filePath, "/")[1];
try {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(thisFilePath).build());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 批量文件删除
* @param filePaths
* @return Map
*/
public Map<String, String> removeFile(List<String> filePaths) {
Map<String,String > result = new HashMap<>();
for (String path : filePaths) {
String bucketName = StringUtils.split(path, "/")[0];
String fileName = StringUtils.split(path, "/")[1];
try {
// 这样Minio提供了removeObjects方法,配合,我这里因为桶可能不唯一,暂定
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
result.put(getFileNameByPath(path), "删除成功");
} catch (Exception e) {
e.printStackTrace();
result.put(getFileNameByPath(path), "网络异常,删除失败");
}
}
return result;
}
/**
* 设置不同浏览器编码
* @param fileName
* @param request
*/
@SneakyThrows
public static String fileNameEncoding(String fileName, HttpServletRequest request) {
// 获得请求头中的User-Agent
String agent = request.getHeader("User-Agent");
// 根据不同的客户端进行不同的编码
if (agent.contains("MSIE")) {
// IE浏览器
fileName = URLEncoder.encode(fileName, "utf-8");
} else if (agent.contains("Firefox")) {
// 火狐浏览器
BASE64Encoder base64Encoder = new BASE64Encoder();
fileName = "=?utf-8?B?" + base64Encoder.encode(fileName.getBytes("utf-8")) + "?=";
} else {
// 其它浏览器
fileName = URLEncoder.encode(fileName, "utf-8");
}
return fileName;
}
}
最后在controller包下编写MinioController.java进行测试:
import com.wanglang.minioexercise.utils.MinioUtil;
import com.wanglang.minioexercise.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/minio")
public class MinioController {
@Autowired
private MinioUtil minioUtil;
/**
* 创建桶
* @param bucketName
* @return Boolean
*/
@GetMapping("/createBucket/{bucketName}")
public R<Boolean> createBucket(@PathVariable String bucketName) {
if (minioUtil.existBucket(bucketName)) return R.error("已存在");
return minioUtil.makeBucket(bucketName) ? R.success(true) : R.error("创建失败");
}
/**
* 删除桶
* @param bucketName
* @return Boolean
*/
@GetMapping("/removeBucketByName/{bucketName}")
public R<Boolean> removeBucketByName(@PathVariable String bucketName) {
if (!minioUtil.existBucket(bucketName)) return R.error("未查到该桶");
return minioUtil.removeBucket(bucketName) ? R.success(true) : R.error("删除失败");
}
/**
* 单文件上传
* @param file
* @param bucketName
* @return String 文件路径
*/
@PostMapping("/uploadSingleFileByFileOrBucketName")
public R<String> uploadSingleFileByFileOrBucketName(@RequestParam MultipartFile file,
@RequestParam String bucketName) {
return (bucketName.equals("") || minioUtil.existBucket(bucketName))
? R.success(minioUtil.upload(file, bucketName))
: R.error("该桶不存在");
}
/**
* 批量文件上传
* @param files
* @param bucketName
* @return Map 文件文件路径集合
*/
@PostMapping("/uploadClusterFileByFileOrBucketName")
public R<Map<String, String>> uploadClusterFileByFileOrBucketName(@RequestParam MultipartFile[] files,
@RequestParam String bucketName) {
return (bucketName.equals("") || minioUtil.existBucket(bucketName))
? R.success(minioUtil.upload(files, bucketName))
: R.error("该桶不存在");
}
/**
* 单文件下载(因为存在多级目录,通过文件路径)
* @param filePath
* @param response
* @param request
* @return Boolean
*/
@PostMapping("/downloadSingleFileByFilePath")
public void downloadSingleFileByFilePath(@RequestParam String filePath,
HttpServletResponse response, HttpServletRequest request) {
minioUtil.download(response, request, filePath);
}
/**
* 批量文件下载(因为存在多级目录,通过文件路径)
* @param filePaths
* @param response 可为空
* @param response
* @param request
*/
@PostMapping("/downloadClusterFileByFilePaths")
public void downloadClusterFileByFilePaths(@RequestParam List<String> filePaths, @RequestParam String zipName,
HttpServletResponse response, HttpServletRequest request) {
minioUtil.download(response, request, filePaths, zipName);
}
/**
* 单文件删除(多级目录,使用文件路径)
* @param filePath
* @return Boolean
*/
@PostMapping("/removeSingleFileByFilePath")
public R<Boolean> removeSingleFileByFilePath(@RequestParam String filePath) {
return minioUtil.removeFile(filePath) ? R.success(true) : R.error("删除失败");
}
/**
* 文件批量删除(多级目录,使用文件路径)
* @param filePaths
* @return Map
*/
@PostMapping("/removeClusterFileByFilePaths")
public R<Map<String, String>> removeClusterFileByFilePaths(@RequestParam List<String> filePaths) {
return R.success(minioUtil.removeFile(filePaths));
}
}
到此整合结束。
测试
我这里使用的ApiPost7进行测试,提供一个批量上传示例:
在本地磁盘和Web端即可查看到变化。