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 | 176 ++++++++++++++++++++++++++++++++++++----------------------
1 files changed, 108 insertions(+), 68 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 cad733a..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
@@ -3,122 +3,162 @@
import com.ard.common.config.OssConfiguration;
import com.ard.oss.model.CredentialsDTO;
import com.ard.oss.service.impl.OssServiceContext;
-import com.ard.sample.utils.S3PutObjectResponse;
+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;
+import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+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
* 承接大疆 PUT 上传请求,中转上传到 MinIO HTTPS 接口
*/
+@Api(tags = "机场上传minio")
@Slf4j
@RestController
@RequestMapping("/minioTransfer") // 根路径,匹配 Nginx 转发的 /dock/、/kmz/ 路径
public class MinioProxyUploadController {
-// @Autowired
-// private MinioStsUtils minioStsUtils; // 复用你已有的 HTTPS 临时凭证工具类
-
+ // 注入你的凭证服务(保持原有注入方式不变)
@Autowired
private OssServiceContext ossService;
- // 你的 MinIO HTTPS 原生地址(固定,与服务器版本匹配)
-// private static final String MINIO_HTTPS_ENDPOINT = "https://192.168.1.3:9001";
+ // 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);
+ }
/**
- * 核心接口:中转大疆的 PUT 上传请求到 MinIO
- * 匹配路径:/{bucketName}/{objectName}(例:/dock/xxx.png)
- * @param bucketName 桶名(dock/kmz,与大疆上传一致)
- * @param objectName 文件名(例:dji-test-20240124.png)
- * @param fileContent 文件二进制内容(大疆上传的核心数据)
- * @param contentType 文件 MIME 类型(默认二进制流,适配多种文件格式)
- * @return S3 规范 XML 响应(返回给大疆,衔接后续流程)
+ * 完全复刻 MinIO 原生 PUT 上传接口
+ * 通配符匹配所有路径,保留原始URI格式
*/
- @PutMapping("/{bucketName}/{objectName}")
- public ResponseEntity<S3PutObjectResponse> proxyUpload(
+ @ApiOperation("MinIO原生PUT接口代理(无感中转)")
+ @PutMapping(value = "/{bucketName}/**", consumes = MediaType.ALL_VALUE)
+ public ResponseEntity<?> proxyMinioPutObject(
@PathVariable String bucketName,
- @PathVariable String objectName,
- @RequestBody byte[] fileContent,
- @RequestHeader(value = "Content-Type", defaultValue = "application/octet-stream") String contentType
+ // 透传大疆设备原始请求体,不修改、不封装
+ @RequestBody(required = false) byte[] rawRequestBody,
+ HttpServletRequest request
) {
+ 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());
+
+ log.info("中转MinIO PUT请求 | 桶名:{} | 原始对象路径:{} | 请求体大小:{}byte",
+ bucketName, objectKey, rawRequestBody == null ? 0 : rawRequestBody.length);
+
try {
- // 步骤 1:获取 MinIO HTTPS 临时凭证(复用你的已有工具类,已验证可行)
- CredentialsDTO credentials = ossService.getCredentials();
+ // ===================== 2. 复用原有逻辑:获取临时凭证 =====================
+ var credentials = ossService.getCredentials();
if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
+ log.error("临时凭证获取失败");
+ return buildNativeS3Error("InvalidCredentials", "临时凭证无效", requestUri, HttpStatus.FORBIDDEN);
}
- // 步骤 2:构建 MinioClient(适配 SDK 8.3.7,忽略私有 SSL 证书,使用临时凭证)
+ // ===================== 3. 复用原有逻辑:构建MinIO客户端 =====================
MinioClient minioClient = MinioClient.builder()
- .endpoint(OssConfiguration.endpoint) // 指定 MinIO HTTPS 原生地址
+ .endpoint(OssConfiguration.endpoint)
.credentialsProvider(new StaticProvider(
credentials.getAccessKeyId(),
credentials.getAccessKeySecret(),
- credentials.getSecurityToken() // SDK 8.3.7 支持临时凭证的 sessionToken 参数
+ credentials.getSecurityToken()
))
.build();
- // 步骤 3:中转上传文件到 MinIO(核心:SDK 8.3.7 兼容 PutObjectArgs 构建)
- minioClient.putObject(
- PutObjectArgs.builder()
- .bucket(bucketName) // 桶名与大疆上传一致(dock/kmz)
- .object(objectName) // 文件名与大疆上传一致
- .stream(
- new ByteArrayInputStream(fileContent), // 文件二进制流
- fileContent.length, // 文件字节长度
- -1 // 自动分块(SDK 8.3.7 支持 -1 自动处理,适配小文件上传)
- )
- .contentType(contentType) // 文件 MIME 类型
- .build()
- );
+ // 校验桶存在性(兼容原生接口行为,桶不存在返回404)
+ boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+ if (!bucketExists) {
+ return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", requestUri, HttpStatus.NOT_FOUND);
+ }
- // 步骤 4:构造符合 S3 规范的成功响应(返回给大疆,确保流程衔接)
- S3PutObjectResponse successResponse = new S3PutObjectResponse(
- // ETag:生成文件 MD5 值(带双引号,符合 S3 规范,依赖 commons-codec)
- "\"" + org.apache.commons.codec.digest.DigestUtils.md5Hex(fileContent) + "\"",
- // LastModified:当前时间(ISO 格式,MinIO 服务器兼容该格式)
- OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
- );
+ // ===================== 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()
+ );
+ }
- // 步骤 5:返回 200 OK + XML 格式响应(Spring Boot 自动转换实体类为 XML)
- return ResponseEntity.status(HttpStatus.OK)
+ // ===================== 5. 原生接口成功响应:200 OK + 空响应体(核心对齐!)=====================
+ log.info("中转上传成功 | 路径:{}", objectKey);
+ return ResponseEntity.ok()
+ // 无任何响应体,与MinIO原生行为完全一致
+ .body(null);
+
+ } 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(successResponse);
-
+ .body(nativeError);
} catch (MinioException e) {
- // MinIO 上传异常(SDK 8.3.7 抛出的异常类型,与高版本一致)
- e.printStackTrace();
- log.info("MinIO 上传异常:{}",e.getMessage());
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
- } catch (NoSuchAlgorithmException | InvalidKeyException e) {
- // 凭证/算法异常(适配 SDK 8.3.7 的加密校验逻辑)
- e.printStackTrace();
- log.info("MinIO 凭证或算法异常:{}",e.getMessage());
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
+ log.error("MinIO客户端异常:{}", e.getMessage());
+ return buildNativeS3Error("ClientError", e.getMessage(), requestUri, HttpStatus.INTERNAL_SERVER_ERROR);
} catch (Exception e) {
- // 其他通用异常
- e.printStackTrace();
- log.info("MinIO 异常:{}",e.getMessage());
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
+ log.error("代理中转通用异常:{}", e.getMessage(), e);
+ return buildNativeS3Error("InternalError", "服务器代理异常", requestUri, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
-}
+
+ /**
+ * 兜底构造S3标准XML错误响应,兼容大疆设备解析
+ */
+ 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(error);
+ }
+}
\ No newline at end of file
--
Gitblit v1.9.3