package com.ard.web.controller.sample.media; import com.ard.common.config.OssConfiguration; import com.ard.oss.model.CredentialsDTO; 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; 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.*; 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 OssServiceContext ossService; // S3标准错误码 -> HTTP状态码 映射表(行业通用标准) private static final Map 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, // 透传大疆设备原始请求体,不修改、不封装 @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 { // ===================== 2. 复用原有逻辑:获取临时凭证 ===================== var credentials = ossService.getCredentials(); if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) { log.error("临时凭证获取失败"); return buildNativeS3Error("InvalidCredentials", "临时凭证无效", requestUri, HttpStatus.FORBIDDEN); } // ===================== 3. 复用原有逻辑:构建MinIO客户端 ===================== MinioClient minioClient = MinioClient.builder() .endpoint(OssConfiguration.endpoint) .credentialsProvider(new StaticProvider( credentials.getAccessKeyId(), credentials.getAccessKeySecret(), credentials.getSecurityToken() )) .build(); // 校验桶存在性(兼容原生接口行为,桶不存在返回404) boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!bucketExists) { return buildNativeS3Error("NoSuchBucket", "指定存储桶不存在", requestUri, HttpStatus.NOT_FOUND); } // ===================== 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 + 空响应体(核心对齐!)===================== 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(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 buildNativeS3Error("InternalError", "服务器代理异常", requestUri, HttpStatus.INTERNAL_SERVER_ERROR); } } /** * 兜底构造S3标准XML错误响应,兼容大疆设备解析 */ private ResponseEntity 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); } }