| | |
| | | import com.ard.common.config.OssConfiguration; |
| | | import com.ard.oss.model.CredentialsDTO; |
| | | import com.ard.oss.service.impl.OssServiceContext; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.media.model.FileUploadDTO; |
| | | import com.ard.sample.utils.S3PutObjectResponse; |
| | | 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.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.web.bind.annotation.*; |
| | | |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.IOException; |
| | | import java.security.InvalidKeyException; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.time.OffsetDateTime; |
| | |
| | | @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"; |
| | | |
| | | /** |
| | | * 核心接口:中转大疆的 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 响应(返回给大疆,衔接后续流程) |
| | | */ |
| | | @ApiOperation("minio上传") |
| | | @PutMapping("/{bucketName}/{objectName}") |
| | | public ResponseEntity<S3PutObjectResponse> proxyUpload( |
| | | @ApiOperation("minio上传/创建目录") |
| | | // 关键修改:{objectName:.*} 支持路径中包含斜杠,适配多层级文件/目录 |
| | | @PutMapping(value = "/{bucketName}/{objectName:.*}", produces = MediaType.APPLICATION_XML_VALUE) |
| | | public ResponseEntity<?> proxyUpload( |
| | | @PathVariable String bucketName, |
| | | @PathVariable String objectName, |
| | | @RequestBody byte[] fileContent, |
| | | // 允许请求体为空(目录创建场景) |
| | | @RequestBody(required = false) byte[] fileContent, |
| | | @RequestHeader(value = "Content-Type", defaultValue = "application/octet-stream") String contentType |
| | | ) { |
| | | try { |
| | | log.info("开始中转上传文件到 MinIO,收到参数:bucketName={}, objectName={}",bucketName,objectName); |
| | | // 步骤 1:获取 MinIO HTTPS 临时凭证(复用你的已有工具类,已验证可行) |
| | | // 基础参数校验 |
| | | 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) { |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); |
| | | log.error("MinIO临时凭证获取失败"); |
| | | return buildS3Error("InvalidCredentials", "临时凭证无效", "/" + bucketName + "/" + objectName); |
| | | } |
| | | |
| | | // 步骤 2:构建 MinioClient(适配 SDK 8.3.7,忽略私有 SSL 证书,使用临时凭证) |
| | | // ===================== 保留你原有逻辑:构建MinioClient ===================== |
| | | 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 类型 |
| | | .bucket(bucketName) |
| | | .object(objectName) |
| | | .stream(inputStream, actualContent.length, -1) |
| | | .contentType(contentType) |
| | | .build() |
| | | ); |
| | | log.info("文件上传成功,文件名:{}",objectName); |
| | | |
| | | // 步骤 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) |
| | | ); |
| | | log.info("构造回复大疆机场的响应信息-S3PutObjectResponse:{}",successResponse); |
| | | // 生成标准响应参数 |
| | | String eTag = "\"" + DigestUtils.md5Hex(actualContent) + "\""; |
| | | String lastModified = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
| | | S3PutObjectResponse successResponse = new S3PutObjectResponse(eTag, lastModified); |
| | | |
| | | // 步骤 5:返回 200 OK + XML 格式响应(Spring Boot 自动转换实体类为 XML) |
| | | return ResponseEntity.status(HttpStatus.OK) |
| | | log.info("操作成功 | 路径:{} | ETag:{}", objectName, eTag); |
| | | return ResponseEntity.ok() |
| | | .contentType(MediaType.APPLICATION_XML) |
| | | .body(successResponse); |
| | | |
| | | } 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); |
| | | } 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) { |
| | | // 其他通用异常 |
| | | e.printStackTrace(); |
| | | log.info("MinIO 异常:{}",e.getMessage()); |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); |
| | | log.error("通用服务异常: {}", e.getMessage(), e); |
| | | return buildS3Error("InternalError", "服务器内部异常", "/" + bucketName + "/" + objectName); |
| | | } |
| | | } |
| | | |
| | | @ApiOperation("测试") |
| | | @PostMapping("/test") |
| | | public ResponseResult fastUpload() { |
| | | return ResponseResult.success(); |
| | | /** |
| | | * 构造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); |
| | | } |
| | | } |
| | | } |