From 042cbf63dc814d845c76df5cca424d4e340e5e2e Mon Sep 17 00:00:00 2001 From: xiongcz Date: Thu, 13 Mar 2025 16:00:04 +0800 Subject: [PATCH] =?UTF-8?q?minIO=E4=B8=8A=E4=BC=A0=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 26 ++++ .../web/config/GlobalExceptionHandler.java | 25 ++++ .../bhdemo/demos/web/config/MinIOConfig.java | 23 +++ ...feMappingJackson2HttpMessageConverter.java | 37 +++++ .../bh/bhdemo/demos/web/config/WebConfig.java | 22 +++ .../demos/web/controller/MinIOController.java | 131 +++++++++++++++++ .../web/entity/MinIOUploadRequestPojo.java | 15 ++ .../bh/bhdemo/demos/web/entity/MinioPojo.java | 20 +++ .../demos/web/service/MinIOService.java | 139 ++++++++++++++++++ src/main/resources/application-dev.yml | 15 +- src/main/resources/application-test.yml | 17 ++- src/main/resources/application.yml | 6 +- 12 files changed, 469 insertions(+), 7 deletions(-) create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/GlobalExceptionHandler.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/MinIOConfig.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/Utf8SafeMappingJackson2HttpMessageConverter.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/WebConfig.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/controller/MinIOController.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinIOUploadRequestPojo.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinioPojo.java create mode 100644 src/main/java/cn/com/connor/bh/bhdemo/demos/web/service/MinIOService.java diff --git a/pom.xml b/pom.xml index fb09aee..28135fc 100644 --- a/pom.xml +++ b/pom.xml @@ -81,8 +81,34 @@ org.springframework.boot spring-boot-starter-amqp + 2.3.7.RELEASE + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.3.0 + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + io.minio + minio + 8.5.2 + + + commons-io + commons-io + 2.11.0 + + + diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/GlobalExceptionHandler.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..983b397 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package cn.com.connor.bh.bhdemo.demos.web.config; + +import com.fasterxml.jackson.databind.JsonMappingException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ControllerAdvice; + + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof JsonMappingException) { + JsonMappingException jsonMappingException = (JsonMappingException) cause; + String errorMessage = "Invalid UTF-8 character encountered: " + jsonMappingException.getMessage(); + // 记录详细的错误日志 + System.err.println(errorMessage); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } + return new ResponseEntity<>("Invalid request payload", HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/MinIOConfig.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/MinIOConfig.java new file mode 100644 index 0000000..3b95b62 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/MinIOConfig.java @@ -0,0 +1,23 @@ +package cn.com.connor.bh.bhdemo.demos.web.config; + +import cn.com.connor.bh.bhdemo.demos.web.entity.MinioPojo; +import io.minio.MinioClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration() +public class MinIOConfig { + @Autowired + private MinioPojo minioPojo; + + + @Bean + public MinioClient minioClient(){ + return MinioClient.builder() + .endpoint(minioPojo.getUrl()) //传入url地址 + //传入用户名和密码 + .credentials(minioPojo.getAccessKey(), minioPojo.getSecretKey()) + .build(); //完成MinioClient的初始化 + } +} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/Utf8SafeMappingJackson2HttpMessageConverter.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/Utf8SafeMappingJackson2HttpMessageConverter.java new file mode 100644 index 0000000..7f5e590 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/Utf8SafeMappingJackson2HttpMessageConverter.java @@ -0,0 +1,37 @@ +//package cn.com.connor.bh.bhdemo.demos.web.config; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import org.apache.commons.io.IOUtils; +//import org.springframework.http.HttpInputMessage; +//import org.springframework.http.converter.HttpMessageNotReadableException; +//import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +// +//import java.io.IOException; +//import java.nio.charset.StandardCharsets; +// +//public class Utf8SafeMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { +// private final ObjectMapper objectMapper; +// +// public Utf8SafeMappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { +// this.objectMapper = objectMapper; +// } +// +// @Override +// protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { +// byte[] body = IOUtils.toByteArray(inputMessage.getBody()); +// try { +// // 检查是否为有效UTF-8 +// String content = new String(body, StandardCharsets.UTF_8); +// validateUtf8(content); +// return objectMapper.readValue(content, clazz); +// } catch (IllegalArgumentException e) { +// throw new HttpMessageNotReadableException("Invalid UTF-8 encoding in request body", e, inputMessage); +// } +// } +// +// private void validateUtf8(String content) { +// if (!StandardCharsets.UTF_8.newEncoder().canEncode(content)) { +// throw new IllegalArgumentException("Invalid UTF-8 characters found"); +// } +// } +//} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/WebConfig.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/WebConfig.java new file mode 100644 index 0000000..dd01815 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/config/WebConfig.java @@ -0,0 +1,22 @@ +//package cn.com.connor.bh.bhdemo.demos.web.config; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.http.converter.HttpMessageConverter; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//import java.util.List; +// +//@Configuration +//public class WebConfig implements WebMvcConfigurer { +// @Bean +// public Utf8SafeMappingJackson2HttpMessageConverter utf8SafeMappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { +// return new Utf8SafeMappingJackson2HttpMessageConverter(objectMapper); +// } +// +// @Override +// public void configureMessageConverters(List> converters) { +// converters.add(utf8SafeMappingJackson2HttpMessageConverter(new ObjectMapper())); +// } +//} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/controller/MinIOController.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/controller/MinIOController.java new file mode 100644 index 0000000..4c2146a --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/controller/MinIOController.java @@ -0,0 +1,131 @@ +package cn.com.connor.bh.bhdemo.demos.web.controller; + +import cn.com.connor.bh.bhdemo.demos.web.entity.MinIOUploadRequestPojo; +import cn.com.connor.bh.bhdemo.demos.web.service.MinIOService; +import cn.hutool.core.codec.Base64; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/minio") +public class MinIOController { + private static final Logger logger = LoggerFactory.getLogger(MinIOController.class); + + private final ObjectMapper objectMapper; + + public MinIOController(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Autowired + private MinIOService minIOService; + + @PostMapping("/upload") + public ResponseEntity> uploadFile(@RequestParam("filePath") MultipartFile file, + @RequestParam("bucketFileName") String bucketFileName) throws Exception { + Map response = new HashMap<>(); + try { + logger.info("开始上传文件到MinIO,上传目标位置:{}", bucketFileName); + + logger.info("开始上传文件到MinIO,上传目标位置:{},原始文件:{}", bucketFileName, file.getOriginalFilename()); + if (bucketFileName == null || bucketFileName.isEmpty()) { + logger.error("上传文件路径或文件名不能为空!"); + response.put("message", "上传文件路径或文件名不能为空!"); + return ResponseEntity.badRequest().body(response); + } + + String resultUrl = minIOService.uploadFile(file, bucketFileName); + if (resultUrl != null){ + response.put("message", "File uploaded successfully"); + response.put("url", resultUrl); + }else { + response.put("message", "文件上传失败,请联系管理员。"); + } + return ResponseEntity.ok(response); + + } catch (IllegalArgumentException e) { + logger.error("文件上传参数非法", e); + response.put("message", "非法参数:" + e.getMessage()); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + logger.error("文件上传过程中发生未知异常", e); + response.put("message", "文件上传失败,请联系管理员。"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 清空minIO目录 + * @param directory + * @return + * @throws Exception + */ + @PostMapping("/deleteByDir") + public ResponseEntity> deleteByDir(@RequestParam("directory") String directory) throws Exception { + Map response = new HashMap<>(); + try { + + logger.info("开始清空MinIO目录,清空目标位置:{}", directory); + if (directory == null || directory.isEmpty()) { + logger.error("清空minIO目录路径不能为空!"); + response.put("message", "清空minIO目录路径不能为空!"); + return ResponseEntity.badRequest().body(response); + } + boolean deletResult = minIOService.deleteByDir(directory); + if (deletResult){ + response.put("message", "清空minIO目录成功"); + return ResponseEntity.ok(response); + }else { + response.put("message", "清空minIO目录失败,请联系管理员。"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + }catch (Exception e) { + logger.error("清空minIO目录过程中发生未知异常", e); + response.put("message", "清空minIO目录失败,请联系管理员。"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + + } + + private String parseValueFromBody(String body, String paramName) { + String[] parts = body.split("&"); + for (String part : parts) { + String[] keyValue = part.split("="); + if (keyValue.length == 2 && keyValue[0].equals(paramName)) { + try { + return URLDecoder.decode(keyValue[1], "GBK"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + } + return null; + } + + public static String removeFirstAtSymbol(String str,String symbol) { + int atIndex = str.indexOf(symbol); + if (atIndex != -1) { + return str.substring(0, atIndex) + str.substring(atIndex + 1); + } + return str; // 如果没有找到 @ 符号,返回原字符串 + } + + private String cleanString(String input) { + // 使用正则表达式或其他方法清理无效字符 + return input.replaceAll("[^\\x00-\\x7F]", ""); + } +} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinIOUploadRequestPojo.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinIOUploadRequestPojo.java new file mode 100644 index 0000000..f2bb273 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinIOUploadRequestPojo.java @@ -0,0 +1,15 @@ +package cn.com.connor.bh.bhdemo.demos.web.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MinIOUploadRequestPojo { + private String filePath; // 文件在客户端或服务器上的路径 + private String bucketFileName; // 在MinIO中的目标文件名 +} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinioPojo.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinioPojo.java new file mode 100644 index 0000000..c0c04b7 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/entity/MinioPojo.java @@ -0,0 +1,20 @@ +package cn.com.connor.bh.bhdemo.demos.web.entity; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "minio") +@Component +@Data +public class MinioPojo { + + private String url; + private String username; + private String password; + //桶名 + private String bucketName; + private String accessKey; + private String secretKey; +} diff --git a/src/main/java/cn/com/connor/bh/bhdemo/demos/web/service/MinIOService.java b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/service/MinIOService.java new file mode 100644 index 0000000..2dfce29 --- /dev/null +++ b/src/main/java/cn/com/connor/bh/bhdemo/demos/web/service/MinIOService.java @@ -0,0 +1,139 @@ +package cn.com.connor.bh.bhdemo.demos.web.service; + +import cn.com.connor.bh.bhdemo.demos.web.entity.MinioPojo; +import io.minio.*; +import io.minio.messages.Item; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Component +public class MinIOService { + private static final Logger logger = LoggerFactory.getLogger(MinIOService.class); + @Autowired + private MinioClient minioClient; + @Autowired + private MinioPojo minioPojo; + + + /** + * + * @param file 本地文件路径 + * @param bucketfilename minIO上文件的全路径名,如:2021/01/01/评审单.PDF + * @return + */ + public String uploadFile(MultipartFile file, String bucketfilename) { + logger.info("service-uploadFile:开始上传文件到MinIO"); + String returnInfo = null; + // 检查文件是否为空 + if (file.getSize() == 0 || file.isEmpty()) { + throw new IllegalArgumentException("上传的文件无效或为空!"); + } + + String bucketName = minioPojo.getBucketName(); + try { + logger.info("service-uploadFile:开始上传文件到MinIO=> " + minioPojo.getUrl() + "/" + bucketName + "/" + bucketfilename); + logger.info("01"); + // 判断桶是否存在 + boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + if (!bucketExists) { + logger.info("服务-uploadFile:桶不存在,正在创建桶 => " + bucketName); + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + logger.info("02"); + // 使用 MultipartFile 的输入流上传文件 + try (InputStream inputStream = file.getInputStream()) { + minioClient.putObject(PutObjectArgs.builder() + .bucket(bucketName) + .object(bucketfilename) + .stream(inputStream, file.getSize(), -1) + .contentType(file.getContentType()) + .build()); + } + logger.info("03"); + // 构造返回的文件访问 URL + URI fileUri = new URI(minioPojo.getUrl() + "/" + bucketName + bucketfilename); + returnInfo = fileUri.toString(); + + }catch (IllegalArgumentException e1){ + logger.error("服务-uploadFile:参数错误", e1); + throw new IllegalArgumentException("文件上传失败!", e1); + }catch (Exception e) { + logger.error("服务-uploadFile:未知错误", e); + throw new RuntimeException("文件上传失败!", e); + } + return returnInfo; + } + + + /** + * 检查给定的路径是否为一个有效的文件 + * @param filePath 文件路径 + * @return 如果路径存在且是一个文件,则返回true;否则返回false + */ + private boolean isFileExists(String filePath) { + File file = new File(filePath); + return file.exists() && file.isFile(); + } + + /** + * 根据文件名获取内容类型 + * @param file 文件对象 + * @return 内容类型 + */ + private String getContentType(File file) { + String fileName = file.getName(); + if (fileName.endsWith(".pdf")) { + return "application/pdf"; + } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (fileName.endsWith(".png")) { + return "image/png"; + } else if (fileName.endsWith(".txt")) { + return "text/plain"; + } else { + return "application/octet-stream"; + } + } + + /** + * 删除minIO指定目录下的所有文件 + * @param directory 目录路径,形如:"/2023/01/01/" + * @return 删除成功返回true,否则返回false + */ + public boolean deleteByDir(String directory) { + logger.info("service-deleteByDir:开始删除目录下的所有文件 => " + directory); + boolean result = false; + String bucketName = minioPojo.getBucketName(); + try { + // 列出目录下的所有对象 + Iterable> results = minioClient.listObjects( + ListObjectsArgs.builder().bucket(bucketName).prefix(directory).build()); + + for (Result resultItem : results) { + Item item = resultItem.get(); + // 删除每个对象 + minioClient.removeObject( + RemoveObjectArgs.builder().bucket(bucketName).object(item.objectName()).build()); + } + result = true; + } catch (Exception e) { + logger.error("服务-deleteByDir: 删除文件失败", e); + } + + + return result; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index eaf00ce..1d4c7c5 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -9,14 +9,23 @@ spring: rabbitmq: host: 192.168.3.125 port: 5672 - username: kuma - password: 123 + username: guest + password: guest template: default-receive-queue: StoCAPP + + mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: cn.com.connor.bh.bhdemo.demos.web.entity configuration: - map-underscore-to-camel-case: true \ No newline at end of file + map-underscore-to-camel-case: true + +#MinIO上传配置 +minio: + url: http://192.168.3.125:9000 + username: minioadmin + password: minioadmin + bucketName: bhdemo \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 826d21e..460e6cf 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -7,16 +7,27 @@ spring: username: infodba password: infodba rabbitmq: - host: 192.168.3.125 + host: localhost port: 5672 - username: kuma - password: 123 + virtual-host: whbh + username: admin + password: admin template: default-receive-queue: StoCAPP + mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: cn.com.connor.bh.bhdemo.demos.web.entity configuration: map-underscore-to-camel-case: true + +#MinIO上传配置 +minio: + url: http://127.0.0.1:9005 + username: minioadmin + password: PSlCwr3gQaJl6aFHT4jA + bucketName: tcfile + accessKey: PSlCwr3gQaJl6aFHT4jA #访问的key + secretKey: 5fwp5Umfo2I8kr76CINbNjuGDBHvRxHQEhPjeRRl #访问的密钥 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 02ff619..782ed25 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,4 +32,8 @@ spring: profiles: active: test #需要使用的配置文件的后缀 server: - port: 8080 \ No newline at end of file + port: 8080 + servlet: + multipart: + max-file-size=100MB: + max-request-size=100MB: