package com.ruoyi.common.utils; 
 | 
  
 | 
import com.baomidou.mybatisplus.core.toolkit.IdWorker; 
 | 
import com.baomidou.mybatisplus.core.toolkit.StringUtils; 
 | 
import com.ruoyi.common.exception.UtilException; 
 | 
import com.ruoyi.common.exception.file.InvalidExtensionException; 
 | 
import com.ruoyi.common.utils.file.FileUploadUtils; 
 | 
import com.ruoyi.common.utils.file.MimeTypeUtils; 
 | 
import com.ruoyi.framework.web.domain.MinioResult; 
 | 
import io.minio.*; 
 | 
import io.minio.http.Method; 
 | 
import io.minio.messages.DeleteError; 
 | 
import io.minio.messages.DeleteObject; 
 | 
import lombok.Getter; 
 | 
import org.springframework.beans.factory.annotation.Autowired; 
 | 
import org.springframework.beans.factory.annotation.Value; 
 | 
import org.springframework.stereotype.Component; 
 | 
import org.springframework.util.FastByteArrayOutputStream; 
 | 
import org.springframework.web.multipart.MultipartFile; 
 | 
  
 | 
import javax.servlet.ServletOutputStream; 
 | 
import javax.servlet.http.HttpServletResponse; 
 | 
import java.io.InputStream; 
 | 
import java.net.URLEncoder; 
 | 
import java.nio.charset.StandardCharsets; 
 | 
import java.text.SimpleDateFormat; 
 | 
import java.util.*; 
 | 
import java.util.concurrent.TimeUnit; 
 | 
import java.util.stream.Collectors; 
 | 
  
 | 
@Component 
 | 
public class MinioUtils { 
 | 
  
 | 
    @Autowired 
 | 
    private MinioClient minioClient; 
 | 
  
 | 
    @Value("${minio.preview-expiry}") 
 | 
    private Integer previewExpiry; 
 | 
  
 | 
    /** 
 | 
     * -- GETTER -- 
 | 
     *  获取默认存储桶名称 
 | 
     * 
 | 
     * @return 
 | 
     */ 
 | 
    @Getter 
 | 
    @Value("${minio.default-bucket}") 
 | 
    private String defaultBucket; 
 | 
  
