springboot3集成minio

1.说明

注意:本代码是在若依springboot3版本上实现的,如果你不是在若依上面实现,需要将所有用到若依的相关代码修改后才能运行

文件管理

  • 文件上传:支持单文件上传,可指定存储桶和路径,支持自动按日期目录存储
  • 文件下载:支持文件直接下载,自动处理文件名编码
  • 文件预览:支持图片、文档等文件的在线预览功能
  • 文件删除:支持单文件删除和批量删除
  • 文件重命名:支持文件重命名操作
  • 图片处理:支持图片压缩和格式转换(WebP),可配置压缩质量

文件夹管理

  • 创建文件夹:支持创建多级文件夹结构
  • 删除文件夹:支持递归删除文件夹及其内容
  • 重命名文件夹:支持文件夹重命名,自动处理路径下所有对象
  • 文件列表:支持按目录层级展示文件和文件夹,可选择递归展示

存储桶管理

  • 桶创建:支持创建存储桶,自动校验桶名合法性
  • 桶删除:支持删除存储桶,可选择强制删除非空桶
  • 策略设置:支持设置桶的访问策略(只读/读写/私有)
  • 桶统计:提供桶内对象数量和存储容量等统计信息

系统特性

  • 异常处理:规范化的异常处理机制,友好的错误提示
  • 参数校验:完善的参数校验,确保数据安全
  • 连接管理:支持连接超时设置和连接状态检测
  • 文件验证:支持文件类型和大小限制验证

架构设计

分层架构

  • Controller层:提供RESTful API接口,处理请求参数和响应
  • Service层:实现业务逻辑,处理核心功能
  • Config层:负责模块配置和客户端初始化
  • Util层:提供通用工具方法
  • Exception层:自定义异常处理
  • Domain层:定义数据模型

核心类说明

配置类
  • MinioConfig:MinIO服务配置类,负责客户端初始化和配置参数定义
  • MinioConstants:MinIO相关常量定义,包括文件分隔符、日期格式等
控制层
  • MinioFileController:文件操作控制器,提供文件上传、下载、预览、删除等接口
  • MinioBucketController:存储桶管理控制器,提供桶创建、策略设置、统计信息等接口
服务层
  • IMinioFileService:文件服务接口,定义文件操作相关方法
  • IMinioBucketService:存储桶服务接口,定义桶管理相关方法
  • MinioFileServiceImpl:文件服务实现,包含文件处理核心逻辑
  • MinioBucketServiceImpl:存储桶服务实现,包含桶管理核心逻辑
工具类
  • MinioUtils:MinIO操作辅助工具类,提供桶名验证、错误信息处理等功能
异常类
  • MinioException:自定义MinIO异常类,统一异常处理
领域类
  • FileTreeNode:文件树节点类,用于文件列表展示

API接口说明

