Spring Boot 集成 MinIO 实现文件上传
一、 Minio 服务准备
MinIO的搭建过程参考 Docker 搭建 MinIO 对象存储。
登录MinIO控制台,新建一个 Bucket,修改 Bucket 权限为公开。
二、MinIO 集成
- 添加 MinIO 依赖
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
- 在项目配置文件
application.yml
中添加自定义配置。properties 文件自行转换
minio:
host: http://【服务器公网ip】:【minio运行端口号,默认9000】/
access-key: 账号
secret-key: 密码
- 创建配置文件类
@Data
@Component
public class MinioConfig {
@Value(value = "${minio.host}")
private String host;
@Value(value = "${minio.access-key}")
private String accessKey;
@Value(value = "${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient(){
return MinioClient.builder()
.endpoint(host)
.credentials(accessKey, secretKey)
.build();
}
}
- 创建文件上传工具类
@Component
@Slf4j
@AllArgsConstructor
public class MinioUtils {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
/**
* 初始化Bucket
*/
private void createBucket(String bucketName) {
// 设置公开读写
String POLICY_PATTERN = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::%s/*\"]}]}";
try {
// 判断 BucketName 是否存在
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(
String.format(POLICY_PATTERN, bucketName)
).build());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 验证bucketName是否存在
*
* @return boolean true:存在
*/
public boolean bucketExists(String bucketName) {
if (StringUtils.isBlank(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
boolean flag = true;
try {
flag = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
/**
* 获取全部bucket
* <p>
*/
public List<String> getAllBuckets() {
List<String> list = null;
try {
final List<Bucket> buckets = minioClient.listBuckets();
list = new ArrayList<>(buckets.size());
for (Bucket bucket : buckets) {
list.add(bucket.name());
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
/**
* 根据bucketName获取信息
*
* @param bucketName bucket名称
* @return
*/
public String getBucket(String bucketName) throws Exception {
final Optional<Bucket> first = minioClient.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
String name = null;
if (first.isPresent()) {
name = first.get().name();
}
return name;
}
/**
* 获取桶中文件名和大小列表
*
* @param bucketName bucket名称
* @param recursive 查询是否递归
* @return
*/
public List<Object> getFileList(String bucketName, boolean recursive) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
List<Object> items = new ArrayList<>();
try {
Iterable<Result<Item>> myObjects = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix("/2022-08-03/4674a894-abaf-48cb-9ea9-40a4e8560af9/Desktop").recursive(recursive).build());
Iterator<Result<Item>> iterator = myObjects.iterator();
String format = "{'fileName':'%s','fileSize':'%s'}";
for (Result<Item> myObject : myObjects) {
System.out.println(myObject.get().objectName());
}
while (iterator.hasNext()) {
Item item = iterator.next().get();
items.add(JSON.parse(String.format(format, item.objectName(), formatFileSize(item.size()))));
// items.add(JSON.parse(String.format(format, "/".concat("test").concat("/").concat(item.objectName()), formatFileSize(item.size()))));
}
} catch (Exception e) {
e.printStackTrace();
log.info(e.getMessage());
}
items.remove(0);
return items;
}
/**
* 文件上传
*
* @param bucketName 存储桶名称
* @param file file
* @return map
*/
public Map<String, Object> uploadFile(String bucketName, MultipartFile[] file) {
if (file == null || file.length == 0) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
createBucket(bucketName);
List<String> urlList = new ArrayList<>(file.length);
for (MultipartFile multipartFile : file) {
String originFileName = multipartFile.getOriginalFilename();
if (StringUtils.isBlank(originFileName)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
String[] originFileNameArr = originFileName.split("\\.");
String suffix = originFileNameArr[originFileNameArr.length - 1];
String newFileName = UUID.randomUUID().toString().replace("-", "").concat(".").concat(suffix);
urlList.add(String.format("%s%s/%s", minioConfig.getHost(), bucketName, newFileName));
try {
// 文件上传
InputStream in = multipartFile.getInputStream();
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(newFileName)
.stream(in, multipartFile.getSize(), -1)
.contentType(multipartFile.getContentType())
.build());
in.close();
} catch (Exception e) {
log.error(e.getMessage());
}
}
Map<String, Object> data = new HashMap<>();
data.put("bucketName", bucketName);
data.put("urlList", urlList);
return data;
}
/**
* 获取上传文件的完整路径
*
* @param bucketName 桶名称
* @param fileName 文件名
* @return
*/
public String getPresignedObjectUrl(String bucketName, String fileName) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
if (StringUtils.isEmpty(fileName)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
// 验证桶是否存在在
final boolean validationBucket = bucketExists(bucketName);
if (!validationBucket) {
throw new ServerException(ErrorCode.BUCKET_NOT_EXIST);
}
// 验证文件是否存在
final boolean validationFileName = doFileNameExist(bucketName, fileName);
if (!validationFileName) {
throw new ServerException(ErrorCode.FILE_NOT_EXIST);
}
String url = null;
try {
// 获取桶和文件的完整路径
url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(fileName)
.method(Method.GET)
.build());
} catch (MinioException e) {
log.error("Error occurred: " + e);
} catch (Exception e) {
e.printStackTrace();
}
return url;
}
/**
* 创建文件夹或目录
*
* @param bucketName 存储桶
* @param objectName 目录路径
*/
public Map<String, String> putDirObject(String bucketName, String objectName) throws Exception {
// 判断桶是否存在
if (!bucketExists(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_EXIST);
}
final ObjectWriteResponse response = minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(
new ByteArrayInputStream(new byte[]{}), 0, -1)
.build());
Map<String, String> map = new HashMap<>();
map.put("etag", response.etag());
map.put("versionId", response.versionId());
return map;
}
/**
* 判断文件是否存在
*
* @param fileName 对象
* @return true:存在
*/
public boolean doFileNameExist(String bucketName, String fileName) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
if (StringUtils.isEmpty(fileName)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileName).build());
} catch (Exception e) {
exist = false;
}
return exist;
}
/**
* 文件下载
*
* @param response
* @param fileName
*/
public void downloadFile(HttpServletResponse response, String bucketName, String fileName) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
if (StringUtils.isEmpty(fileName)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
// 判断文件是否存在
final boolean flag = doFileNameExist(bucketName, fileName);
if (!flag) {
throw new ServerException(ErrorCode.FILE_NOT_EXIST);
}
InputStream in = null;
try {
// 获取对象信息
StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(fileName).build());
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
// 文件下载
in = minioClient.getObject(
GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
IOUtils.copy(in, response.getOutputStream());
} catch (Exception e) {
log.error(e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
}
/**
* 删除文件
*
* @param bucketName bucket名称
* @param fileName 文件名称
* 说明:当前方法不能真正删除,需要验证
*/
public void deleteFile(String bucketName, String fileName) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
if (StringUtils.isEmpty(fileName)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
try {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
/**
* 批量文件删除
*
* @param bucketName bucket名称
* @param fileNames 文件名
*/
public void deleteBatchFile(String bucketName, List<String> fileNames) {
if (StringUtils.isEmpty(bucketName)) {
throw new ServerException(ErrorCode.BUCKET_NAME_NOT_NULL);
}
if (CollectionUtils.isEmpty(fileNames)) {
throw new ServerException(ErrorCode.FILE_NAME_NOT_NULL);
}
try {
List<DeleteObject> objects = new LinkedList<>();
for (String fileName : fileNames) {
objects.add(new DeleteObject(fileName));
}
Iterable<Result<DeleteError>> results =
minioClient.removeObjects(
RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
log.error("Error occurred: " + error);
}
} catch (Exception e) {
log.error("批量删除失败!error:{}", e);
}
}
/**
* 文件大小
*
* @param fileS
* @return
*/
private static String formatFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + " B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + " KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + " MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + " GB";
}
return fileSizeString;
}
}
三、上传文件实战
新建 UploadController,实现上传文件接口。
@Tag(name = "基础接口")
@AllArgsConstructor
@RestController
@RequestMapping("/file")
public class UploadController {
private final MinioUtils minioUtils;
@PostMapping("upload")
@Operation(summary = "上传文件")
public Result<Map<String, Object>> upload(@RequestParam(defaultValue = "common") String bucketName,
@RequestParam(name = "file", required = false) MultipartFile[] file) {
return Result.ok(minioUtils.uploadFile(bucketName, file));
}
}
调用上传文件接口后,系统会根据 bucketName 首先判断 bucket 是否存在,不存在则会开始创建,并且设置成公共读写。然后遍历文件数组,对文件重命名,并且记录下上传后的文件访问 url。最后进行文件上传。