注意:本代码是在若依springboot3版本上实现的,如果你不是在若依上面实现,需要将所有用到若依的相关代码修改后才能运行
在application.yml中添加以下配置:
# MinIO文件服务器配置
minio:
# minioAPI服务地址
url: http://192.168.186.132:9000
# 访问密钥(Access Key)
accessKey: minioadmin
# 私有密钥(Secret Key)
secretKey: minioadmin
# 默认存储桶名称
defaultBucketName: ruoyi
# 连接超时(秒)
connectTimeout: 10
# 写入超时(秒)
writeTimeout: 100
# 读取超时(秒)
readTimeout: 20
# 是否自动创建默认桶
autoCreateBucket: true
# 默认桶权限(read-only,read-write,private)
defaultBucketPolicy: read-only
# 是否启用图片压缩
compressEnabled: true
# 是否将压缩后的图片转换为webp格式
convertToWebp: true
# 压缩图片质量(0-100的整数)
compressQuality: 30
# 图片压缩阈值(字节),超过此大小才压缩
compressThreshold: 102400
io.minio
minio
8.5.2
org.sejda.imageio
webp-imageio
0.4.18
net.coobird
thumbnailator
0.1.6
package com.ruoyi.file.utils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.file.local.config.LocalFileConstants;
import com.ruoyi.file.local.exception.LocalFileException;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
/**
* 本地文件操作工具类
*
* @author ruoyi
*/
public class FileUtils {
private static final Logger log = LoggerFactory.getLogger(FileUtils.class);
/**
* 自定义MIME类型映射
*/
private static final Map MIME_TYPE_MAP = new HashMap<>();
static {
// 初始化自定义MIME类型映射
MIME_TYPE_MAP.put("webp", "image/webp");
MIME_TYPE_MAP.put("svg", "image/svg+xml");
MIME_TYPE_MAP.put("avif", "image/avif");
MIME_TYPE_MAP.put("heic", "image/heic");
MIME_TYPE_MAP.put("heif", "image/heif");
}
/**
* 生成唯一文件名
*
* @param originalFilename 原始文件名
* @return 唯一文件名
*/
public static String generateUniqueFileName(String originalFilename) {
String extension = FilenameUtils.getExtension(originalFilename);
return IdUtils.fastSimpleUUID() + (StringUtils.isNotBlank(extension) ? "." + extension : "");
}
/**
* 创建路径中的所有目录
*
* @param path 完整路径
* @return 创建的目录
*/
public static File createDirectories(String path) {
File directory = new File(path);
if (!directory.exists() && !directory.mkdirs()) {
throw new LocalFileException("无法创建目录: " + path);
}
return directory;
}
/**
* 获取文件的MIME类型
*
* @param path 文件路径
* @return MIME类型
*/
public static String getContentType(Path path) {
try {
// 先尝试从文件系统获取MIME类型
String contentType = Files.probeContentType(path);
// 如果获取不到MIME类型或为null,尝试从文件扩展名获取
if (contentType == null || contentType.isEmpty()) {
String extension = FilenameUtils.getExtension(path.toString()).toLowerCase();
if (extension != null && !extension.isEmpty()) {
String mimeType = MIME_TYPE_MAP.get(extension);
if (mimeType != null) {
return mimeType;
}
}
}
return contentType != null ? contentType : LocalFileConstants.DEFAULT_CONTENT_TYPE;
} catch (IOException e) {
log.error("获取文件类型失败: {}", e.getMessage());
return LocalFileConstants.DEFAULT_CONTENT_TYPE;
}
}
/**
* 格式化文件大小
*
* @param size 文件大小(字节)
* @return 格式化后的文件大小
*/
public static String formatFileSize(long size) {
if (size <= 0) return "0 B";
final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return String.format("%.2f %s", size / Math.pow(1024, digitGroups), units[digitGroups]);
}
}
package com.ruoyi.file.utils;
import com.luciad.imageio.webp.WebPWriteParam;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
import org.apache.poi.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
/**
* 图片处理工具类
*
* @author ruoyi
*/
public class ImageCompressUtils {
// 支持的WebP格式扩展名列表,用于判断是否需要转换为WebP格式
// 注意:这里的扩展名不包含点号(.),例如 "jpg" 而不是 ".jpg"
// 这个列表是为了在处理图片时判断是否需要将图片转换为WebP格式
// 如果图片的扩展名在这个列表中,那么就会将其转换为WebP格式
// 如果图片的扩展名不在这个列表中,那么就不会进行转换,因为其他格式不支持转换为WebP格式
public static final List WEBP_SUPPORTED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "webp");
// 图片文件扩展名列表,用于判断文件是否为图片类型
public static final List IMAGE_EXTENSIONS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "tif", "svg", "ico");
/**
* 自定义方法获取文件扩展名(不使用FilenameUtils)
* @param filename 文件名
* @return 小写的文件扩展名(不包含点号)
*/
public static String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "";
}
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1 || dotIndex == filename.length() - 1) {
return "";
}
return filename.substring(dotIndex + 1).toLowerCase();
}
/**
* 判断文件是否为图片类型
*
* @param file 待检查的文件
* @return 如果是图片返回true,否则返回false
*/
public static boolean isImage(MultipartFile file) {
if (file == null || file.isEmpty()) {
System.err.println("ImageUtils.isImage: 文件为空");
return false;
}
// 检查文件扩展名 - 使用自定义方法获取扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename != null) {
String extension = getFileExtension(originalFilename);
System.err.println("ImageUtils.isImage: 文件扩展名 = " + extension);
if (!extension.isEmpty() && IMAGE_EXTENSIONS.contains(extension)) {
System.err.println("ImageUtils.isImage: 扩展名检测为图片");
return true;
}
} else {
System.err.println("ImageUtils.isImage: 无法获取文件名");
}
// 检查文件的MIME类型
String contentType = file.getContentType();
System.err.println("ImageUtils.isImage: 内容类型 = " + contentType);
if (contentType != null && contentType.startsWith("image/")) {
System.err.println("ImageUtils.isImage: MIME类型检测为图片");
return true;
}
return false;
}
/**
* 将图片压缩并转换为WebP格式
*
* @param file 图片文件
* @param compressionQuality 压缩质量,范围从0.0到1.0
* @param isConvertToWebP 是否转化为webp
* @throws IOException 当IO操作失败时抛出
*/
public static byte[] compressAndToWebp(MultipartFile file, float compressionQuality, boolean isConvertToWebP) throws IOException {
// 使用自定义方法获取扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new IllegalArgumentException("文件名不能为空");
}
String extension = getFileExtension(originalFilename);
System.err.println("compressAndToWebp: 文件扩展名 = " + extension);
// 检查图片格式是否支持
if (!WEBP_SUPPORTED_EXTENSIONS.contains(extension)) {
throw new IllegalArgumentException("不支持的图片格式: " + extension);
}
// 读取文件字节
byte[] fileBytes = file.getBytes();
System.err.println("compressAndToWebp: 读取文件字节完成,大小 = " + fileBytes.length);
// 使用ByteArrayInputStream读取图片
try (ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes)) {
BufferedImage originalImage = ImageIO.read(bais);
if (originalImage == null) {
System.err.println("compressAndToWebp: 无法读取图片数据,图片为null");
throw new IllegalArgumentException("无法读取图片数据");
}
System.err.println("compressAndToWebp: 成功读取图片,尺寸 = " + originalImage.getWidth() + "x" + originalImage.getHeight());
// 如果是PNG格式且有透明通道,特殊处理
if (extension.equals("png") && originalImage.getColorModel().hasAlpha()) {
System.err.println("compressAndToWebp: PNG图片有透明通道,进行特殊处理");
// 创建不带透明通道的新图像
BufferedImage newImage = new BufferedImage(
originalImage.getWidth(),
originalImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
// 使用白色背景填充
Graphics2D g = newImage.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, originalImage.getWidth(), originalImage.getHeight());
g.drawImage(originalImage, 0, 0, null);
g.dispose();
originalImage = newImage;
}
if (!isConvertToWebP) {
System.err.println("compressAndToWebp: 不转换为WebP,直接压缩为JPEG");
// 如果不转换为WebP,直接返回JPEG压缩后的字节数组
return compressToJpeg(originalImage, compressionQuality);
}
System.err.println("compressAndToWebp: 转换为WebP格式");
// 转换为WebP格式
return convertToWebP(originalImage, compressionQuality);
} catch (IOException e) {
System.err.println("compressAndToWebp异常: " + e.getMessage());
e.printStackTrace();
throw e;
}
}
/**
* 压缩为JPEG格式
*/
private static byte[] compressToJpeg(BufferedImage image, float compressionQuality) throws IOException {
try (ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream()) {
ImageWriter jpegWriter = ImageIO.getImageWritersByFormatName("jpeg").next();
ImageWriteParam jpegWriteParam = jpegWriter.getDefaultWriteParam();
jpegWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpegWriteParam.setCompressionQuality(compressionQuality);
try (ImageOutputStream jpegIos = ImageIO.createImageOutputStream(jpegOutputStream)) {
jpegWriter.setOutput(jpegIos);
jpegWriter.write(null, new IIOImage(image, null, null), jpegWriteParam);
} finally {
jpegWriter.dispose();
}
return jpegOutputStream.toByteArray();
}
}
/**
* 转换为WebP格式
*/
private static byte[] convertToWebP(BufferedImage image, float compressionQuality) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
WebPWriteParam writeParam = new WebPWriteParam(Locale.getDefault());
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSY_COMPRESSION]);
writeParam.setCompressionQuality(compressionQuality);
Iterator writers = ImageIO.getImageWritersByMIMEType("image/webp");
if (!writers.hasNext()) {
throw new IllegalStateException("No writers found for WebP format");
}
ImageWriter writer = writers.next();
try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) {
writer.setOutput(ios);
writer.write(null, new IIOImage(image, null, null), writeParam);
} finally {
writer.dispose();
}
return baos.toByteArray();
}
}
}
package com.ruoyi.file.utils;
import org.apache.commons.lang3.StringUtils;
/**
* 路径处理工具类
*/
public class PathUtils {
/**
* 规范化路径
* @param path 原始路径
* @return 规范化后的路径,以斜杠开头,不以斜杠结尾
*/
public static String normalizePath(String path) {
if (StringUtils.isBlank(path)) {
return "/";
}
// 去除首尾空格
path = path.trim();
// 将所有反斜杠转为正斜杠
path = path.replace('\\', '/');
// 处理连续斜杠
while (path.contains("//")) {
path = path.replace("//", "/");
}
// 去除末尾的斜杠
path = StringUtils.removeEnd(path, "/");
// 确保路径以斜杠开头
if (!path.startsWith("/")) {
path = "/" + path;
}
return path;
}
/**
* 确保路径以斜杠结尾
* @param path 原始路径
* @return 以斜杠结尾的路径
*/
public static String ensureEndWithSlash(String path) {
if (StringUtils.isBlank(path)) {
return "/";
}
if (!path.endsWith("/")) {
path = path + "/";
}
return path;
}
/**
* 确保路径不以斜杠结尾
* @param path 原始路径
* @return 不以斜杠结尾的路径
*/
public static String ensureNotEndWithSlash(String path) {
if (StringUtils.isBlank(path)) {
return "/";
}
return StringUtils.removeEnd(path, "/");
}
}
package com.ruoyi.file.minio.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "文件树节点")
public class FileTreeNode {
/** 桶名称 */
@Schema(description = "桶名称")
private String bucketName;
/** 文件或目录名称 */
@Schema(description = "文件或目录名称")
private String name;
/** 是否为目录 */
@Schema(description = "是否为目录")
private boolean isDirectory;
/** 是否有子项 */
@Schema(description = "是否有子项")
private boolean hasChildren;
/** 子节点列表 */
@Schema(description = "子节点列表")
private List children;
/** 相对于根路径 */
@Schema(description = "相对于根路径")
private String path;
/** 浏览器访问路径 */
@Schema(description = "浏览器访问路径")
private String url;
/** 文件大小 **/
@Schema(description = "文件大小")
private String size;
/** 创建时间 **/
@Schema(description = "创建时间")
private String createTime;
/** 更新时间 **/
@Schema(description = "更新时间")
private String updateTime;
}
package com.ruoyi.file.minio.exception;
import java.io.Serial;
/**
* MinIO操作异常类
*
* @author ruoyi
*/
public class MinioException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private String code;
/**
* 构造方法
*
* @param message 错误消息
*/
public MinioException(String message) {
super(message);
}
/**
* 构造方法
*
* @param message 错误消息
* @param cause 异常原因
*/
public MinioException(String message, Throwable cause) {
super(message, cause);
}
/**
* 构造方法
*
* @param code 错误码
* @param message 错误消息
*/
public MinioException(String code, String message) {
super(message);
this.code = code;
}
/**
* 构造方法
*
* @param code 错误码
* @param message 错误消息
* @param cause 异常原因
*/
public MinioException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public String getCode() {
return code;
}
}
package com.ruoyi.file.minio.util;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.file.minio.config.MinioConstants;
import com.ruoyi.file.minio.exception.MinioException;
import io.minio.errors.*;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* MinIO工具类
*
* @author ruoyi
*/
public class MinioUtils {
/**
* 检查存储桶名称是否有效
*
* @param bucketName 存储桶名称
* @throws MinioException 如果名称无效则抛出异常
*/
public static void checkBucketName(String bucketName) throws MinioException {
if (StringUtils.isBlank(bucketName)) {
throw new MinioException("存储桶名称不能为空");
}
// 名称长度必须在3到63个字符之间
if (bucketName.length() < 3 || bucketName.length() > 63) {
throw new MinioException("存储桶名称长度必须在3到63个字符之间");
}
// 只能包含小写字母、数字和短横线
if (!bucketName.matches("^[a-z0-9.-]+$")) {
throw new MinioException("存储桶名称只能包含小写字母、数字、短横线和点");
}
// 必须以字母或数字开头和结尾
if (!bucketName.matches("^[a-z0-9].*[a-z0-9]$")) {
throw new MinioException("存储桶名称必须以字母或数字开头和结尾");
}
// 不能是IP地址格式
if (bucketName.matches("^(\\d{1,3}\\.){3}\\d{1,3}$")) {
throw new MinioException("存储桶名称不能是IP地址格式");
}
}
/**
* 从异常中提取友好的错误消息
*
* @param e 异常
* @return 友好的错误消息
*/
public static String getFriendlyErrorMessage(Exception e) {
if (e instanceof ErrorResponseException) {
ErrorResponseException ere = (ErrorResponseException) e;
return ere.getMessage() + (ere.errorResponse() != null ? ": " + ere.errorResponse().message() : "");
} else if (e instanceof InsufficientDataException) {
return "MinIO客户端收到的数据少于预期,操作失败";
} else if (e instanceof InternalException) {
return "MinIO服务器内部错误";
} else if (e instanceof InvalidKeyException) {
return "无效的访问密钥或私有密钥";
} else if (e instanceof InvalidResponseException) {
return "MinIO客户端收到无效的响应";
} else if (e instanceof IOException) {
return "I/O错误: " + e.getMessage();
} else if (e instanceof NoSuchAlgorithmException) {
return "请求中指定的签名算法不可用";
} else if (e instanceof ServerException) {
return "MinIO服务器端错误";
} else if (e instanceof XmlParserException) {
return "解析XML响应时发生错误";
}
return e.getMessage();
}
}
package com.ruoyi.file.minio.service;
import com.ruoyi.file.minio.domain.FileTreeNode;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* MinIO文件管理服务接口
*
* @author ruoyi
*/
public interface IMinioFileService {
/**
* 上传文件
*
* @param file 文件
* @param bucketName 存储桶名称,如果为空则使用默认的桶
* @param objectPath 对象存储路径,如果为空则使用默认路径(当前日期)
* @return 文件访问路径等信息
*/
Map uploadFile(MultipartFile file, String bucketName, String objectPath);
/**
* 创建文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 文件夹路径
*/
void createFolder(String bucketName, String folderPath);
/**
* 删除文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
*/
void deleteFile(String bucketName, String objectName);
/**
* 删除文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 文件夹路径
* @param recursive 是否递归删除所有文件,true-删除所有,false-如果目录非空则不删除
*/
void deleteFolder(String bucketName, String folderPath, boolean recursive);
/**
* 重命名文件
*
* @param bucketName 存储桶名称
* @param objectName 原对象名称
* @param newObjectName 新对象名称
*/
void renameFile(String bucketName, String objectName, String newObjectName);
/**
* 重命名文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 原文件夹路径
* @param newFolderPath 新文件夹路径
* @return 成功重命名的对象数量
*/
int renameFolder(String bucketName, String folderPath, String newFolderPath);
/**
* 获取文件流
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 文件流
*/
InputStream getObject(String bucketName, String objectName);
/**
* 获取文件信息和文件流
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 包含文件信息和文件流的Map,包括stream(输入流)、size(大小)、contentType(内容类型)、lastModified(最后修改时间)等
*/
Map getObjectInfo(String bucketName, String objectName);
/**
* 下载文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param response HTTP响应对象
*/
void downloadFile(String bucketName, String objectName, HttpServletResponse response);
/**
* 预览文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param response HTTP响应对象
*/
void previewFile(String bucketName, String objectName, HttpServletResponse response);
/**
* 列出指定目录下的文件和文件夹
*
* @param bucketName 存储桶名称
* @param prefix 前缀(目录路径)
* @param recursive 是否递归查询
* @return 文件和文件夹列表,以FileTreeNode格式返回
*/
List listObjects(String bucketName, String prefix, boolean recursive);
}
package com.ruoyi.file.minio.service;
import java.util.List;
import java.util.Map;
/**
* MinIO存储桶管理服务接口
*
* @author ruoyi
*/
public interface IMinioBucketService {
/**
* 获取所有存储桶列表
*
* @return 存储桶信息列表,每个桶包含名称、创建时间和访问策略等信息
*/
List
package com.ruoyi.file.minio.service.impl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.file.minio.config.MinioConstants;
import com.ruoyi.file.minio.exception.MinioException;
import com.ruoyi.file.minio.service.IMinioBucketService;
import com.ruoyi.file.minio.util.MinioUtils;
import io.minio.*;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MinIO存储桶管理服务实现类
*
* @author ruoyi
*/
@Service
public class MinioBucketServiceImpl implements IMinioBucketService {
private static final Logger log = LoggerFactory.getLogger(MinioBucketServiceImpl.class);
private final MinioClient minioClient;
public MinioBucketServiceImpl(MinioClient minioClient) {
this.minioClient = minioClient;
}
/**
* 获取所有存储桶列表
*/
@Override
public List
package com.ruoyi.file.minio.service.impl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.file.minio.config.MinioConfig;
import com.ruoyi.file.minio.config.MinioConstants;
import com.ruoyi.file.minio.domain.FileTreeNode;
import com.ruoyi.file.minio.exception.MinioException;
import com.ruoyi.file.minio.service.IMinioBucketService;
import com.ruoyi.file.minio.service.IMinioFileService;
import com.ruoyi.file.minio.util.MinioUtils;
import com.ruoyi.file.utils.FileUtils;
import com.ruoyi.file.utils.ImageCompressUtils;
import com.ruoyi.file.utils.PathUtils;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* MinIO文件管理服务实现
*/
@Service
public class MinioFileServiceImpl implements IMinioFileService {
private static final Logger log = LoggerFactory.getLogger(MinioFileServiceImpl.class);
private final MinioClient minioClient;
private final MinioConfig minioConfig;
private final IMinioBucketService minioBucketService;
public MinioFileServiceImpl(MinioClient minioClient, MinioConfig minioConfig, IMinioBucketService minioBucketService) {
this.minioClient = minioClient;
this.minioConfig = minioConfig;
this.minioBucketService = minioBucketService;
}
/**
* 上传文件
*
* @param file 文件
* @param bucketName 存储桶名称,如果为空则使用默认的桶
* @param objectPath 对象存储路径,如果为空则使用默认路径(当前日期)
* @return 文件访问路径等信息
*/
@Override
public Map uploadFile(MultipartFile file, String bucketName, String objectPath) {
try {
if (file == null || file.isEmpty()) {
throw new MinioException("上传文件不能为空");
}
// 确定存储桶名称
String finalBucketName = StringUtils.isNotBlank(bucketName) ? bucketName : minioConfig.getDefaultBucketName();
// 确定存储路径
String finalObjectPath;
if (StringUtils.isNotBlank(objectPath)) {
finalObjectPath = PathUtils.normalizePath(objectPath);
finalObjectPath = PathUtils.ensureEndWithSlash(finalObjectPath);
// 移除前导斜杠用于MinIO操作
finalObjectPath = finalObjectPath.startsWith("/") ? finalObjectPath.substring(1) : finalObjectPath;
} else {
finalObjectPath = generateDefaultPath().substring(1) + "/"; // 去除前导斜杠
}
// 文件原始名称和大小
String originalFilename = file.getOriginalFilename();
long fileSize = file.getSize();
// 使用FileUtils生成唯一文件名
String fileName = FileUtils.generateUniqueFileName(originalFilename);
String objectName = finalObjectPath + fileName;
// 检查存储桶是否存在,不存在则创建
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(finalBucketName).build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(finalBucketName).build());
minioBucketService.updateBucketPolicy(finalBucketName, MinioConstants.BucketPolicy.READ_WRITE);
}
// 判断是否需要压缩图片
boolean isImage = false;
byte[] compressedImageBytes = null;
String contentType = file.getContentType();
// 检查文件是否为图片
boolean isImageCheck = ImageCompressUtils.isImage(file);
// 如果启用了图片压缩,且文件确实是图片类型,则进行压缩
if (minioConfig.isCompressEnabled() && isImageCheck && fileSize > minioConfig.getCompressThreshold()) {
isImage = true;
try {
float quality = minioConfig.getCompressQuality() / 100.0f; // 将整数质量值转换为0-1之间的浮点数
boolean convertToWebp = minioConfig.isConvertToWebp();
// 压缩图片并可能转换为WebP格式
compressedImageBytes = ImageCompressUtils.compressAndToWebp(file, quality, convertToWebp);
// 如果转换为WebP格式,更新文件名
if (convertToWebp) {
fileName = FileUtils.generateUniqueFileName("image.webp");
objectName = finalObjectPath + fileName;
}
log.info("图片压缩成功:原始大小={}, 压缩后大小={}, 压缩率={}%",
fileSize,
compressedImageBytes.length,
String.format("%.2f", (1 - compressedImageBytes.length / (float) fileSize) * 100));
} catch (Exception e) {
// 压缩失败,记录错误但继续使用原始文件
log.error("图片压缩失败,将使用原始文件: {}", e.getMessage());
isImage = false;
compressedImageBytes = null;
}
}
// 上传文件到MinIO
if (compressedImageBytes != null) {
// 上传压缩后的图片
minioClient.putObject(
PutObjectArgs.builder()
.bucket(finalBucketName)
.object(objectName)
.stream(new ByteArrayInputStream(compressedImageBytes), compressedImageBytes.length, -1)
.contentType(isImage ? (minioConfig.isConvertToWebp() ? "image/webp" : contentType) : contentType)
.build()
);
} else {
// 上传原始文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(finalBucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(contentType)
.build()
);
}
// 构建文件访问URL
String url = minioConfig.getUrl() + "/" + finalBucketName + "/" + objectName;
// 返回文件信息
Map result = new HashMap<>();
result.put("fileName", fileName);
result.put("originalFilename", originalFilename);
result.put("size", FileUtils.formatFileSize(isImage && compressedImageBytes != null ? compressedImageBytes.length : fileSize));
result.put("contentType", isImage && minioConfig.isConvertToWebp() ? "image/webp" : contentType);
result.put("bucketName", finalBucketName);
result.put("objectName", objectName);
result.put("url", url);
return result;
} catch (Exception e) {
throw new MinioException("上传文件失败: " + e.getMessage(), e);
}
}
/**
* 创建文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 文件夹路径
*/
@Override
public void createFolder(String bucketName, String folderPath) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(folderPath)) {
throw new IllegalArgumentException("文件夹路径不能为空");
}
// 使用PathUtils规范化路径
String normalizedPath = PathUtils.normalizePath(folderPath);
normalizedPath = PathUtils.ensureEndWithSlash(normalizedPath);
// 移除路径开头的斜杠,MinIO不需要前导斜杠
String folderName = normalizedPath.startsWith("/") ? normalizedPath.substring(1) : normalizedPath;
// 检查桶是否存在,如果不存在则创建
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
minioBucketService.updateBucketPolicy(bucketName, MinioConstants.BucketPolicy.READ_WRITE);
}
// 创建空文件作为文件夹标记
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(folderName)
.stream(new ByteArrayInputStream(new byte[0]), 0, -1)
.build()
);
log.info("已创建文件夹: " + bucketName + "/" + folderName);
} catch (Exception e) {
throw new RuntimeException("创建文件夹失败: " + e.getMessage(), e);
}
}
/**
* 删除文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
*/
@Override
public void deleteFile(String bucketName, String objectName) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(objectName)) {
throw new IllegalArgumentException("对象名称不能为空");
}
// 处理路径,使用PathUtils规范化
String normalizedPath = PathUtils.normalizePath(objectName);
// 移除路径开头的斜杠,MinIO不需要前导斜杠
String object = normalizedPath.startsWith("/") ? normalizedPath.substring(1) : normalizedPath;
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 删除文件
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(object)
.build()
);
log.info("删除文件成功: {}/{}", bucketName, object);
} catch (Exception e) {
throw new RuntimeException("删除文件失败: " + e.getMessage(), e);
}
}
/**
* 删除文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 文件夹路径
* @param recursive 是否递归删除所有文件,true-删除所有,false-如果目录非空则不删除
*/
@Override
public void deleteFolder(String bucketName, String folderPath, boolean recursive) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(folderPath)) {
throw new IllegalArgumentException("文件夹路径不能为空");
}
// 规范化路径
String normalizedPath = PathUtils.normalizePath(folderPath);
normalizedPath = PathUtils.ensureEndWithSlash(normalizedPath);
// 移除路径开头的斜杠,MinIO不需要前导斜杠
String folder = normalizedPath.startsWith("/") ? normalizedPath.substring(1) : normalizedPath;
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 获取文件夹下的所有对象
List objectsToDelete = new ArrayList<>();
Iterable> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(folder)
.recursive(true)
.build()
);
// 收集要删除的对象
int count = 0;
for (Result- result : results) {
Item item = result.get();
objectsToDelete.add(new DeleteObject(item.objectName()));
count++;
}
// 检查是否找到了对象
if (objectsToDelete.isEmpty()) {
throw new IllegalArgumentException("找不到指定的文件夹: " + folderPath);
}
// 如果非递归模式且文件夹非空(不仅包含文件夹本身),则不删除
if (!recursive && count > 1) {
throw new IllegalArgumentException("文件夹不为空,无法删除: " + folderPath);
}
// 批量删除对象
Iterable
> deleteResults = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(objectsToDelete)
.build()
);
// 检查删除结果
for (Result deleteResult : deleteResults) {
DeleteError error = deleteResult.get();
log.error("删除对象失败: {}", error.message());
}
log.info("删除文件夹及其内容成功: {}/{}", bucketName, folder);
} catch (Exception e) {
throw new RuntimeException("删除文件夹失败: " + e.getMessage(), e);
}
}
/**
* 重命名文件
*
* @param bucketName 存储桶名称
* @param objectName 原对象名称
* @param newObjectName 新对象名称
*/
@Override
public void renameFile(String bucketName, String objectName, String newObjectName) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(objectName) || StringUtils.isBlank(newObjectName)) {
throw new IllegalArgumentException("对象名称不能为空");
}
// 使用PathUtils规范化路径
String normalizedOldPath = PathUtils.normalizePath(objectName);
String normalizedNewPath = PathUtils.normalizePath(newObjectName);
// MinIO不需要前导斜杠
String sourceObject = normalizedOldPath.startsWith("/") ? normalizedOldPath.substring(1) : normalizedOldPath;
String targetObject = normalizedNewPath.startsWith("/") ? normalizedNewPath.substring(1) : normalizedNewPath;
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 检查源文件是否存在
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(sourceObject)
.build()
);
} catch (Exception e) {
throw new IllegalArgumentException("源文件不存在: " + objectName);
}
// 复制对象到新路径
minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(sourceObject).build())
.bucket(bucketName)
.object(targetObject)
.build()
);
// 删除旧对象
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(sourceObject)
.build()
);
log.info("重命名文件成功: {}/{} -> {}/{}", bucketName, sourceObject, bucketName, targetObject);
} catch (Exception e) {
throw new RuntimeException("重命名文件失败: " + e.getMessage(), e);
}
}
/**
* 重命名文件夹
*
* @param bucketName 存储桶名称
* @param folderPath 原文件夹路径
* @param newFolderPath 新文件夹路径
* @return 成功重命名的对象数量
*/
@Override
public int renameFolder(String bucketName, String folderPath, String newFolderPath) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(folderPath) || StringUtils.isBlank(newFolderPath)) {
throw new IllegalArgumentException("文件夹路径不能为空");
}
// 使用PathUtils规范化路径
String normalizedOldPath = PathUtils.normalizePath(folderPath);
String normalizedNewPath = PathUtils.normalizePath(newFolderPath);
// 确保路径以斜杠结尾
normalizedOldPath = PathUtils.ensureEndWithSlash(normalizedOldPath);
normalizedNewPath = PathUtils.ensureEndWithSlash(normalizedNewPath);
// 检查路径层级,除最后一级目录外,前面的目录必须一致
java.nio.file.Path sourcePath = java.nio.file.Paths.get(normalizedOldPath);
java.nio.file.Path targetPath = java.nio.file.Paths.get(normalizedNewPath);
java.nio.file.Path sourceParent = sourcePath.getParent();
java.nio.file.Path targetParent = targetPath.getParent();
// 如果两个路径都是根路径下的直接子目录,则允许重命名
boolean isRootLevelRename = (sourceParent == null || sourceParent.toString().equals("/") || sourceParent.toString().isEmpty())
&& (targetParent == null || targetParent.toString().equals("/") || targetParent.toString().isEmpty());
if (!isRootLevelRename && (sourceParent == null || targetParent == null || !sourceParent.equals(targetParent))) {
throw new IllegalArgumentException("只能重命名最后一级目录,前面的目录层级必须一致");
}
// 处理前缀路径,移除开头的斜杠用于Minio操作
String oldFolder = normalizedOldPath.startsWith("/") ? normalizedOldPath.substring(1) : normalizedOldPath;
String newFolder = normalizedNewPath.startsWith("/") ? normalizedNewPath.substring(1) : normalizedNewPath;
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 获取文件夹下的所有对象
List objectsToRename = new ArrayList<>();
Iterable> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(oldFolder)
.recursive(true)
.build()
);
for (Result- result : results) {
Item item = result.get();
objectsToRename.add(item.objectName());
}
// 检查是否找到了原文件夹
if (objectsToRename.isEmpty()) {
throw new IllegalArgumentException("找不到指定的文件夹: " + folderPath);
}
// 重命名每个对象
int renamedCount = 0;
for (String oldObjectName : objectsToRename) {
String newObjectName = oldObjectName.replace(oldFolder, newFolder);
// 复制对象到新路径
minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(oldObjectName).build())
.bucket(bucketName)
.object(newObjectName)
.build()
);
// 删除旧对象
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(oldObjectName)
.build()
);
renamedCount++;
}
// 如果没有重命名任何对象(可能是一个空文件夹),创建一个目标空文件夹
if (renamedCount == 0) {
// 检查原始文件夹是否存在
boolean folderExists = false;
Iterable
> checkResults = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(oldFolder)
.build()
);
// 原文件夹存在但为空,则创建新的空文件夹,并删除原始空文件夹
if (checkResults.iterator().hasNext()) {
createFolder(bucketName, newFolder);
// 删除原始空文件夹
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(oldFolder)
.build()
);
renamedCount = 1;
} else {
throw new IllegalArgumentException("找不到指定的文件夹: " + folderPath);
}
}
return renamedCount;
} catch (Exception e) {
throw new RuntimeException("重命名文件夹失败: " + e.getMessage(), e);
}
}
/**
* 获取文件流
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 文件流
*/
@Override
public InputStream getObject(String bucketName, String objectName) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
if (StringUtils.isBlank(objectName)) {
throw new IllegalArgumentException("对象名称不能为空");
}
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 获取对象
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()
);
} catch (Exception e) {
throw new RuntimeException("获取文件失败: " + e.getMessage(), e);
}
}
/**
* 获取文件信息和文件流
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @return 包含文件信息和文件流的Map,包括stream(输入流)、size(大小)、contentType(内容类型)、lastModified(最后修改时间)等
*/
@Override
public Map getObjectInfo(String bucketName, String objectName) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new MinioException("存储桶名称不能为空");
}
if (StringUtils.isBlank(objectName)) {
throw new MinioException("对象名称不能为空");
}
// 处理路径,移除前导斜杠
String object = objectName.startsWith("/") ? objectName.substring(1) : objectName;
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new MinioException("存储桶 '" + bucketName + "' 不存在");
}
// 获取对象状态信息
StatObjectResponse stat = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(object)
.build()
);
// 获取对象数据流
InputStream stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(object)
.build()
);
// 组装结果
Map objectInfo = new HashMap<>();
objectInfo.put("stream", stream);
objectInfo.put("size", stat.size());
objectInfo.put("contentType", stat.contentType());
objectInfo.put("etag", stat.etag());
objectInfo.put("lastModified", stat.lastModified());
objectInfo.put("name", object.substring(object.lastIndexOf("/") + 1));
objectInfo.put("path", object);
objectInfo.put("url", minioConfig.getUrl() + "/" + bucketName + "/" + object);
return objectInfo;
} catch (MinioException e) {
throw e;
} catch (Exception e) {
String errorMsg = "获取文件信息失败: " + MinioUtils.getFriendlyErrorMessage(e);
throw new MinioException(errorMsg, e);
}
}
/**
* 列出指定目录下的文件和文件夹
*
* @param bucketName 存储桶名称
* @param prefix 前缀(目录路径)
* @param recursive 是否递归查询
* @return 文件和文件夹列表,以嵌套的TreeNode格式返回
*/
@Override
public List listObjects(String bucketName, String prefix, boolean recursive) {
try {
if (StringUtils.isBlank(bucketName)) {
throw new IllegalArgumentException("存储桶名称不能为空");
}
// 检查桶是否存在
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new IllegalArgumentException("存储桶 '" + bucketName + "' 不存在");
}
// 规范化前缀路径
String normalizedPrefix = processPrefix(prefix);
// 根据是否递归查询选择不同的处理方式
if (recursive) {
return listObjectsRecursive(bucketName, normalizedPrefix);
} else {
return listObjectsNonRecursive(bucketName, normalizedPrefix);
}
} catch (Exception e) {
throw new RuntimeException("列出对象失败: " + e.getMessage(), e);
}
}
/**
* 处理前缀路径
*
* @param prefix 原始前缀路径
* @return 处理后的前缀
*/
private String processPrefix(String prefix) {
// 处理特殊情况:根目录 "/"
if (prefix != null && prefix.equals("/")) {
return null;
}
// 规范化并处理前缀
String finalPrefix = "";
if (StringUtils.isNotBlank(prefix)) {
// 去除前导斜杠(MinIO不需要前导斜杠)
finalPrefix = prefix.startsWith("/") ? prefix.substring(1) : prefix;
// 确保目录以斜杠结尾
if (!finalPrefix.endsWith("/") && !finalPrefix.isEmpty()) {
finalPrefix = finalPrefix + "/";
}
}
return finalPrefix;
}
/**
* 递归模式列出对象
*/
private List listObjectsRecursive(String bucketName, String prefix) throws Exception {
// 获取对象列表
Iterable> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(true)
.build()
);
// 创建节点映射表,用于构建树
Map nodeMap = new HashMap<>();
// 处理所有对象,构建树形结构
for (Result- result : results) {
Item item = result.get();
String objectName = item.objectName();
// 跳过空对象
if (objectName.isEmpty()) {
continue;
}
// 规范化对象路径
String path = "/" + objectName;
boolean isDirectory = item.isDir() || objectName.endsWith("/");
// 如果是目录,移除末尾斜杠以便于处理
if (isDirectory && path.endsWith("/")) {
path = PathUtils.ensureNotEndWithSlash(path);
}
// 创建当前对象的节点,添加大小和时间信息
Long size = isDirectory ? 0L : item.size();
Date lastModified = isDirectory ? null : Date.from(item.lastModified().toInstant());
processPathAndCreateNodes(bucketName, path, isDirectory, nodeMap, size, lastModified);
}
// 构建父子关系并获取根节点
return buildTreeStructure(nodeMap);
}
/**
* 非递归模式列出对象
*/
private List
listObjectsNonRecursive(String bucketName, String prefix) throws Exception {
List result = new ArrayList<>();
// 使用MinIO的delimiter方式获取当前层级
Iterable> levelItems = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.delimiter("/")
.build()
);
// 处理当前层级对象
for (Result- itemResult : levelItems) {
try {
Item item = itemResult.get();
String objectName = item.objectName();
// 跳过空对象
if (objectName.isEmpty()) {
continue;
}
// 将对象名转换为路径
String path = "/" + objectName;
boolean isDirectory = item.isDir() || objectName.endsWith("/");
// 如果是目录,移除末尾斜杠
if (isDirectory && path.endsWith("/")) {
path = PathUtils.ensureNotEndWithSlash(path);
}
// 创建节点
FileTreeNode node = createNodeFromItem(bucketName, path, item);
// 直接添加到结果列表
result.add(node);
} catch (Exception e) {
log.debug("处理对象时出错: {}", e.getMessage());
// 跳过单个对象的错误,继续处理其他对象
continue;
}
}
// 如果找不到任何对象,且指定了前缀,可能是因为前缀本身就是一个文件夹
if (result.isEmpty() && StringUtils.isNotBlank(prefix)) {
// 检查前缀是否存在
try {
// 移除末尾斜杠以检查文件夹对象
String folderObject = prefix.endsWith("/") ? prefix : prefix + "/";
minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(folderObject)
.build()
);
// 如果能够执行到这里,说明文件夹存在但为空
log.debug("访问的文件夹存在但为空: {}/{}", bucketName, prefix);
} catch (Exception e) {
// 前缀不存在或无法访问
log.debug("找不到指定的前缀: {}/{}", bucketName, prefix);
}
}
return result;
}
/**
* 从MinIO的Item对象创建FileTreeNode
*/
private FileTreeNode createNodeFromItem(String bucketName, String path, Item item) {
boolean isDirectory = item.isDir() || item.objectName().endsWith("/");
// 创建基本节点
FileTreeNode node = createNode(bucketName, path, isDirectory);
// 如果不是目录,设置文件信息
if (!isDirectory) {
// 设置文件大小
node.setSize(FileUtils.formatFileSize(item.size()));
// 设置时间信息
Date lastModified = Date.from(item.lastModified().toInstant());
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedDate = dateFormat.format(lastModified);
node.setCreateTime(formattedDate);
node.setUpdateTime(formattedDate);
// 设置URL
String objectPath = path.startsWith("/") ? path.substring(1) : path;
node.setUrl(minioConfig.getUrl() + "/" + bucketName + "/" + objectPath);
} else {
// 为目录设置空的子节点列表(非递归模式)
node.setChildren(new ArrayList<>());
}
return node;
}
/**
* 处理路径并创建所有必要的节点
*/
private void processPathAndCreateNodes(String bucketName, String path, boolean isDirectory, Map
nodeMap) {
processPathAndCreateNodes(bucketName, path, isDirectory, nodeMap, null, null);
}
/**
* 处理路径并创建所有必要的节点(包含文件大小和时间信息)
*/
private void processPathAndCreateNodes(String bucketName, String path, boolean isDirectory,
Map nodeMap, Long size, Date lastModified) {
// 如果是根路径,跳过
if (path.equals("/")) {
return;
}
// 如果节点已存在,跳过
if (nodeMap.containsKey(path)) {
return;
}
// 创建当前节点
FileTreeNode node = createNode(bucketName, path, isDirectory, size, lastModified);
nodeMap.put(path, node);
// 确保所有父目录路径都存在
String parentPath = getParentPath(path);
if (parentPath == null || parentPath.equals("/")) {
return;
}
// 递归创建父路径节点
if (!nodeMap.containsKey(parentPath)) {
processPathAndCreateNodes(bucketName, parentPath, true, nodeMap);
}
}
/**
* 构建树结构,建立父子关系
*/
private List buildTreeStructure(Map nodeMap) {
List rootNodes = new ArrayList<>();
// 遍历所有节点,建立父子关系
for (Map.Entry entry : nodeMap.entrySet()) {
String path = entry.getKey();
FileTreeNode node = entry.getValue();
// 根节点直接添加到结果
String parentPath = getParentPath(path);
if (parentPath == null || parentPath.equals("/")) {
rootNodes.add(node);
} else {
// 非根节点添加到父节点的子节点列表中
FileTreeNode parentNode = nodeMap.get(parentPath);
if (parentNode != null) {
if (parentNode.getChildren() == null) {
parentNode.setChildren(new ArrayList<>());
}
if (!parentNode.getChildren().contains(node)) {
parentNode.getChildren().add(node);
parentNode.setHasChildren(true);
}
}
}
}
return rootNodes;
}
/**
* 创建文件树节点
*/
private FileTreeNode createNode(String bucketName, String path, boolean isDirectory) {
FileTreeNode node = new FileTreeNode();
node.setBucketName(bucketName);
// 提取名称
String name;
if (path.equals("/")) {
name = "/";
} else {
int lastSlashIndex = path.lastIndexOf('/');
name = lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path;
}
node.setName(name);
node.setPath(path);
node.setDirectory(isDirectory);
node.setHasChildren(isDirectory);
if (isDirectory) {
node.setChildren(new ArrayList<>());
node.setSize("0");
} else {
node.setChildren(null);
// 如果不是目录,则设置URL访问路径
String objectPath = path.startsWith("/") ? path.substring(1) : path;
node.setUrl(minioConfig.getUrl() + "/" + bucketName + "/" + objectPath);
}
return node;
}
/**
* 创建文件树节点,包含大小和时间信息
*/
private FileTreeNode createNode(String bucketName, String path, boolean isDirectory, Long size, Date lastModified) {
FileTreeNode node = createNode(bucketName, path, isDirectory);
// 设置文件大小(格式化为易读形式)
if (!isDirectory && size != null) {
node.setSize(FileUtils.formatFileSize(size));
} else {
node.setSize("-");
}
// 设置时间信息
if (lastModified != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedDate = dateFormat.format(lastModified);
node.setCreateTime(formattedDate);
node.setUpdateTime(formattedDate);
}
return node;
}
/**
* 获取父路径
*/
private String getParentPath(String path) {
if (path.equals("/")) {
return null;
}
int lastSlashIndex = path.lastIndexOf('/');
if (lastSlashIndex <= 0) {
return "/";
}
return path.substring(0, lastSlashIndex);
}
/**
* 生成默认的存储路径,格式为:/yyyy/MM/dd/
*/
private String generateDefaultPath() {
LocalDate now = LocalDate.now();
return "/" + now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}
/**
* 下载文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param response HTTP响应对象
*/
@Override
public void downloadFile(String bucketName, String objectName, HttpServletResponse response) {
try {
// 检查桶策略,如果是私有桶,需要校验当前用户是否有权限
Map policyInfo = minioBucketService.getBucketPolicy(bucketName);
boolean isPrivate = MinioConstants.BucketPolicy.PRIVATE.equals(policyInfo.get("policyType"));
// 如果是私有桶,记录访问日志
if (isPrivate) {
log.info("访问私有存储桶 {} 中的文件 {}", bucketName, objectName);
// 实际应用中,您可以在此处添加权限验证逻辑
}
// 获取文件流和元数据
Map fileInfo = getObjectInfo(bucketName, objectName);
InputStream inputStream = (InputStream) fileInfo.get("stream");
String contentType = (String) fileInfo.getOrDefault("contentType", MediaType.APPLICATION_OCTET_STREAM_VALUE);
long contentLength = (long) fileInfo.getOrDefault("size", -1L);
// 提取文件名
String fileName = objectName.substring(objectName.lastIndexOf("/") + 1);
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
// 设置响应头
response.setContentType(contentType);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName);
if (contentLength > 0) {
response.setContentLengthLong(contentLength);
}
// 将文件流写入响应
IOUtils.copy(inputStream, response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
log.error("文件下载失败: {}", e.getMessage(), e);
try {
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.getWriter().write("文件下载失败: " + e.getMessage());
} catch (IOException ex) {
log.error("写入错误响应失败", ex);
}
}
}
/**
* 预览文件
*
* @param bucketName 存储桶名称
* @param objectName 对象名称
* @param response HTTP响应对象
*/
@Override
public void previewFile(String bucketName, String objectName, HttpServletResponse response) {
try {
// 检查桶策略,如果是私有桶,需要校验当前用户是否有权限
Map policyInfo = minioBucketService.getBucketPolicy(bucketName);
boolean isPrivate = MinioConstants.BucketPolicy.PRIVATE.equals(policyInfo.get("policyType"));
// 如果是私有桶,记录访问日志
if (isPrivate) {
log.info("预览私有存储桶 {} 中的文件 {}", bucketName, objectName);
// 实际应用中,您可以在此处添加权限验证逻辑
}
// 获取文件流和元数据
Map fileInfo = getObjectInfo(bucketName, objectName);
InputStream inputStream = (InputStream) fileInfo.get("stream");
String contentType = (String) fileInfo.getOrDefault("contentType", MediaType.APPLICATION_OCTET_STREAM_VALUE);
long contentLength = (long) fileInfo.getOrDefault("size", -1L);
// 设置响应头
response.setContentType(contentType);
if (contentLength > 0) {
response.setContentLengthLong(contentLength);
}
// 将文件流写入响应
IOUtils.copy(inputStream, response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
log.error("文件预览失败: {}", e.getMessage(), e);
try {
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.getWriter().write("文件预览失败: " + e.getMessage());
} catch (java.io.IOException ex) {
log.error("写入错误响应失败", ex);
}
}
}
}
package com.ruoyi.file.minio.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MinIO对象存储配置类
*
* @author ruoyi
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/** MinIO服务地址 */
private String url;
/** MinIO访问密钥(Access Key) */
private String accessKey;
/** MinIO私有密钥(Secret Key) */
private String secretKey;
/** 默认存储桶名称 */
private String defaultBucketName;
/** 连接超时时间(单位:秒),默认为10秒 */
private int connectTimeout = 10;
/** 写入超时时间(单位:秒),默认为60秒 */
private int writeTimeout = 100;
/** 读取超时时间(单位:秒),默认为10秒 */
private int readTimeout = 20;
/** 是否自动创建默认存储桶,默认为true */
private boolean autoCreateBucket = true;
/** 默认存储桶策略,可选值:read-only(只读)、read-write(读写)、private(私有),默认为read-only */
private String defaultBucketPolicy = "read-only";
/** 是否启用图片压缩,默认不启用 */
private boolean compressEnabled = false;
/** 是否将压缩后的图片转换为webp格式(true,false) */
private boolean convertToWebp = false;
/** 压缩图片质量(0-100的整数,0代表最差,100代表最好) */
private Integer compressQuality = 30;
/** 图片压缩阈值(单位:字节),超过此大小的图片才会被压缩,默认为100KB */
private Long compressThreshold = 102400L;
/**
* 创建MinIO客户端
*
* @return MinIO客户端对象
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(url)
.credentials(accessKey, secretKey)
.build();
}
}
package com.ruoyi.file.minio.config;
/**
* MinIO常量定义类
*
* @author ruoyi
*/
public class MinioConstants {
/**
* 默认文件夹分隔符
*/
public static final String FOLDER_SEPARATOR = "/";
/**
* 默认日期格式(按年月日组织目录)
*/
public static final String DATE_FORMAT_PATH = "yyyy/MM/dd";
/**
* 默认内容类型
*/
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
/**
* 存储桶相关策略常量
*/
public static class BucketPolicy {
/**
* 公共读策略
*/
public static final String READ_ONLY = "read-only";
/**
* 公共读写策略
*/
public static final String READ_WRITE = "read-write";
/**
* 私有策略
*/
public static final String PRIVATE = "private";
}
}
package com.ruoyi.file.minio.contoller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.file.minio.exception.MinioException;
import com.ruoyi.file.minio.service.IMinioBucketService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MinIO存储桶管理控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/minio/bucket")
@Tag(name = "MinIO存储桶管理", description = "MinIO存储桶的创建、删除、策略设置等操作")
public class MinioBucketController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(MinioBucketController.class);
private final IMinioBucketService minioBucketService;
public MinioBucketController(IMinioBucketService minioBucketService) {
this.minioBucketService = minioBucketService;
}
/**
* 获取所有存储桶
*/
@GetMapping("/list")
@Schema(description = "获取MinIO中所有的存储桶及其详细信息")
@ApiResponse(responseCode = "200", description = "获取成功,返回存储桶列表")
public AjaxResult listBuckets() {
try {
List> buckets = minioBucketService.listBuckets();
return success(buckets);
} catch (MinioException e) {
log.error("获取存储桶列表失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("获取存储桶列表失败: {}", e.getMessage(), e);
return error("获取存储桶列表失败: " + e.getMessage());
}
}
/**
* 创建存储桶
*/
@PostMapping
@Schema(description = "创建MinIO存储桶,可设置访问策略类型")
@ApiResponse(responseCode = "200", description = "存储桶创建成功")
public AjaxResult createBucket(
@Parameter(description = "存储桶名称(必须符合DNS命名规范)", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "访问策略类型(read-only:只读, read-write:读写, private:私有)", schema = @Schema(defaultValue = "read-only"))
@RequestParam(value = "policyType", defaultValue = "read-only") String policyType) {
try {
minioBucketService.createBucket(bucketName, policyType);
return success("存储桶创建成功");
} catch (MinioException e) {
log.error("创建存储桶失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("创建存储桶失败: {}", e.getMessage(), e);
return error("创建存储桶失败: " + e.getMessage());
}
}
/**
* 获取存储桶访问策略
*/
@GetMapping("/policy")
@Schema(description = "获取MinIO存储桶的访问策略信息")
public AjaxResult getBucketPolicy(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName) {
try {
Map policyInfo = minioBucketService.getBucketPolicy(bucketName);
return success(policyInfo);
} catch (MinioException e) {
log.error("获取存储桶访问策略失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("获取存储桶访问策略失败: {}", e.getMessage(), e);
return error("获取存储桶访问策略失败: " + e.getMessage());
}
}
/**
* 设置存储桶的访问策略
*/
@PutMapping("/policy")
@Schema(description = "修改MinIO存储桶的访问策略")
@ApiResponse(responseCode = "200", description = "访问策略设置成功")
public AjaxResult setBucketPolicy(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "策略类型(read-only:只读, read-write:读写, private:私有)", required = true)
@RequestParam("policyType") String policyType) {
try {
minioBucketService.updateBucketPolicy(bucketName, policyType);
return success("存储桶访问策略设置成功");
} catch (MinioException e) {
log.error("设置存储桶访问策略失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("设置存储桶访问策略失败: {}", e.getMessage(), e);
return error("设置存储桶访问策略失败: " + e.getMessage());
}
}
/**
* 设置存储桶为只读访问策略
*/
@PutMapping("/policy/read-only")
@Schema(description = "设置MinIO存储桶为只读访问(公共读取权限)")
public AjaxResult setBucketReadOnlyPolicy(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName) {
try {
minioBucketService.setBucketReadOnlyPolicy(bucketName);
return success("存储桶已设置为只读访问");
} catch (MinioException e) {
log.error("设置存储桶只读策略失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("设置存储桶只读策略失败: {}", e.getMessage(), e);
return error("设置存储桶只读策略失败: " + e.getMessage());
}
}
/**
* 设置存储桶为读写访问策略
*/
@PutMapping("/policy/read-write")
@Schema(description = "设置MinIO存储桶为读写访问(公共读写权限)")
public AjaxResult setBucketReadWritePolicy(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName) {
try {
minioBucketService.setBucketReadWritePolicy(bucketName);
return success("存储桶已设置为读写访问");
} catch (MinioException e) {
log.error("设置存储桶读写策略失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("设置存储桶读写策略失败: {}", e.getMessage(), e);
return error("设置存储桶读写策略失败: " + e.getMessage());
}
}
/**
* 设置存储桶为私有访问策略
*/
@PutMapping("/policy/private")
@Schema(description = "设置MinIO存储桶为私有访问(仅授权用户可访问)")
public AjaxResult setBucketPrivatePolicy(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName) {
try {
minioBucketService.setBucketPrivatePolicy(bucketName);
return success("存储桶已设置为私有访问");
} catch (MinioException e) {
log.error("设置存储桶私有策略失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("设置存储桶私有策略失败: {}", e.getMessage(), e);
return error("设置存储桶私有策略失败: " + e.getMessage());
}
}
/**
* 获取存储桶统计信息
*/
@GetMapping("/stats")
@Schema(description = "获取MinIO存储桶的对象数量和存储大小等统计信息")
public AjaxResult getBucketStats(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName) {
try {
Map stats = minioBucketService.getBucketStats(bucketName);
return success(stats);
} catch (MinioException e) {
log.error("获取存储桶统计信息失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("获取存储桶统计信息失败: {}", e.getMessage(), e);
return error("获取存储桶统计信息失败: " + e.getMessage());
}
}
/**
* 删除存储桶
*/
@DeleteMapping
@Schema(description = "删除MinIO存储桶,可选择是否递归删除非空桶")
@ApiResponse(responseCode = "200", description = "存储桶删除成功")
public AjaxResult deleteBucket(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "是否递归删除(true表示先清空桶再删除,false表示桶不为空则报错)", schema = @Schema(defaultValue = "false"))
@RequestParam(value = "recursive", defaultValue = "false") boolean recursive) {
try {
minioBucketService.deleteBucket(bucketName, recursive);
return success("存储桶删除成功");
} catch (MinioException e) {
log.error("删除存储桶失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("删除存储桶失败: {}", e.getMessage(), e);
return error("删除存储桶失败: " + e.getMessage());
}
}
/**
* 测试MinIO连接状态
*/
@GetMapping("/connection/test")
@Schema(description = "测试MinIO连接状态,检查应用是否能成功连接到MinIO服务器")
@ApiResponse(responseCode = "200", description = "返回连接测试结果")
public AjaxResult testConnection() {
try {
long startTime = System.currentTimeMillis();
// 尝试执行一个基本操作来测试连接
minioBucketService.listBuckets();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
Map result = new HashMap<>();
result.put("status", "success");
result.put("message", "成功连接到MinIO服务");
result.put("responseTime", responseTime + "ms");
return success(result);
} catch (MinioException e) {
log.error("MinIO连接测试失败: {}", e.getMessage(), e);
Map result = new HashMap<>();
result.put("status", "failed");
result.put("message", "MinIO连接失败: " + e.getMessage());
result.put("error", e.getMessage());
return AjaxResult.error("MinIO连接失败", result);
} catch (Exception e) {
log.error("MinIO连接测试失败: {}", e.getMessage(), e);
Map result = new HashMap<>();
result.put("status", "failed");
result.put("message", "MinIO连接失败: " + e.getMessage());
result.put("error", e.getMessage());
return AjaxResult.error("MinIO连接失败", result);
}
}
}
package com.ruoyi.file.minio.contoller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.file.minio.exception.MinioException;
import com.ruoyi.file.minio.service.IMinioFileService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* MinIO文件管理控制器
*
* @author ruoyi
*/
@RestController
@RequestMapping("/minio/file")
@Tag(name = "MinIO文件管理", description = "MinIO文件上传、下载、删除、列表等操作")
public class MinioFileController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(MinioFileController.class);
private final IMinioFileService minioFileService;
public MinioFileController(IMinioFileService minioFileService) {
this.minioFileService = minioFileService;
}
/**
* 上传文件
*/
@PostMapping("/upload")
@Schema(description = "上传文件至MinIO存储,支持指定存储桶和对象路径")
@ApiResponse(responseCode = "200", description = "上传成功,返回文件访问信息")
public AjaxResult uploadFile(
@Parameter(description = "要上传的文件", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "存储桶名称,不指定则使用默认桶")
@RequestParam(value = "bucketName", required = false) String bucketName,
@Parameter(description = "对象存储路径,不指定则使用默认日期路径")
@RequestParam(value = "objectPath", required = false) String objectPath) {
try {
Map result = minioFileService.uploadFile(file, bucketName, objectPath);
return success(result);
} catch (MinioException e) {
log.error("文件上传失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
return error("文件上传失败: " + e.getMessage());
}
}
/**
* 创建文件夹
*/
@PostMapping("/folder")
@Schema(description = "在MinIO存储中创建文件夹结构")
@ApiResponse(responseCode = "200", description = "文件夹创建成功")
public AjaxResult createFolder(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "文件夹路径", required = true)
@RequestParam("folderPath") String folderPath) {
try {
minioFileService.createFolder(bucketName, folderPath);
return success("文件夹创建成功");
} catch (MinioException e) {
log.error("创建文件夹失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("创建文件夹失败: {}", e.getMessage(), e);
return error("创建文件夹失败: " + e.getMessage());
}
}
/**
* 删除文件
*/
@DeleteMapping
@Schema(description = "删除MinIO存储中的指定文件")
@ApiResponse(responseCode = "200", description = "文件删除成功")
public AjaxResult deleteFile(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "对象名称/路径", required = true)
@RequestParam("objectName") String objectName) {
try {
minioFileService.deleteFile(bucketName, objectName);
return success("文件删除成功");
} catch (MinioException e) {
log.error("删除文件失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("删除文件失败: {}", e.getMessage(), e);
return error("删除文件失败: " + e.getMessage());
}
}
/**
* 删除文件夹
*/
@DeleteMapping("/folder")
@Schema(description = "删除MinIO存储中的文件夹及其内容")
@ApiResponse(responseCode = "200", description = "文件夹删除成功")
public AjaxResult deleteFolder(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "文件夹路径", required = true)
@RequestParam("folderPath") String folderPath,
@Parameter(description = "是否递归删除", schema = @Schema(defaultValue = "false"))
@RequestParam(value = "recursive", defaultValue = "false") boolean recursive) {
try {
minioFileService.deleteFolder(bucketName, folderPath, recursive);
return success("文件夹删除成功");
} catch (MinioException e) {
log.error("删除文件夹失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("删除文件夹失败: {}", e.getMessage(), e);
return error("删除文件夹失败: " + e.getMessage());
}
}
/**
* 重命名文件
*/
@PutMapping
@Schema(description = "重命名MinIO存储中的文件")
@ApiResponse(responseCode = "200", description = "文件重命名成功")
public AjaxResult renameFile(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "原对象名称/路径", required = true)
@RequestParam("objectName") String objectName,
@Parameter(description = "新对象名称/路径", required = true)
@RequestParam("newObjectName") String newObjectName) {
try {
minioFileService.renameFile(bucketName, objectName, newObjectName);
return success("文件重命名成功");
} catch (MinioException e) {
log.error("重命名文件失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("重命名文件失败: {}", e.getMessage(), e);
return error("重命名文件失败: " + e.getMessage());
}
}
/**
* 重命名文件夹
*/
@PutMapping("/folder")
@Schema(description = "重命名MinIO存储中的文件夹")
@ApiResponse(responseCode = "200", description = "文件夹重命名成功,返回处理对象数量")
public AjaxResult renameFolder(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "原文件夹路径", required = true)
@RequestParam("folderPath") String folderPath,
@Parameter(description = "新文件夹路径", required = true)
@RequestParam("newFolderPath") String newFolderPath) {
try {
int count = minioFileService.renameFolder(bucketName, folderPath, newFolderPath);
return success("文件夹重命名成功,共处理 " + count + " 个对象");
} catch (MinioException e) {
log.error("重命名文件夹失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("重命名文件夹失败: {}", e.getMessage(), e);
return error("重命名文件夹失败: " + e.getMessage());
}
}
/**
* 下载文件
*/
@GetMapping("/download")
@Schema(description = "从MinIO存储下载指定文件")
public void downloadFile(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "对象名称/路径", required = true)
@RequestParam("objectName") String objectName,
HttpServletResponse response) {
minioFileService.downloadFile(bucketName, objectName, response);
}
/**
* 预览文件
*/
@GetMapping("/preview")
@Schema(description = "预览MinIO存储中的文件,直接输出文件内容而不是附件形式")
public void previewFile(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "对象名称/路径", required = true)
@RequestParam("objectName") String objectName,
HttpServletResponse response) {
minioFileService.previewFile(bucketName, objectName, response);
}
/**
* 列出文件和文件夹
*/
@GetMapping("/list")
@Schema(description = "列出MinIO存储中指定路径下的文件和文件夹")
@ApiResponse(responseCode = "200", description = "获取列表成功,返回文件和文件夹集合")
public AjaxResult listObjects(
@Parameter(description = "存储桶名称", required = true)
@RequestParam("bucketName") String bucketName,
@Parameter(description = "前缀/目录路径")
@RequestParam(value = "prefix", required = false) String prefix,
@Parameter(description = "是否递归查询", schema = @Schema(defaultValue = "false"))
@RequestParam(value = "recursive", defaultValue = "false") boolean recursive) {
try {
return success(minioFileService.listObjects(bucketName, prefix, recursive));
} catch (MinioException e) {
log.error("列出对象失败: {}", e.getMessage(), e);
return error(e.getMessage());
} catch (Exception e) {
log.error("列出对象失败: {}", e.getMessage(), e);
return error("列出对象失败: " + e.getMessage());
}
}
}
MinIO文件管理系统
存储服务:{{ connectionStatus.text }}
存储桶:
{{ bucketName }}
请选择存储桶
当前路径:
{{ segment }}
/
根目录
上传文件
新建文件夹
返回上级
{{ scope.row.name }}
{{ scope.row.directory ? '文件夹' : '文件' }}
{{ scope.row.size || '-' }}
{{ scope.row.createTime || '-' }}
{{ scope.row.updateTime || '-' }}
文件夹为空
拖拽文件到此处或 点击上传
存储桶管理
新建存储桶
{{ bucket.name }}
暂无存储桶