文件接口 (/minio/file/*)

  • POST /upload:上传文件
  • POST /folder:创建文件夹
  • DELETE /:删除文件
  • DELETE /folder:删除文件夹
  • PUT /:重命名文件
  • PUT /folder:重命名文件夹
  • GET /download:下载文件
  • GET /preview:预览文件
  • GET /list:列出文件和文件夹

存储桶接口 (/minio/bucket/*)

  • GET /list:获取所有存储桶
  • POST /:创建存储桶
  • DELETE /:删除存储桶
  • GET /policy:获取存储桶访问策略
  • PUT /policy:设置存储桶访问策略
  • PUT /policy/read-only:设置存储桶为只读访问
  • PUT /policy/read-write:设置存储桶为读写访问
  • PUT /policy/private:设置存储桶为私有访问
  • GET /stats:获取存储桶统计信息
  • GET /connection/test:测试MinIO连接状态

配置说明

在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

2.导入依赖


    io.minio
    minio
    8.5.2


 

    org.sejda.imageio
    webp-imageio
    0.4.18




    net.coobird
    thumbnailator
    0.1.6

3.相关代码

1.FileUtils

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]);
    }

} 

2.ImageCompressUtils

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();
        }
    }
}

3.PathUtils

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, "/");
    }
}

4.FileTreeNode

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;
}

5.MinioException

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;
    }
}

6.MinioUtils

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();
    }

} 

7.IMinioFileService

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);
}

8.IMinioBucketService

package com.ruoyi.file.minio.service;

import java.util.List;
import java.util.Map;

/**
 * MinIO存储桶管理服务接口
 *
 * @author ruoyi
 */
public interface IMinioBucketService {

    /**
     * 获取所有存储桶列表
     *
     * @return 存储桶信息列表,每个桶包含名称、创建时间和访问策略等信息
     */
    List> listBuckets();

    /**
     * 检查存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 如果存在返回true,否则返回false
     */
    boolean bucketExists(String bucketName);

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称(必须符合DNS命名规范)
     * @param isPublic   是否为公共访问桶(true表示可公开读取,false表示私有)
     */
    void createBucket(String bucketName, Boolean isPublic);

    /**
     * 创建存储桶并设置指定的策略类型
     *
     * @param bucketName  存储桶名称(必须符合DNS命名规范)
     * @param policyType  策略类型:"read-only"(只读), "read-write"(读写), "private"(私有), "write-only"(只写)
     */
    void createBucket(String bucketName, String policyType);

    /**
     * 获取存储桶访问策略
     *
     * @param bucketName 存储桶名称
     * @return 存储桶策略信息映射
     */
    Map getBucketPolicy(String bucketName);

    /**
     * 修改存储桶访问策略
     *
     * @param bucketName 存储桶名称
     * @param isPublic   是否为公共访问桶(true表示可公开读取,false表示私有)
     * @deprecated 使用 {@link #updateBucketPolicy(String, String)} 代替
     */
    @Deprecated
    void updateBucket(String bucketName, Boolean isPublic);

    /**
     * 更新存储桶访问策略
     *
     * @param bucketName 存储桶名称
     * @param policyType 策略类型:"read-only"(只读), "read-write"(读写), "private"(私有), "write-only"(只写)
     */
    void updateBucketPolicy(String bucketName, String policyType);

    /**
     * 设置存储桶为只读访问策略
     *
     * @param bucketName 存储桶名称
     */
    void setBucketReadOnlyPolicy(String bucketName);

    /**
     * 设置存储桶为读写访问策略
     *
     * @param bucketName 存储桶名称
     */
    void setBucketReadWritePolicy(String bucketName);

    /**
     * 设置存储桶为私有访问策略
     *
     * @param bucketName 存储桶名称
     */
    void setBucketPrivatePolicy(String bucketName);

    /**
     * 设置存储桶为只写访问策略
     *
     * @param bucketName 存储桶名称
     */
    void setBucketWriteOnlyPolicy(String bucketName);

    /**
     * 删除存储桶
     *
     * @param bucketName 存储桶名称
     * @param recursive  是否递归删除非空桶,true-先清空桶再删除,false-如果桶不为空则抛出异常
     */
    void deleteBucket(String bucketName, boolean recursive);

    /**
     * 获取存储桶统计信息
     *
     * @param bucketName 存储桶名称
     * @return 统计信息,包含对象数量、总大小等
     */
    Map getBucketStats(String bucketName);
}

9.MinioBucketServiceImpl

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> listBuckets() {
        try {
            List> bucketList = new ArrayList<>();
            List buckets = minioClient.listBuckets();

            for (Bucket bucket : buckets) {
                Map bucketInfo = new HashMap<>();
                bucketInfo.put("name", bucket.name());
                bucketInfo.put("creationTime", bucket.creationDate());

                // 获取桶策略
                Map policyInfo = getBucketPolicy(bucket.name());
                bucketInfo.put("policyType", policyInfo.get("policyType"));
                // 获取桶统计信息
                try {
                    Map stats = getBucketStats(bucket.name());
                    bucketInfo.put("objectCount", stats.get("objectCount"));
                    bucketInfo.put("size", stats.get("size"));
                } catch (Exception e) {
                    log.warn("获取桶 {} 统计信息失败: {}", bucket.name(), e.getMessage());
                    bucketInfo.put("objectCount", 0);
                    bucketInfo.put("size", 0);
                }

                bucketList.add(bucketInfo);
            }

            return bucketList;
        } catch (Exception e) {
            String errorMsg = "获取桶列表失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 检查存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 如果存在返回true,否则返回false
     */
    @Override
    public boolean bucketExists(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "检查桶是否存在失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称(必须符合DNS命名规范)
     * @param isPublic   是否为公共访问桶(true表示可公开读取,false表示私有)
     */
    @Override
    public void createBucket(String bucketName, Boolean isPublic) {
        String policyType = (isPublic != null && isPublic) ? 
                MinioConstants.BucketPolicy.READ_ONLY : MinioConstants.BucketPolicy.PRIVATE;
        createBucket(bucketName, policyType);
    }

    /**
     * 创建存储桶并设置指定的策略类型
     *
     * @param bucketName  存储桶名称(必须符合DNS命名规范)
     * @param policyType  策略类型:"read-only"(只读), "read-write"(读写), "private"(私有), "write-only"(只写)
     */
    @Override
    public void createBucket(String bucketName, String policyType) {
        try {
            // 验证桶名称
            MinioUtils.checkBucketName(bucketName);

            // 检查桶是否已存在
            boolean exists = bucketExists(bucketName);
            if (exists) {
                throw new MinioException("存储桶 '" + bucketName + "' 已存在");
            }

            // 创建桶
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            log.info("成功创建存储桶: {}", bucketName);

            // 设置桶访问策略
            updateBucketPolicy(bucketName, policyType);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "创建存储桶失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 修改存储桶访问策略
     *
     * @param bucketName 存储桶名称
     * @param isPublic   是否为公共访问桶(true表示可公开读取,false表示私有)
     * @deprecated 使用更灵活的updateBucketPolicy方法代替
     */
    @Override
    @Deprecated
    public void updateBucket(String bucketName, Boolean isPublic) {
        String policyType = (isPublic != null && isPublic) ? 
                MinioConstants.BucketPolicy.READ_ONLY : MinioConstants.BucketPolicy.PRIVATE;
        updateBucketPolicy(bucketName, policyType);
    }

    /**
     * 更新存储桶访问策略
     * 
     * @param bucketName 存储桶名称
     * @param policyType 策略类型:"read-only"(只读), "read-write"(读写), "private"(私有), "write-only"(只写)
     */
    @Override
    public void updateBucketPolicy(String bucketName, String policyType) {
        try {
            MinioUtils.checkBucketName(bucketName);
            
            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }
            
            // 根据策略类型应用相应的策略
            switch (policyType) {
                case MinioConstants.BucketPolicy.READ_ONLY:
                    setBucketReadOnlyPolicy(bucketName);
                    break;
                case MinioConstants.BucketPolicy.READ_WRITE:
                    setBucketReadWritePolicy(bucketName);
                    break;
                case MinioConstants.BucketPolicy.PRIVATE:
                default:
                    setBucketPrivatePolicy(bucketName);
                    break;
            }
            
            log.info("成功更新存储桶 {} 的访问策略为: {}", bucketName, policyType);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "更新桶访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 设置存储桶为只写访问策略
     * 
     * @param bucketName 存储桶名称
     */
    @Override
    public void setBucketWriteOnlyPolicy(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);
            
            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }
            
            // 设置只写策略(允许上传但不允许下载)
            String policy = "{\n" +
                    "    \"Version\": \"2012-10-17\",\n" +
                    "    \"Statement\": [\n" +
                    "        {\n" +
                    "            \"Effect\": \"Allow\",\n" +
                    "            \"Principal\": {\"AWS\": [\"*\"]},\n" +
                    "            \"Action\": [\"s3:PutObject\"],\n" +
                    "            \"Resource\": [\"arn:aws:s3:::" + bucketName + "/*\"]\n" +
                    "        }\n" +
                    "    ]\n" +
                    "}";

            minioClient.setBucketPolicy(
                    SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(policy)
                            .build()
            );
            
            log.info("成功设置存储桶 {} 为只写访问策略", bucketName);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "设置只写访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 获取存储桶访问策略
     *
     * @param bucketName 存储桶名称
     * @return 存储桶策略信息映射
     */
    @Override
    public Map getBucketPolicy(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);

            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }

            Map policyInfo = new HashMap<>();
            String policyType = MinioConstants.BucketPolicy.PRIVATE;
            boolean isPublic = false;

            try {
                String policy = minioClient.getBucketPolicy(
                        GetBucketPolicyArgs.builder().bucket(bucketName).build()
                );

                if (StringUtils.isNotEmpty(policy)) {
                    // 判断策略类型
                    if (policy.contains("\"Effect\":\"Allow\"")) {
                        isPublic = true;

                        // 区分只读和读写
                        if (policy.contains("\"s3:GetObject\"")) {
                            if (!policy.contains("\"s3:PutObject\"") && !policy.contains("\"s3:DeleteObject\"")) {
                                policyType = MinioConstants.BucketPolicy.READ_ONLY;
                            } else if (policy.contains("\"s3:PutObject\"") || policy.contains("\"s3:DeleteObject\"")) {
                                policyType = MinioConstants.BucketPolicy.READ_WRITE;
                            }
                        } else if (policy.contains("\"s3:PutObject\"") && !policy.contains("\"s3:GetObject\"")) {
                            // 只写策略
                            policyType = "write-only";
                        }
                    }
                    
                    // 记录下日志以便调试
                    log.debug("桶 {} 的策略内容: {}", bucketName, policy);
                    log.debug("桶 {} 的识别策略类型: {}", bucketName, policyType);
                }
            } catch (Exception e) {
                // 没有策略则默认为私有
                log.warn("获取桶 {} 策略失败,将视为私有桶: {}", bucketName, e.getMessage());
            }

            policyInfo.put("isPublic", isPublic);
            policyInfo.put("policyType", policyType);
            policyInfo.put("bucketName", bucketName);

            return policyInfo;
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "获取桶访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 设置存储桶为只读访问策略
     */
    @Override
    public void setBucketReadOnlyPolicy(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);
            
            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }
            
            // 简化的只读策略,只设置允许读取的权限,不使用Deny语句
            String policy = "{\n" +
                    "    \"Version\": \"2012-10-17\",\n" +
                    "    \"Statement\": [\n" +
                    "        {\n" +
                    "            \"Effect\": \"Allow\",\n" +
                    "            \"Principal\": {\"AWS\": [\"*\"]},\n" +
                    "            \"Action\": [\"s3:GetObject\", \"s3:ListBucket\"],\n" +
                    "            \"Resource\": [\"arn:aws:s3:::" + bucketName + "/*\", \"arn:aws:s3:::" + bucketName + "\"]\n" +
                    "        }\n" +
                    "    ]\n" +
                    "}";

            minioClient.setBucketPolicy(
                    SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(policy)
                            .build()
            );
            
            log.info("成功设置存储桶 {} 为只读访问策略", bucketName);
            
            // 验证策略是否设置成功
            String resultPolicy = minioClient.getBucketPolicy(
                    GetBucketPolicyArgs.builder().bucket(bucketName).build()
            );
            log.debug("设置后的桶 {} 策略内容: {}", bucketName, resultPolicy);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "设置只读访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 设置存储桶为读写访问策略
     */
    @Override
    public void setBucketReadWritePolicy(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);

            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }

            String policy = "{\n" +
                    "    \"Version\": \"2012-10-17\",\n" +
                    "    \"Statement\": [\n" +
                    "        {\n" +
                    "            \"Effect\": \"Allow\",\n" +
                    "            \"Principal\": {\"AWS\": [\"*\"]},\n" +
                    "            \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\"],\n" +
                    "            \"Resource\": [\"arn:aws:s3:::" + bucketName + "/*\"]\n" +
                    "        }\n" +
                    "    ]\n" +
                    "}";

            minioClient.setBucketPolicy(
                    SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(policy)
                            .build()
            );

            log.info("成功设置存储桶 {} 为读写访问策略", bucketName);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "设置读写访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 设置存储桶为私有访问策略
     */
    @Override
    public void setBucketPrivatePolicy(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);
            
            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }
            
            // 使用显式拒绝的私有策略,显式拒绝所有操作
            String policy = "{\n" +
                    "    \"Version\": \"2012-10-17\",\n" +
                    "    \"Statement\": [\n" +
                    "        {\n" +
                    "            \"Effect\": \"Deny\",\n" +
                    "            \"Principal\": {\"AWS\": [\"*\"]},\n" +
                    "            \"Action\": [\"s3:GetObject\", \"s3:ListBucket\", \"s3:PutObject\", \"s3:DeleteObject\"],\n" +
                    "            \"Resource\": [\"arn:aws:s3:::" + bucketName + "/*\", \"arn:aws:s3:::" + bucketName + "\"]\n" +
                    "        }\n" +
                    "    ]\n" +
                    "}";
            
            minioClient.setBucketPolicy(
                    SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(policy)
                            .build()
            );
            
            log.info("成功设置存储桶 {} 为私有访问策略", bucketName);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "设置私有访问策略失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 删除存储桶
     */
    @Override
    public void deleteBucket(String bucketName, boolean recursive) {
        try {
            MinioUtils.checkBucketName(bucketName);

            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }

            // 检查桶是否为空并收集对象名称
            Iterable> objects = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(bucketName)
                            .recursive(true)
                            .build()
            );

            List objectNames = new ArrayList<>();
            boolean isEmpty = true;

            // 收集所有对象名称
            for (Result result : objects) {
                Item item = result.get();
                isEmpty = false;
                objectNames.add(item.objectName());
            }

            // 处理非空桶
            if (!isEmpty) {
                if (!recursive) {
                    // 非递归模式,桶不为空则报错
                    throw new MinioException("存储桶 '" + bucketName + "' 不为空,无法删除。请先清空桶或使用递归删除选项。");
                } else {
                    // 递归模式:删除桶内所有对象
                    log.info("递归删除存储桶 {} 中的 {} 个对象", bucketName, objectNames.size());
                    for (String objectName : objectNames) {
                        minioClient.removeObject(
                                RemoveObjectArgs.builder()
                                        .bucket(bucketName)
                                        .object(objectName)
                                        .build()
                        );
                    }
                }
            }

            // 删除桶
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
            log.info("成功删除存储桶: {}", bucketName);
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "删除存储桶失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 获取存储桶统计信息
     *
     * @param bucketName 存储桶名称
     * @return 统计信息,包含对象数量、总大小等
     */
    @Override
    public Map getBucketStats(String bucketName) {
        try {
            MinioUtils.checkBucketName(bucketName);

            // 检查桶是否存在
            if (!bucketExists(bucketName)) {
                throw new MinioException("存储桶 '" + bucketName + "' 不存在");
            }

            Map stats = new HashMap<>();
            long objectCount = 0;
            long totalSize = 0;

            // 获取所有对象进行统计
            Iterable> objects = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(bucketName)
                            .recursive(true)
                            .build()
            );

            // 计算对象数量和总大小
            for (Result result : objects) {
                Item item = result.get();
                objectCount++;
                totalSize += item.size();
            }

            stats.put("objectCount", objectCount);
            stats.put("size", totalSize);
            stats.put("sizeHuman", formatSize(totalSize));

            return stats;
        } catch (MinioException e) {
            throw e;
        } catch (Exception e) {
            String errorMsg = "获取存储桶统计信息失败: " + MinioUtils.getFriendlyErrorMessage(e);
            log.error(errorMsg, e);
            throw new MinioException(errorMsg, e);
        }
    }

    /**
     * 格式化文件大小为人类可读格式
     *
     * @param size 文件大小(字节)
     * @return 格式化后的大小
     */
    private String formatSize(long size) {
        if (size <= 0) {
            return "0 B";
        }

        final String[] units = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB"};
        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));

        return String.format("%.2f %s", size / Math.pow(1024, digitGroups), units[digitGroups]);
    }
}

10.MinioFileServiceImpl

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);
            }
        }
    }
}

11.MinioConfig

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();
    }
}

12.MinioConstants

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";

    }
} 

13.MinioBucketController

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);
        }
    }
}

14.MinioFileController

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());
        }
    }
}

4.前端vue代码

1.index.vue





 

2.file-list.vue






3.bucket.vue





 

你可能感兴趣的:(springboot3集成minio)