zhaowenyu
2026-02-04 1f6e8a56efec18d9343f6ac0f93d4cf21e9ab668
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
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.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.credentials.StaticProvider;
import io.minio.errors.MinioException;
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
 
/**
 * 适配 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;
 
    @ApiOperation("minio上传/创建目录")
    // 关键修改:{objectName:.*} 支持路径中包含斜杠,适配多层级文件/目录
    @PutMapping(value = "/{bucketName}/{objectName:.*}", produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<?> proxyUpload(
            @PathVariable String bucketName,
            @PathVariable String objectName,
            // 允许请求体为空(目录创建场景)
            @RequestBody(required = false) byte[] fileContent,
            @RequestHeader(value = "Content-Type", defaultValue = "application/octet-stream") String contentType
    ) {
        // 基础参数校验
        if (bucketName.isBlank() || objectName.isBlank()) {
            return buildS3Error("InvalidRequest", "存储桶/对象名称不能为空", "/" + bucketName + "/" + objectName);
        }
 
        // 标识:是否为目录创建请求(对象名以 / 结尾)
        boolean isCreateDirectory = objectName.endsWith("/");
        // 处理空数据:目录创建/空请求体时,使用空字节数组
        byte[] actualContent = (fileContent == null) ? new byte[0] : fileContent;
 
        log.info("开始代理操作 | 存储桶:{} | 对象路径:{} | 操作类型:{}",
                bucketName, objectName, isCreateDirectory ? "创建目录" : "上传文件");
 
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(actualContent)) {
            // ===================== 保留你原有逻辑:获取临时凭证 =====================
            CredentialsDTO credentials = ossService.getCredentials();
            if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
                log.error("MinIO临时凭证获取失败");
                return buildS3Error("InvalidCredentials", "临时凭证无效", "/" + bucketName + "/" + objectName);
            }
 
            // ===================== 保留你原有逻辑:构建MinioClient =====================
            MinioClient minioClient = MinioClient.builder()
                    .endpoint(OssConfiguration.endpoint)
                    .credentialsProvider(new StaticProvider(
                            credentials.getAccessKeyId(),
                            credentials.getAccessKeySecret(),
                            credentials.getSecurityToken()
                    ))
                    .build();
 
            // ===================== 核心上传逻辑:兼容文件/目录/多层级路径 =====================
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .stream(inputStream, actualContent.length, -1)
                            .contentType(contentType)
                            .build()
            );
 
            // 生成标准响应参数
            String eTag = "\"" + DigestUtils.md5Hex(actualContent) + "\"";
            String lastModified = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
            S3PutObjectResponse successResponse = new S3PutObjectResponse(eTag, lastModified);
 
            log.info("操作成功 | 路径:{} | ETag:{}", objectName, eTag);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_XML)
                    .body(successResponse);
 
        }  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 (Exception e) {
            log.error("通用服务异常: {}", e.getMessage(), e);
            return buildS3Error("InternalError", "服务器内部异常", "/" + bucketName + "/" + objectName);
        }
    }
 
    /**
     * 构造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)
                .contentType(MediaType.APPLICATION_XML)
                .body(errorResponse);
    }
}