From db82545cc79a8c35a7a3836b38919c574e3ee4d9 Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Wed, 04 Feb 2026 11:55:32 +0800
Subject: [PATCH] 2026-02-04 优化minio上传

---
 ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java |  150 +++++++++++++++++++++++++++++++-------------------
 1 files changed, 93 insertions(+), 57 deletions(-)

diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
index 462192f..649e862 100644
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/MinioProxyUploadController.java
@@ -5,10 +5,13 @@
 import com.ard.oss.service.impl.OssServiceContext;
 import com.ard.sample.media.Response.S3ErrorResponse;
 import com.ard.sample.media.Response.S3PutObjectResponse;
+import io.minio.BucketExistsArgs;
 import io.minio.MinioClient;
 import io.minio.PutObjectArgs;
 import io.minio.credentials.StaticProvider;
+import io.minio.errors.ErrorResponseException;
 import io.minio.errors.MinioException;
+import io.minio.messages.ErrorResponse;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
@@ -19,12 +22,17 @@
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * 适配 MinIO SDK 8.3.7 + 服务器 RELEASE.2024-08-03
@@ -40,38 +48,52 @@
     @Autowired
     private OssServiceContext ossService;
 
-    @ApiOperation("minio上传/创建目录")
-    // 关键修改:{objectName:.*} 支持路径中包含斜杠,适配多层级文件/目录
-    @PutMapping(value = "/{bucketName}/{objectName:.*}", produces = MediaType.APPLICATION_XML_VALUE)
-    public ResponseEntity<?> proxyUpload(
+    // S3标准错误码 -> HTTP状态码 映射表(行业通用标准)
+    private static final Map<String, HttpStatus> S3_ERROR_CODE_MAP;
+
+    // 初始化常见错误映射
+    static {
+        S3_ERROR_CODE_MAP = new HashMap<>();
+        S3_ERROR_CODE_MAP.put("NoSuchBucket", HttpStatus.NOT_FOUND);
+        S3_ERROR_CODE_MAP.put("NoSuchKey", HttpStatus.NOT_FOUND);
+        S3_ERROR_CODE_MAP.put("AccessDenied", HttpStatus.FORBIDDEN);
+        S3_ERROR_CODE_MAP.put("InvalidCredentials", HttpStatus.FORBIDDEN);
+        S3_ERROR_CODE_MAP.put("BucketAlreadyExists", HttpStatus.CONFLICT);
+        S3_ERROR_CODE_MAP.put("InvalidRequest", HttpStatus.BAD_REQUEST);
+    }
+
+    /**
+     * 完全复刻 MinIO 原生 PUT 上传接口
+     * 通配符匹配所有路径,保留原始URI格式
+     */
+    @ApiOperation("MinIO原生PUT接口代理(无感中转)")
+    @PutMapping(value = "/{bucketName}/**", consumes = MediaType.ALL_VALUE)
+    public ResponseEntity<?> proxyMinioPutObject(
             @PathVariable String bucketName,
-            @PathVariable String objectName,
-            // 允许请求体为空(目录创建场景)
-            @RequestBody(required = false) byte[] fileContent,
-            @RequestHeader(value = "Content-Type", defaultValue = "application/octet-stream") String contentType
+            // 透传大疆设备原始请求体,不修改、不封装
+            @RequestBody(required = false) byte[] rawRequestBody,
+            HttpServletRequest request
     ) {
-        // 基础参数校验
-        if (bucketName.isBlank() || objectName.isBlank()) {
-            return buildS3Error("InvalidRequest", "存储桶/对象名称不能为空", "/" + bucketName + "/" + objectName);
-        }
+        log.info("接收参数bucketName:{}",bucketName);
+        // ===================== 1. 解析原始路径:保留所有斜杠/末尾分隔符,完全对齐原生MinIO =====================
+        String rawUri = request.getRequestURI();
+        String requestUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8);
+        String bucketPrefix = "/" + bucketName;
+        // 提取原始ObjectKey,无任何修改
+        String objectKey = requestUri.substring(bucketPrefix.length());
 
-        // 标识:是否为目录创建请求(对象名以 / 结尾)
-        boolean isCreateDirectory = objectName.endsWith("/");
-        // 处理空数据:目录创建/空请求体时,使用空字节数组
-        byte[] actualContent = (fileContent == null) ? new byte[0] : fileContent;
+        log.info("中转MinIO PUT请求 | 桶名:{} | 原始对象路径:{} | 请求体大小:{}byte",
+                bucketName, objectKey, rawRequestBody == null ? 0 : rawRequestBody.length);
 
-        log.info("开始代理操作 | 存储桶:{} | 对象路径:{} | 操作类型:{}",
-                bucketName, objectName, isCreateDirectory ? "创建目录" : "上传文件");
-
-        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(actualContent)) {
-            // ===================== 保留你原有逻辑:获取临时凭证 =====================
-            CredentialsDTO credentials = ossService.getCredentials();
+        try {
+            // ===================== 2. 复用原有逻辑:获取临时凭证 =====================
+            var credentials = ossService.getCredentials();
             if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
-                log.error("MinIO临时凭证获取失败");
-                return buildS3Error("InvalidCredentials", "临时凭证无效", "/" + bucketName + "/" + objectName);
+                log.error("临时凭证获取失败");
+                return buildNativeS3Error("InvalidCredentials", "临时凭证无效", requestUri, HttpStatus.FORBIDDEN);
             }
 
-            // ===================== 保留你原有逻辑:构建MinioClient =====================
+            // ===================== 3. 复用原有逻辑:构建MinIO客户端 =====================
             MinioClient minioClient = MinioClient.builder()
                     .endpoint(OssConfiguration.endpoint)
                     .credentialsProvider(new StaticProvider(
@@ -81,48 +103,62 @@
                     ))
                     .build();
 
-            // ===================== 核心上传逻辑:兼容文件/目录/多层级路径 =====================
-            minioClient.putObject(
-                    PutObjectArgs.builder()
-                            .bucket(bucketName)
-                            .object(objectName)
-                            .stream(inputStream, actualContent.length, -1)
-                            .contentType(contentType)
-                            .build()
-            );
+            // 校验桶存在性(兼容原生接口行为,桶不存在返回404)
+            boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+            if (!bucketExists) {
+                return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", requestUri, HttpStatus.NOT_FOUND);
+            }
 
-            // 生成标准响应参数
-            String eTag = "\"" + DigestUtils.md5Hex(actualContent) + "\"";
-            String lastModified = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
-            S3PutObjectResponse successResponse = new S3PutObjectResponse(eTag, lastModified);
+            // ===================== 4. 透传请求:原样转发大疆原始数据到MinIO,无业务篡改 =====================
+            // 空请求体=大疆原生创建目录请求,直接透传;带请求体=上传文件,原生处理
+            byte[] requestBody = rawRequestBody == null ? new byte[0] : rawRequestBody;
+            try (ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody)) {
+                minioClient.putObject(
+                        PutObjectArgs.builder()
+                                .bucket(bucketName)
+                                .object(objectKey)
+                                // 透传原始流、原始长度,完全复刻原生请求
+                                .stream(inputStream, requestBody.length, -1)
+                                // 透传原生Content-Type,无修改
+                                .contentType(request.getContentType() == null ? "application/octet-stream" : request.getContentType())
+                                .build()
+                );
+            }
 
