zhaowenyu
2026-02-04 db82545cc79a8c35a7a3836b38919c574e3ee4d9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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<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,
            // 透传大疆设备原始请求体,不修改、不封装
            @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<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);
    }
}