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);
|
// ===================== 核心修复:安全路径解析,替换替代截取,杜绝索引错误 =====================
|
// String contextPath = request.getContextPath();
|
String rawUri = URLDecoder.decode(request.getRequestURI(), StandardCharsets.UTF_8);
|
// 剔除上下文路径,得到纯业务路径
|
String purePath = rawUri.substring("/minioTransfer".length());
|
String bucketPrefix = "/" + bucketName;
|
|
log.info("中转MinIO 路径解析开始 | 原始URI:{} | 纯业务路径:{} | 桶前缀:{}", rawUri, purePath, bucketPrefix);
|
|
// 校验路径是否以桶前缀开头
|
if (!purePath.startsWith(bucketPrefix)) {
|
return buildNativeS3Error("InvalidRequest", "请求路径格式不匹配", rawUri, HttpStatus.BAD_REQUEST);
|
}
|
|
// 修复点:replace替换前缀,替代 substring 长度截取,彻底解决索引错位问题
|
String objectKey = purePath.replaceFirst("^" + bucketPrefix, "");
|
// 兜底处理空路径
|
if (objectKey.isEmpty()) {
|
objectKey = "/";
|
}
|
|
log.info("中转MinIO 路径解析成功 | 纯业务路径:{} | 桶名:{} | 最终对象路径:{}",
|
purePath, bucketName, objectKey);
|
|
// byte[] requestData = rawRequestBody == null ? new byte[0] : rawRequestBody;
|
|
try {
|
// ===================== 2. 复用原有逻辑:获取临时凭证 =====================
|
var credentials = ossService.getCredentials();
|
if (credentials == null || credentials.getAccessKeyId() == null || credentials.getSecurityToken() == null) {
|
log.error("临时凭证获取失败");
|
return buildNativeS3Error("InvalidCredentials", "临时凭证无效", rawUri, 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", "指定存储桶不存在", bucketName, 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(), rawUri, HttpStatus.INTERNAL_SERVER_ERROR);
|
} catch (Exception e) {
|
log.error("代理中转通用异常:{}", e.getMessage(), e);
|
return buildNativeS3Error("InternalError", "服务器代理异常", rawUri, 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);
|
}
|
}
|