 | 
    /** 
 | 
     * 判断存储桶是否存在,不存在则创建 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     */ 
 | 
    public void existBucket(String bucketName) { 
 | 
        try { 
 | 
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); 
 | 
            if (!exists) { 
 | 
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); 
 | 
            } 
 | 
        } catch (Exception e) { 
 | 
            e.printStackTrace(); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 创建存储桶 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @return 是否创建成功 
 | 
     */ 
 | 
    public Boolean makeBucket(String bucketName) { 
 | 
        try { 
 | 
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); 
 | 
        } catch (Exception e) { 
 | 
            e.printStackTrace(); 
 | 
            return false; 
 | 
        } 
 | 
        return true; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 删除存储桶 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @return 是否删除成功 
 | 
     */ 
 | 
    public Boolean removeBucket(String bucketName) { 
 | 
        try { 
 | 
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); 
 | 
        } catch (Exception e) { 
 | 
            e.printStackTrace(); 
 | 
            return false; 
 | 
        } 
 | 
        return true; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 判断对象是否存在 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param originalFileName MinIO中存储对象全路径 
 | 
     * @return 对象是否存在 
 | 
     */ 
 | 
    public boolean existObject(String bucketName, String originalFileName) { 
 | 
        try { 
 | 
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(originalFileName).build()); 
 | 
        } catch (Exception e) { 
 | 
            e.printStackTrace(); 
 | 
            return false; 
 | 
        } 
 | 
        return true; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 文件上传 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param file       文件 
 | 
     * @return 桶中位置 
 | 
     */ 
 | 
    public MinioResult upload(String bucketName, MultipartFile file, Boolean isPreviewExpiry) throws InvalidExtensionException { 
 | 
        MultipartFile[] fileArr = {file}; 
 | 
        List<MinioResult> fileNames = upload(bucketName, fileArr, isPreviewExpiry); 
 | 
        return fileNames.isEmpty() ? null : fileNames.get(0); 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 上传文件 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param fileList   文件列表 
 | 
     * @return 桶中位置列表 
 | 
     */ 
 | 
    public List<MinioResult> upload(String bucketName, List<MultipartFile> fileList, Boolean isPreviewExpiry) throws InvalidExtensionException { 
 | 
        MultipartFile[] fileArr = fileList.toArray(new MultipartFile[0]); 
 | 
        return upload(bucketName, fileArr, isPreviewExpiry); 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * description: 上传文件 
 | 
     * 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param fileArr    文件列表 
 | 
     * @return 桶中位置列表 
 | 
     */ 
 | 
    public List<MinioResult> upload(String bucketName, MultipartFile[] fileArr, Boolean isPreviewExpiry) throws InvalidExtensionException { 
 | 
        for (MultipartFile file : fileArr) { 
 | 
            FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); 
 | 
        } 
 | 
        // 保证桶一定存在 
 | 
        existBucket(bucketName); 
 | 
        // 执行正常操作 
 | 
        List<MinioResult> bucketFileNames = new ArrayList<>(fileArr.length); 
 | 
        for (MultipartFile file : fileArr) { 
 | 
            // 获取原始文件名称 
 | 
            String originalFileName = file.getOriginalFilename(); 
 | 
            // 获取当前日期,格式例如:2020-11 
 | 
            String datePath = new SimpleDateFormat("yyyy-MM").format(new Date()); 
 | 
            // 文件名称 
 | 
            String uuid = IdWorker.get32UUID(); 
 | 
            // 获取文件后缀 
 | 
            String suffix = originalFileName.substring(originalFileName.lastIndexOf(".")); 
 | 
            String bucketFilePath = datePath + "/" + uuid + suffix; 
 | 
  
 | 
            // 推送文件到MinIO 
 | 
            try (InputStream in = file.getInputStream()) { 
 | 
                minioClient.putObject(PutObjectArgs.builder() 
 | 
                        .bucket(bucketName) 
 | 
                        .object(bucketFilePath) 
 | 
                        .stream(in, in.available(), -1) 
 | 
                        .contentType(file.getContentType()) 
 | 
                        .build() 
 | 
                ); 
 | 
            } catch (Exception e) { 
 | 
                throw new UtilException("MinioUtils:上传文件工具类异常:" + e); 
 | 
            } 
 | 
            MinioResult minioResult = new MinioResult(); 
 | 
            minioResult.setBucketFileName(bucketFilePath); 
 | 
            // 返回永久预览地址 
 | 
            if (isPreviewExpiry) { 
 | 
                String previewUrl = getPreviewUrl(bucketFilePath, bucketName, isPreviewExpiry); 
 | 
                minioResult.setPreviewExpiry(previewUrl); 
 | 
            } 
 | 
            minioResult.setOriginalName(originalFileName); 
 | 
            bucketFileNames.add(minioResult); 
 | 
        } 
 | 
        return bucketFileNames; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 文件下载 
 | 
     * 
 | 
     * @param bucketName       存储桶名称 
 | 
     * @param bucketFileName   桶中文件名称 
 | 
     * @param originalFileName 原始文件名称 
 | 
     * @param response         response对象 
 | 
     */ 
 | 
    public void download(String bucketName, String bucketFileName, String originalFileName, HttpServletResponse response) { 
 | 
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build(); 
 | 
        try (GetObjectResponse objResponse = minioClient.getObject(objectArgs)) { 
 | 
            byte[] buf = new byte[1024]; 
 | 
            int len; 
 | 
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) { 
 | 
                while ((len = objResponse.read(buf)) != -1) { 
 | 
                    os.write(buf, 0, len); 
 | 
                } 
 | 
                os.flush(); 
 | 
                byte[] bytes = os.toByteArray(); 
 | 
                response.setCharacterEncoding("utf-8"); 
 | 
                //设置强制下载不打开 
 | 
                response.setContentType("application/force-download"); 
 | 
                // 设置附件名称编码 
 | 
                originalFileName = new String(originalFileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1); 
 | 
                // 设置附件名称 
 | 
                response.addHeader("Content-Disposition", "attachment;fileName=" + originalFileName); 
 | 
                // 写入文件 
 | 
                try (ServletOutputStream stream = response.getOutputStream()) { 
 | 
                    stream.write(bytes); 
 | 
                    stream.flush(); 
 | 
                } 
 | 
            } 
 | 
        } catch (Exception e) { 
 | 
            throw new UtilException("MinioUtils:上传文件工具类异常"); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 获取已上传对象的文件流(后端因为业务需要获取文件流可以调用该方法) 
 | 
     * 
 | 
     * @param bucketName     存储桶名称 
 | 
     * @param bucketFileName 桶中文件名称 
 | 
     * @return 文件流 
 | 
     */ 
 | 
    public InputStream getFileStream(String bucketName, String bucketFileName) throws Exception { 
 | 
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build(); 
 | 
        return minioClient.getObject(objectArgs); 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 批量删除文件对象结果 
 | 
     * 
 | 
     * @param bucketName      存储桶名称 
 | 
     * @param bucketFileName 桶中文件名称 
 | 
     * @return 删除结果 
 | 
     */ 
 | 
    public DeleteError removeObjectsResult(String bucketName, String bucketFileName) { 
 | 
        List<DeleteError> results = removeObjectsResult(bucketName, Collections.singletonList(bucketFileName)); 
 | 
        return !results.isEmpty() ? results.get(0) : null; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 批量删除文件对象结果 
 | 
     * 
 | 
     * @param bucketName      存储桶名称 
 | 
     * @param bucketFileNames 桶中文件名称集合 
 | 
     * @return 删除结果 
 | 
     */ 
 | 
    public List<DeleteError> removeObjectsResult(String bucketName, List<String> bucketFileNames) { 
 | 
        Iterable<Result<DeleteError>> results = removeObjects(bucketName, bucketFileNames); 
 | 
        List<DeleteError> res = new ArrayList<>(); 
 | 
        for (Result<DeleteError> result : results) { 
 | 
            try { 
 | 
                res.add(result.get()); 
 | 
            } catch (Exception e) { 
 | 
                throw new UtilException("MinioUtils:上传文件工具类异常"); 
 | 
            } 
 | 
        } 
 | 
        return res; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 批量删除文件对象 
 | 
     * 
 | 
     * @param bucketName      存储桶名称 
 | 
     * @param bucketFileNames 桶中文件名称集合 
 | 
     */ 
 | 
    private Iterable<Result<DeleteError>> removeObjects(String bucketName, List<String> bucketFileNames) { 
 | 
        List<DeleteObject> dos = bucketFileNames.stream().map(DeleteObject::new).collect(Collectors.toList()); 
 | 
        return minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build()); 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 查询预览url 
 | 
     * @param bucketFileName minio文件名称 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param isPreviewExpiry 是否需要过期时间 默认24小时 
 | 
     * @return 
 | 
     */ 
 | 
    public String getPreviewUrl(String bucketFileName, String bucketName, Boolean isPreviewExpiry) { 
 | 
        if (StringUtils.isNotBlank(bucketFileName)) { 
 | 
            try { 
 | 
                minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(bucketFileName).build()); 
 | 
                // 为false只生成24小时有效时长的url链接,可以访问该文件 
 | 
                if (isPreviewExpiry){ 
 | 
                    return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(bucketFileName).build()); 
 | 
                }else { 
 | 
                    return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(bucketFileName).expiry(previewExpiry, TimeUnit.HOURS).build()); 
 | 
                } 
 | 
            } catch (Exception e) { 
 | 
                throw new UtilException("MinioUtils:上传文件工具类异常"); 
 | 
            } 
 | 
        } 
 | 
        return null; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * 生成预览URL 
 | 
     * @param bucketFilename 文件在MinIO中的唯一标识 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param useDefaultExpiry 是否使用默认过期时间(true=使用默认过期时间,false=永久有效) 
 | 
     * @return 预览URL 
 | 
     */ 
 | 
    public String getPreviewUrls(String bucketFilename, String bucketName, boolean useDefaultExpiry) { 
 | 
        if (StringUtils.isBlank(bucketFilename)) { 
 | 
            return null; 
 | 
        } 
 | 
  
 | 
        try { 
 | 
            // 验证文件存在性 
 | 
            minioClient.statObject(StatObjectArgs.builder() 
 | 
                    .bucket(bucketName) 
 | 
                    .object(bucketFilename) 
 | 
                    .build()); 
 | 
  
 | 
            GetPresignedObjectUrlArgs.Builder builder = GetPresignedObjectUrlArgs.builder() 
 | 
                    .method(Method.GET) 
 | 
                    .bucket(bucketName) 
 | 
                    .object(bucketFilename); 
 | 
  
 | 
            // 设置过期时间:useDefaultExpiry=true 使用配置的过期时间 
 | 
            if (useDefaultExpiry) { 
 | 
                builder.expiry(previewExpiry, TimeUnit.HOURS); 
 | 
            } 
 | 
  
 | 
            return minioClient.getPresignedObjectUrl(builder.build()); 
 | 
        } catch (Exception e) { 
 | 
            throw new UtilException("生成预览URL失败: " + e.getMessage(), e); 
 | 
        } 
 | 
    } 
 | 
  
 | 
  
 | 
    /** 
 | 
     * 生成下载URL(强制浏览器下载) 
 | 
     * @param bucketFilename 文件在MinIO中的唯一标识 
 | 
     * @param bucketName 存储桶名称 
 | 
     * @param originalFileName 原始文件名(用于下载时显示) 
 | 
     * @param useDefaultExpiry 是否使用默认过期时间(true=使用默认,false=无过期时间) 
 | 
     * @return 下载URL 
 | 
     */ 
 | 
    public String getDownloadUrls(String bucketFilename, String bucketName, String originalFileName, boolean useDefaultExpiry) { 
 | 
        if (StringUtils.isBlank(bucketFilename)) { 
 | 
            return null; 
 | 
        } 
 | 
  
 | 
        try { 
 | 
            // 验证文件存在性 
 | 
            minioClient.statObject(StatObjectArgs.builder() 
 | 
                    .bucket(bucketName) 
 | 
                    .object(bucketFilename) 
 | 
                    .build()); 
 | 
  
 | 
            // 正确编码文件名:替换 + 为 %20 
 | 
            String encodedFileName = URLEncoder.encode(originalFileName, String.valueOf(StandardCharsets.UTF_8)) 
 | 
                    .replace("+", "%20"); 
 | 
  
 | 
            Map<String, String> reqParams = new HashMap<>(); 
 | 
            reqParams.put("response-content-disposition", 
 | 
                    "attachment; filename=\"" + encodedFileName + "\""); 
 | 
  
 | 
            GetPresignedObjectUrlArgs.Builder builder = GetPresignedObjectUrlArgs.builder() 
 | 
                    .method(Method.GET) 
 | 
                    .bucket(bucketName) 
 | 
                    .object(bucketFilename) 
 | 
                    .extraQueryParams(reqParams); 
 | 
  
 | 
            // 根据参数决定是否设置过期时间 
 | 
            if (useDefaultExpiry) { 
 | 
                // 使用默认过期时间(从配置读取) 
 | 
                builder.expiry(previewExpiry, TimeUnit.HOURS); 
 | 
            } else { 
 | 
                // 不设置过期时间(MinIO 默认7天) 
 | 
            } 
 | 
            return minioClient.getPresignedObjectUrl(builder.build()); 
 | 
        } catch (Exception e) { 
 | 
            throw new UtilException("生成下载URL失败: " + e.getMessage(), e); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    public String getDownloadUrl(String bucketFileName, String bucketName) { 
 | 
        if (StringUtils.isNotBlank(bucketFileName)) { 
 | 
            try { 
 | 
                // 检查文件是否存在 
 | 
                minioClient.statObject(StatObjectArgs.builder() 
 | 
                        .bucket(bucketName) 
 | 
                        .object(bucketFileName) 
 | 
                        .build()); 
 | 
  
 | 
                // 设置响应头 
 | 
                Map<String, String> reqParams = new HashMap<>(); 
 | 
                // 提取原始文件名(如果存储时保留了原始名称) 
 | 
                String originalFileName = extractOriginalFileName(bucketFileName); 
 | 
                reqParams.put("response-content-disposition", 
 | 
                        "attachment; filename=\"" + URLEncoder.encode(originalFileName, String.valueOf(StandardCharsets.UTF_8)) + "\""); 
 | 
  
 | 
                // 构建预签名URL参数 
 | 
                GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() 
 | 
                        .method(Method.GET) 
 | 
                        .bucket(bucketName) 
 | 
                        .object(bucketFileName) 
 | 
                        .expiry(previewExpiry, TimeUnit.HOURS) 
 | 
                        .extraQueryParams(reqParams) 
 | 
                        .build(); 
 | 
  
 | 
                return minioClient.getPresignedObjectUrl(args); 
 | 
            } catch (Exception e) { 
 | 
                throw new UtilException("MinioUtils:生成下载链接异常", e); 
 | 
            } 
 | 
        } 
 | 
        return null; 
 | 
    } 
 | 
  
 | 
    private String extractOriginalFileName(String bucketFileName) { 
 | 
        // 示例:如果存储格式为 "原始文件名_UUID" 
 | 
        int underscoreIndex = bucketFileName.lastIndexOf("_"); 
 | 
        if (underscoreIndex > 0) { 
 | 
            return bucketFileName.substring(0, underscoreIndex); 
 | 
        } 
 | 
        // 如果没有特殊格式,直接返回完整文件名 
 | 
        return bucketFileName; 
 | 
    } 
 | 
  
 | 
} 
 |