package com.ruoyi.common.utils.file; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.ruoyi.common.core.domain.MinioResult; import com.ruoyi.common.exception.UtilException; import com.ruoyi.common.exception.file.InvalidExtensionException; import io.minio.*; import io.minio.http.Method; import io.minio.messages.DeleteError; import io.minio.messages.DeleteObject; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; 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 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 fileNames = upload(bucketName, fileArr, isPreviewExpiry); return fileNames.isEmpty() ? null : fileNames.get(0); } /** * 上传文件 * * @param bucketName 存储桶名称 * @param fileList 文件列表 * @return 桶中位置列表 */ public List upload(String bucketName, List 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 upload(String bucketName, MultipartFile[] fileArr, Boolean isPreviewExpiry) throws InvalidExtensionException { for (MultipartFile file : fileArr) { FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } // 保证桶一定存在 existBucket(bucketName); // 执行正常操作 List 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 results = removeObjectsResult(bucketName, Collections.singletonList(bucketFileName)); return !results.isEmpty() ? results.get(0) : null; } /** * 批量删除文件对象结果 * * @param bucketName 存储桶名称 * @param bucketFileNames 桶中文件名称集合 * @return 删除结果 */ public List removeObjectsResult(String bucketName, List bucketFileNames) { Iterable> results = removeObjects(bucketName, bucketFileNames); List res = new ArrayList<>(); for (Result result : results) { try { res.add(result.get()); } catch (Exception e) { throw new UtilException("MinioUtils:上传文件工具类异常"); } } return res; } /** * 批量删除文件对象 * * @param bucketName 存储桶名称 * @param bucketFileNames 桶中文件名称集合 */ private Iterable> removeObjects(String bucketName, List bucketFileNames) { List 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 (小时),默认24小时 * @return 文件下载URL */ public String getDownloadUrl(String bucketFileName, String bucketName) { if (StringUtils.isNotBlank(bucketFileName)) { try { // 检查文件是否存在 minioClient.statObject(StatObjectArgs.builder() .bucket(bucketName) .object(bucketFileName) .build()); // 设置响应头 Map reqParams = new HashMap<>(); // 提取原始文件名(如果存储时保留了原始名称) String originalFileName = extractOriginalFileName(bucketFileName); reqParams.put("response-content-disposition", "attachment; filename=\"" + URLEncoder.encode(originalFileName, 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; } /** * 从bucketFileName中提取原始文件名 * 需根据实际存储规则调整(例如,如果存储时添加了时间戳或UUID后缀) */ private String extractOriginalFileName(String bucketFileName) { // 示例:如果存储格式为 "原始文件名_UUID" int underscoreIndex = bucketFileName.lastIndexOf("_"); if (underscoreIndex > 0) { return bucketFileName.substring(0, underscoreIndex); } // 如果没有特殊格式,直接返回完整文件名 return bucketFileName; } /** * 生成预览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, StandardCharsets.UTF_8) .replace("+", "%20"); Map 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); } } }