-            log.info("操作成功 | 路径:{} | ETag:{}", objectName, eTag);
+            // ===================== 5. 原生接口成功响应:200 OK + 空响应体(核心对齐!)=====================
+            log.info("中转上传成功 | 路径:{}", objectKey);
             return ResponseEntity.ok()
-                    .contentType(MediaType.APPLICATION_XML)
-                    .body(successResponse);
+                    // 无任何响应体,与MinIO原生行为完全一致
+                    .body(null);
 
-        }  catch (MinioException e) {
-            log.error("MinIO服务异常: {}", e.getMessage(), e);
-            return buildS3Error("ClientError", e.getMessage(), "/" + bucketName + "/" + objectName);
-        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
-            log.error("凭证/加密异常: {}", e.getMessage(), e);
-            return buildS3Error("CredentialError", "凭证校验失败", "/" + bucketName + "/" + objectName);
-        } catch (IOException e) {
-            log.error("文件流处理异常: {}", e.getMessage(), e);
-            return buildS3Error("IOError", "文件数据流处理失败", "/" + bucketName + "/" + objectName);
+        } catch (ErrorResponseException e) {
+            // ===================== 最终修正:无任何无效方法调用 =====================
+            ErrorResponse nativeError = e.errorResponse();
+            String errorCode = nativeError.code();
+            // 映射状态码,无匹配则默认500
+            HttpStatus status = S3_ERROR_CODE_MAP.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR);
+
+            log.error("MinIO服务错误 | 错误码:{} | 信息:{}", errorCode, nativeError.message());
+            // 透传MinIO原生XML错误体,大疆设备完美兼容
+            return ResponseEntity.status(status)
+                    .contentType(MediaType.APPLICATION_XML)
+                    .body(nativeError);
+        } catch (MinioException e) {
+            log.error("MinIO客户端异常:{}", e.getMessage());
+            return buildNativeS3Error("ClientError", e.getMessage(), requestUri, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (Exception e) {
-            log.error("通用服务异常: {}", e.getMessage(), e);
-            return buildS3Error("InternalError", "服务器内部异常", "/" + bucketName + "/" + objectName);
+            log.error("代理中转通用异常:{}", e.getMessage(), e);
+            return buildNativeS3Error("InternalError", "服务器代理异常", requestUri, HttpStatus.INTERNAL_SERVER_ERROR);
         }
     }
 
     /**
-     * 构造S3标准XML错误响应
+     * 兜底构造S3标准XML错误响应,兼容大疆设备解析
      */
-    private ResponseEntity<S3ErrorResponse> buildS3Error(String code, String message, String resource) {
-        S3ErrorResponse errorResponse = new S3ErrorResponse(code, message, resource);
-        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+    private ResponseEntity<S3ErrorResponse> buildNativeS3Error(String code, String message, String resource, HttpStatus status) {
+        S3ErrorResponse error = new S3ErrorResponse(code, message, resource);
+        return ResponseEntity.status(status)
                 .contentType(MediaType.APPLICATION_XML)
-                .body(errorResponse);
+                .body(error);
     }
 }
\ No newline at end of file

--
Gitblit v1.9.3