565 lines
17 KiB
Java
565 lines
17 KiB
Java
package com.rj.utils;
|
||
|
||
import com.rj.config.MinIOConfig;
|
||
import io.minio.*;
|
||
import io.minio.http.Method;
|
||
import io.minio.messages.Bucket;
|
||
import io.minio.messages.Item;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.stereotype.Component;
|
||
import org.springframework.web.multipart.MultipartFile;
|
||
|
||
import java.io.*;
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.UUID;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
/**
|
||
* MinIO 工具类
|
||
* 提供文件上传、下载、删除等常用操作
|
||
*
|
||
* @author rj
|
||
* @date 2025-10-02
|
||
*/
|
||
@Slf4j
|
||
@Component
|
||
public class MinIOUtil {
|
||
|
||
@Autowired
|
||
private MinioClient minioClient;
|
||
|
||
@Autowired
|
||
private MinIOConfig minioConfig;
|
||
|
||
/**
|
||
* 检查存储桶是否存在
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @return 是否存在
|
||
*/
|
||
public boolean bucketExists(String bucketName) {
|
||
try {
|
||
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
|
||
} catch (Exception e) {
|
||
log.error("检查存储桶是否存在失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建存储桶
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @return 是否创建成功
|
||
*/
|
||
public boolean createBucket(String bucketName) {
|
||
try {
|
||
if (!bucketExists(bucketName)) {
|
||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||
log.info("存储桶创建成功: {}", bucketName);
|
||
return true;
|
||
}
|
||
log.info("存储桶已存在: {}", bucketName);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("创建存储桶失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取所有存储桶
|
||
*
|
||
* @return 存储桶列表
|
||
*/
|
||
public List<String> getAllBuckets() {
|
||
try {
|
||
List<Bucket> buckets = minioClient.listBuckets();
|
||
List<String> bucketNames = new ArrayList<>();
|
||
for (Bucket bucket : buckets) {
|
||
bucketNames.add(bucket.name());
|
||
}
|
||
return bucketNames;
|
||
} catch (Exception e) {
|
||
log.error("获取存储桶列表失败: {}", e.getMessage());
|
||
return new ArrayList<>();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除存储桶
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @return 是否删除成功
|
||
*/
|
||
public boolean removeBucket(String bucketName) {
|
||
try {
|
||
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
|
||
log.info("存储桶删除成功: {}", bucketName);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("删除存储桶失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传文件
|
||
*
|
||
* @param file 文件
|
||
* @param bucketName 存储桶名称
|
||
* @return 文件名
|
||
*/
|
||
public String uploadFile(MultipartFile file, String bucketName) {
|
||
return uploadFile(file, bucketName, null);
|
||
}
|
||
|
||
/**
|
||
* 上传文件
|
||
*
|
||
* @param file 文件
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称(文件名),如果为null则自动生成
|
||
* @return 文件名
|
||
*/
|
||
public String uploadFile(MultipartFile file, String bucketName, String objectName) {
|
||
try {
|
||
// 确保存储桶存在
|
||
createBucket(bucketName);
|
||
|
||
// 生成文件名
|
||
if (objectName == null || objectName.trim().isEmpty()) {
|
||
objectName = generateFileName(file.getOriginalFilename());
|
||
}
|
||
|
||
// 上传文件
|
||
minioClient.putObject(
|
||
PutObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.stream(file.getInputStream(), file.getSize(), -1)
|
||
.contentType(file.getContentType())
|
||
.build()
|
||
);
|
||
|
||
log.info("文件上传成功: {}/{}", bucketName, objectName);
|
||
return objectName;
|
||
|
||
} catch (Exception e) {
|
||
log.error("文件上传失败: {}", e.getMessage());
|
||
throw new RuntimeException("文件上传失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传文件流
|
||
*
|
||
* @param inputStream 文件流
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @param contentType 内容类型
|
||
* @return 是否上传成功
|
||
*/
|
||
public boolean uploadFile(InputStream inputStream, String bucketName, String objectName, String contentType) {
|
||
try {
|
||
createBucket(bucketName);
|
||
|
||
minioClient.putObject(
|
||
PutObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.stream(inputStream, -1, 10485760) // 10MB
|
||
.contentType(contentType)
|
||
.build()
|
||
);
|
||
|
||
log.info("文件流上传成功: {}/{}", bucketName, objectName);
|
||
return true;
|
||
|
||
} catch (Exception e) {
|
||
log.error("文件流上传失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传字节数组
|
||
*
|
||
* @param data 字节数组
|
||
* @param objectName 对象名称
|
||
* @param contentType 内容类型
|
||
* @return 对象名称
|
||
*/
|
||
public String uploadFile(byte[] data, String objectName, String contentType) {
|
||
try {
|
||
// 使用配置中的默认存储桶
|
||
String bucketName = minioConfig.getBucketName();
|
||
|
||
// 确保存储桶存在
|
||
createBucket(bucketName);
|
||
|
||
minioClient.putObject(
|
||
PutObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.stream(new java.io.ByteArrayInputStream(data), data.length, -1)
|
||
.contentType(contentType)
|
||
.build()
|
||
);
|
||
|
||
log.info("字节数组上传成功: {}/{}, 大小: {} bytes", bucketName, objectName, data.length);
|
||
return objectName;
|
||
|
||
} catch (Exception e) {
|
||
log.error("字节数组上传失败: {}", e.getMessage());
|
||
throw new RuntimeException("字节数组上传失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载文件
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @return 文件流
|
||
*/
|
||
public InputStream downloadFile(String bucketName, String objectName) {
|
||
try {
|
||
return minioClient.getObject(
|
||
GetObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.build()
|
||
);
|
||
} catch (Exception e) {
|
||
log.error("文件下载失败: {}", e.getMessage());
|
||
throw new RuntimeException("文件下载失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载文件到本地
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @param fileName 本地文件路径
|
||
* @return 是否下载成功
|
||
*/
|
||
public boolean downloadFile(String bucketName, String objectName, String fileName) {
|
||
try {
|
||
minioClient.downloadObject(
|
||
DownloadObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.filename(fileName)
|
||
.build()
|
||
);
|
||
log.info("文件下载成功: {}/{} -> {}", bucketName, objectName, fileName);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("文件下载失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除文件
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @return 是否删除成功
|
||
*/
|
||
public boolean removeFile(String bucketName, String objectName) {
|
||
try {
|
||
minioClient.removeObject(
|
||
RemoveObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.build()
|
||
);
|
||
log.info("文件删除成功: {}/{}", bucketName, objectName);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("文件删除失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文件信息
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @return 文件信息
|
||
*/
|
||
public StatObjectResponse getFileInfo(String bucketName, String objectName) {
|
||
try {
|
||
return minioClient.statObject(
|
||
StatObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.build()
|
||
);
|
||
} catch (Exception e) {
|
||
log.error("获取文件信息失败: {}", e.getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文件列表
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @return 文件列表
|
||
*/
|
||
public List<String> listFiles(String bucketName) {
|
||
return listFiles(bucketName, null);
|
||
}
|
||
|
||
/**
|
||
* 获取文件列表
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param prefix 文件前缀
|
||
* @return 文件列表
|
||
*/
|
||
public List<String> listFiles(String bucketName, String prefix) {
|
||
List<String> files = new ArrayList<>();
|
||
try {
|
||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||
ListObjectsArgs.builder()
|
||
.bucket(bucketName)
|
||
.prefix(prefix)
|
||
.build()
|
||
);
|
||
|
||
for (Result<Item> result : results) {
|
||
Item item = result.get();
|
||
files.add(item.objectName());
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("获取文件列表失败: {}", e.getMessage());
|
||
}
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* 获取文件详细列表
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @return 文件详细信息列表
|
||
*/
|
||
public List<FileInfo> listFileDetails(String bucketName) {
|
||
return listFileDetails(bucketName, null);
|
||
}
|
||
|
||
/**
|
||
* 获取文件详细列表
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param prefix 文件前缀
|
||
* @return 文件详细信息列表
|
||
*/
|
||
public List<FileInfo> listFileDetails(String bucketName, String prefix) {
|
||
List<FileInfo> files = new ArrayList<>();
|
||
try {
|
||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||
ListObjectsArgs.builder()
|
||
.bucket(bucketName)
|
||
.prefix(prefix)
|
||
.build()
|
||
);
|
||
|
||
for (Result<Item> result : results) {
|
||
Item item = result.get();
|
||
FileInfo fileInfo = new FileInfo();
|
||
fileInfo.setObjectName(item.objectName());
|
||
fileInfo.setSize(item.size());
|
||
fileInfo.setLastModified(item.lastModified());
|
||
fileInfo.setEtag(item.etag());
|
||
files.add(fileInfo);
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("获取文件详细列表失败: {}", e.getMessage());
|
||
}
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* 获取文件预签名URL(用于临时访问)
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @param expires 过期时间(秒)
|
||
* @return 预签名URL
|
||
*/
|
||
public String getPresignedObjectUrl(String bucketName, String objectName, int expires) {
|
||
try {
|
||
// 检查文件是否存在
|
||
if (!objectExists(bucketName, objectName)) {
|
||
log.error("文件不存在: bucket={}, object={}", bucketName, objectName);
|
||
return null;
|
||
}
|
||
|
||
log.info("生成预签名URL: bucket={}, object={}, expires={}秒", bucketName, objectName, expires);
|
||
|
||
String presignedUrl = minioClient.getPresignedObjectUrl(
|
||
GetPresignedObjectUrlArgs.builder()
|
||
.method(Method.GET)
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.expiry(expires, TimeUnit.SECONDS)
|
||
.build()
|
||
);
|
||
|
||
log.info("生成的预签名URL: {}", presignedUrl);
|
||
return presignedUrl;
|
||
} catch (Exception e) {
|
||
log.error("获取预签名URL失败: bucket={}, object={}, error={}", bucketName, objectName, e.getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查对象是否存在
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @return 是否存在
|
||
*/
|
||
public boolean objectExists(String bucketName, String objectName) {
|
||
try {
|
||
minioClient.statObject(
|
||
StatObjectArgs.builder()
|
||
.bucket(bucketName)
|
||
.object(objectName)
|
||
.build()
|
||
);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.debug("对象不存在: bucket={}, object={}, error={}", bucketName, objectName, e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文件预签名URL(默认7天过期)
|
||
*
|
||
* @param bucketName 存储桶名称
|
||
* @param objectName 对象名称
|
||
* @return 预签名URL
|
||
*/
|
||
public String getPresignedObjectUrl(String bucketName, String objectName) {
|
||
return getPresignedObjectUrl(bucketName, objectName, 7 * 24 * 3600); // 7天
|
||
}
|
||
|
||
/**
|
||
* 复制文件
|
||
*
|
||
* @param sourceBucketName 源存储桶名称
|
||
* @param sourceObjectName 源对象名称
|
||
* @param targetBucketName 目标存储桶名称
|
||
* @param targetObjectName 目标对象名称
|
||
* @return 是否复制成功
|
||
*/
|
||
public boolean copyFile(String sourceBucketName, String sourceObjectName,
|
||
String targetBucketName, String targetObjectName) {
|
||
try {
|
||
createBucket(targetBucketName);
|
||
|
||
minioClient.copyObject(
|
||
CopyObjectArgs.builder()
|
||
.bucket(targetBucketName)
|
||
.object(targetObjectName)
|
||
.source(CopySource.builder()
|
||
.bucket(sourceBucketName)
|
||
.object(sourceObjectName)
|
||
.build())
|
||
.build()
|
||
);
|
||
|
||
log.info("文件复制成功: {}/{} -> {}/{}",
|
||
sourceBucketName, sourceObjectName, targetBucketName, targetObjectName);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("文件复制失败: {}", e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成唯一文件名
|
||
*
|
||
* @param originalFilename 原始文件名
|
||
* @return 唯一文件名
|
||
*/
|
||
private String generateFileName(String originalFilename) {
|
||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||
String extension = "";
|
||
|
||
if (originalFilename != null && originalFilename.contains(".")) {
|
||
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||
}
|
||
|
||
return timestamp + "_" + uuid + extension;
|
||
}
|
||
|
||
/**
|
||
* 文件信息类
|
||
*/
|
||
public static class FileInfo {
|
||
private String objectName;
|
||
private Long size;
|
||
private java.time.ZonedDateTime lastModified;
|
||
private String etag;
|
||
|
||
// Getters and Setters
|
||
public String getObjectName() {
|
||
return objectName;
|
||
}
|
||
|
||
public void setObjectName(String objectName) {
|
||
this.objectName = objectName;
|
||
}
|
||
|
||
public Long getSize() {
|
||
return size;
|
||
}
|
||
|
||
public void setSize(Long size) {
|
||
this.size = size;
|
||
}
|
||
|
||
public java.time.ZonedDateTime getLastModified() {
|
||
return lastModified;
|
||
}
|
||
|
||
public void setLastModified(java.time.ZonedDateTime lastModified) {
|
||
this.lastModified = lastModified;
|
||
}
|
||
|
||
public String getEtag() {
|
||
return etag;
|
||
}
|
||
|
||
public void setEtag(String etag) {
|
||
this.etag = etag;
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
return "FileInfo{" +
|
||
"objectName='" + objectName + '\'' +
|
||
", size=" + size +
|
||
", lastModified=" + lastModified +
|
||
", etag='" + etag + '\'' +
|
||
'}';
|
||
}
|
||
}
|
||
}
|