feat(infra): S3 私有桶预签名核心能力
- S3FileClient.buildDomain() 修复 COS virtual-hosted-style 域名生成 - S3FileClient.presignGetUrl() 支持跨桶签名及 endpoint 校验 - FileApi.presignGetUrl() 修复 Feign nullable 参数注解 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,73 +1,73 @@
|
|||||||
package com.viewsh.module.infra.api.file;
|
package com.viewsh.module.infra.api.file;
|
||||||
|
|
||||||
import com.viewsh.framework.common.pojo.CommonResult;
|
import com.viewsh.framework.common.pojo.CommonResult;
|
||||||
import com.viewsh.module.infra.api.file.dto.FileCreateReqDTO;
|
import com.viewsh.module.infra.api.file.dto.FileCreateReqDTO;
|
||||||
import com.viewsh.module.infra.enums.ApiConstants;
|
import com.viewsh.module.infra.enums.ApiConstants;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||||
@Tag(name = "RPC 服务 - 文件")
|
@Tag(name = "RPC 服务 - 文件")
|
||||||
public interface FileApi {
|
public interface FileApi {
|
||||||
|
|
||||||
String PREFIX = ApiConstants.PREFIX + "/file";
|
String PREFIX = ApiConstants.PREFIX + "/file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存文件,并返回文件的访问路径
|
* 保存文件,并返回文件的访问路径
|
||||||
*
|
*
|
||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
* @return 文件路径
|
* @return 文件路径
|
||||||
*/
|
*/
|
||||||
default String createFile(byte[] content) {
|
default String createFile(byte[] content) {
|
||||||
return createFile(content, null, null, null);
|
return createFile(content, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存文件,并返回文件的访问路径
|
* 保存文件,并返回文件的访问路径
|
||||||
*
|
*
|
||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
* @param name 文件名称,允许空
|
* @param name 文件名称,允许空
|
||||||
* @return 文件路径
|
* @return 文件路径
|
||||||
*/
|
*/
|
||||||
default String createFile(byte[] content, String name) {
|
default String createFile(byte[] content, String name) {
|
||||||
return createFile(content, name, null, null);
|
return createFile(content, name, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存文件,并返回文件的访问路径
|
* 保存文件,并返回文件的访问路径
|
||||||
*
|
*
|
||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
* @param name 文件名称,允许空
|
* @param name 文件名称,允许空
|
||||||
* @param directory 目录,允许空
|
* @param directory 目录,允许空
|
||||||
* @param type 文件的 MIME 类型,允许空
|
* @param type 文件的 MIME 类型,允许空
|
||||||
* @return 文件路径
|
* @return 文件路径
|
||||||
*/
|
*/
|
||||||
default String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
|
default String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
|
||||||
String name, String directory, String type) {
|
String name, String directory, String type) {
|
||||||
return createFile(new FileCreateReqDTO().setName(name).setDirectory(directory).setType(type).setContent(content)).getCheckedData();
|
return createFile(new FileCreateReqDTO().setName(name).setDirectory(directory).setType(type).setContent(content)).getCheckedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(PREFIX + "/create")
|
@PostMapping(PREFIX + "/create")
|
||||||
@Operation(summary = "保存文件,并返回文件的访问路径")
|
@Operation(summary = "保存文件,并返回文件的访问路径")
|
||||||
CommonResult<String> createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO);
|
CommonResult<String> createFile(@Valid @RequestBody FileCreateReqDTO createReqDTO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文件预签名地址,用于读取
|
* 生成文件预签名地址,用于读取
|
||||||
*
|
*
|
||||||
* @param url 完整的文件访问地址
|
* @param url 完整的文件访问地址
|
||||||
* @param expirationSeconds 访问有效期,单位秒
|
* @param expirationSeconds 访问有效期,单位秒
|
||||||
* @return 文件预签名地址
|
* @return 文件预签名地址
|
||||||
*/
|
*/
|
||||||
@GetMapping(PREFIX + "/presigned-url")
|
@GetMapping(PREFIX + "/presigned-url")
|
||||||
@Operation(summary = "生成文件预签名地址,用于读取")
|
@Operation(summary = "生成文件预签名地址,用于读取")
|
||||||
CommonResult<String> presignGetUrl(@NotEmpty(message = "URL 不能为空") @RequestParam("url") String url,
|
CommonResult<String> presignGetUrl(@NotEmpty(message = "URL 不能为空") @RequestParam("url") String url,
|
||||||
Integer expirationSeconds);
|
@RequestParam(value = "expirationSeconds", required = false) Integer expirationSeconds);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,233 +1,279 @@
|
|||||||
package com.viewsh.module.infra.framework.file.core.client.s3;
|
package com.viewsh.module.infra.framework.file.core.client.s3;
|
||||||
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.util.BooleanUtil;
|
import cn.hutool.core.util.BooleanUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.viewsh.framework.common.util.http.HttpUtils;
|
import com.viewsh.framework.common.util.http.HttpUtils;
|
||||||
import com.viewsh.module.infra.framework.file.core.client.AbstractFileClient;
|
import com.viewsh.module.infra.framework.file.core.client.AbstractFileClient;
|
||||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
import software.amazon.awssdk.regions.Region;
|
import software.amazon.awssdk.regions.Region;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||||
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
|
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||||
|
|
||||||
private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24);
|
private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24);
|
||||||
|
|
||||||
private S3Client client;
|
private S3Client client;
|
||||||
private S3Presigner presigner;
|
private S3Presigner presigner;
|
||||||
|
|
||||||
public S3FileClient(Long id, S3FileClientConfig config) {
|
public S3FileClient(Long id, S3FileClientConfig config) {
|
||||||
super(id, config);
|
super(id, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doInit() {
|
protected void doInit() {
|
||||||
// 补全 domain
|
// 补全 domain
|
||||||
if (StrUtil.isEmpty(config.getDomain())) {
|
if (StrUtil.isEmpty(config.getDomain())) {
|
||||||
config.setDomain(buildDomain());
|
config.setDomain(buildDomain());
|
||||||
}
|
}
|
||||||
// 初始化 S3 客户端
|
// 初始化 S3 客户端
|
||||||
// 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
// 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
||||||
String regionStr = resolveRegion();
|
String regionStr = resolveRegion();
|
||||||
Region region = Region.of(regionStr);
|
Region region = Region.of(regionStr);
|
||||||
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
||||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
|
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
|
||||||
URI endpoint = URI.create(buildEndpoint());
|
URI endpoint = URI.create(buildEndpoint());
|
||||||
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
|
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
|
||||||
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
|
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
|
||||||
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
|
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
|
||||||
.build();
|
.build();
|
||||||
client = S3Client.builder()
|
client = S3Client.builder()
|
||||||
.credentialsProvider(credentialsProvider)
|
.credentialsProvider(credentialsProvider)
|
||||||
.region(region)
|
.region(region)
|
||||||
.endpointOverride(endpoint)
|
.endpointOverride(endpoint)
|
||||||
.serviceConfiguration(serviceConfiguration)
|
.serviceConfiguration(serviceConfiguration)
|
||||||
.build();
|
.build();
|
||||||
presigner = S3Presigner.builder()
|
presigner = S3Presigner.builder()
|
||||||
.credentialsProvider(credentialsProvider)
|
.credentialsProvider(credentialsProvider)
|
||||||
.region(region)
|
.region(region)
|
||||||
.endpointOverride(endpoint)
|
.endpointOverride(endpoint)
|
||||||
.serviceConfiguration(serviceConfiguration)
|
.serviceConfiguration(serviceConfiguration)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String upload(byte[] content, String path, String type) {
|
public String upload(byte[] content, String path, String type) {
|
||||||
// 构造 PutObjectRequest
|
// 构造 PutObjectRequest
|
||||||
PutObjectRequest putRequest = PutObjectRequest.builder()
|
PutObjectRequest putRequest = PutObjectRequest.builder()
|
||||||
.bucket(config.getBucket())
|
.bucket(config.getBucket())
|
||||||
.key(path)
|
.key(path)
|
||||||
.contentType(type)
|
.contentType(type)
|
||||||
.contentLength((long) content.length)
|
.contentLength((long) content.length)
|
||||||
.build();
|
.build();
|
||||||
// 上传文件
|
// 上传文件
|
||||||
client.putObject(putRequest, RequestBody.fromBytes(content));
|
client.putObject(putRequest, RequestBody.fromBytes(content));
|
||||||
// 拼接返回路径
|
// 拼接返回路径
|
||||||
return presignGetUrl(path, null);
|
return presignGetUrl(path, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String path) {
|
public void delete(String path) {
|
||||||
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
|
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
|
||||||
.bucket(config.getBucket())
|
.bucket(config.getBucket())
|
||||||
.key(path)
|
.key(path)
|
||||||
.build();
|
.build();
|
||||||
client.deleteObject(deleteRequest);
|
client.deleteObject(deleteRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] getContent(String path) {
|
public byte[] getContent(String path) {
|
||||||
GetObjectRequest getRequest = GetObjectRequest.builder()
|
GetObjectRequest getRequest = GetObjectRequest.builder()
|
||||||
.bucket(config.getBucket())
|
.bucket(config.getBucket())
|
||||||
.key(path)
|
.key(path)
|
||||||
.build();
|
.build();
|
||||||
return IoUtil.readBytes(client.getObject(getRequest));
|
return IoUtil.readBytes(client.getObject(getRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String presignPutUrl(String path) {
|
public String presignPutUrl(String path) {
|
||||||
return presigner.presignPutObject(PutObjectPresignRequest.builder()
|
return presigner.presignPutObject(PutObjectPresignRequest.builder()
|
||||||
.signatureDuration(EXPIRATION_DEFAULT)
|
.signatureDuration(EXPIRATION_DEFAULT)
|
||||||
.putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build())
|
.putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build())
|
||||||
.url().toString();
|
.url().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||||
// 1. 将 url 转换为 path
|
// 1. 将 url 转换为 path 和 bucket
|
||||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
String bucket = config.getBucket();
|
||||||
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
|
boolean crossBucket = false;
|
||||||
|
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
||||||
// 2.1 情况一:公开访问:无需签名
|
// 兼容:域名不匹配时(如同账号不同桶),从 URL 中解析 bucket 和 path
|
||||||
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
if (path.equals(url) && (path.startsWith("http://") || path.startsWith("https://"))) {
|
||||||
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
|
try {
|
||||||
return config.getDomain() + "/" + path;
|
URI uri = URI.create(url);
|
||||||
}
|
String host = uri.getHost();
|
||||||
|
String endpointHost = getEndpointHost();
|
||||||
// 2.2 情况二:私有访问:生成 GET 预签名 URL
|
// 校验是否同一 S3 服务(如 cos.ap-shanghai.myqcloud.com),避免误签非本服务 URL
|
||||||
String finalPath = path;
|
if (host != null && endpointHost != null && host.endsWith("." + endpointHost)) {
|
||||||
Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT;
|
bucket = StrUtil.subBefore(host, ".", false);
|
||||||
URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder()
|
crossBucket = true;
|
||||||
.signatureDuration(expiration)
|
path = StrUtil.removePrefix(uri.getPath(), "/");
|
||||||
.getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build())
|
} else {
|
||||||
.url();
|
// 非同服务 URL,原样返回
|
||||||
return signedUrl.toString();
|
return url;
|
||||||
}
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
/**
|
return url;
|
||||||
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
}
|
||||||
*
|
}
|
||||||
* @return Domain 地址
|
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
|
||||||
*/
|
|
||||||
private String buildDomain() {
|
// 2.1 情况一:公开访问:无需签名
|
||||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
|
||||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
// 跨桶 URL 原样返回,不拼接本桶 domain
|
||||||
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
|
if (crossBucket) {
|
||||||
}
|
return url;
|
||||||
// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
|
}
|
||||||
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
return config.getDomain() + "/" + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 2.2 情况二:私有访问:生成 GET 预签名 URL
|
||||||
* 节点地址补全协议头
|
String finalPath = path;
|
||||||
*
|
String finalBucket = bucket;
|
||||||
* @return 节点地址
|
Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT;
|
||||||
*/
|
URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder()
|
||||||
private String buildEndpoint() {
|
.signatureDuration(expiration)
|
||||||
// 如果已经是 http 或者 https,则不进行拼接
|
.getObjectRequest(b -> b.bucket(finalBucket).key(finalPath)).build())
|
||||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
.url();
|
||||||
return config.getEndpoint();
|
return signedUrl.toString();
|
||||||
}
|
}
|
||||||
return StrUtil.format("https://{}", config.getEndpoint());
|
|
||||||
}
|
/**
|
||||||
|
* 获取 endpoint 的 host 部分(不含协议头)
|
||||||
/**
|
* 例如:https://cos.ap-shanghai.myqcloud.com → cos.ap-shanghai.myqcloud.com
|
||||||
* 解析 AWS 区域
|
*/
|
||||||
* 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
private String getEndpointHost() {
|
||||||
*
|
String endpoint = config.getEndpoint();
|
||||||
* @return 区域字符串
|
if (HttpUtil.isHttp(endpoint) || HttpUtil.isHttps(endpoint)) {
|
||||||
*/
|
try {
|
||||||
private String resolveRegion() {
|
return URI.create(endpoint).getHost();
|
||||||
// 1. 如果配置了 region,直接使用
|
} catch (Exception e) {
|
||||||
if (StrUtil.isNotEmpty(config.getRegion())) {
|
return null;
|
||||||
return config.getRegion();
|
}
|
||||||
}
|
}
|
||||||
|
return endpoint;
|
||||||
// 2.1 尝试从 endpoint 中解析 region
|
}
|
||||||
String endpoint = config.getEndpoint();
|
|
||||||
if (StrUtil.isEmpty(endpoint)) {
|
/**
|
||||||
return "us-east-1";
|
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
||||||
}
|
*
|
||||||
|
* @return Domain 地址
|
||||||
// 2.2 移除协议头(http:// 或 https://)
|
*/
|
||||||
String host = endpoint;
|
private String buildDomain() {
|
||||||
if (HttpUtil.isHttp(endpoint) || HttpUtil.isHttps(endpoint)) {
|
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||||
try {
|
// Path-Style(如 MinIO):{endpoint}/{bucket}
|
||||||
host = URI.create(endpoint).getHost();
|
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
|
||||||
} catch (Exception e) {
|
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
|
||||||
// 解析失败,使用默认值
|
}
|
||||||
return "us-east-1";
|
// Virtual-Hosted-Style(如腾讯云 COS、阿里云 OSS):{scheme}://{bucket}.{host}
|
||||||
}
|
URI uri = URI.create(config.getEndpoint());
|
||||||
}
|
return StrUtil.format("{}://{}.{}", uri.getScheme(), config.getBucket(), uri.getHost());
|
||||||
if (StrUtil.isEmpty(host)) {
|
}
|
||||||
return "us-east-1";
|
// 没有协议头,直接拼接为虚拟主机风格
|
||||||
}
|
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||||
|
}
|
||||||
// 3.1 AWS S3 格式:s3.us-west-2.amazonaws.com 或 s3.amazonaws.com
|
|
||||||
if (host.contains("amazonaws.com")) {
|
/**
|
||||||
// 匹配 s3.{region}.amazonaws.com 格式
|
* 节点地址补全协议头
|
||||||
if (host.startsWith("s3.") && host.contains(".amazonaws.com")) {
|
*
|
||||||
String regionPart = host.substring(3, host.indexOf(".amazonaws.com"));
|
* @return 节点地址
|
||||||
if (StrUtil.isNotEmpty(regionPart) && !regionPart.equals("accelerate")) {
|
*/
|
||||||
return regionPart;
|
private String buildEndpoint() {
|
||||||
}
|
// 如果已经是 http 或者 https,则不进行拼接
|
||||||
}
|
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||||
// s3.amazonaws.com 或 s3-accelerate.amazonaws.com 使用默认值
|
return config.getEndpoint();
|
||||||
return "us-east-1";
|
}
|
||||||
}
|
return StrUtil.format("https://{}", config.getEndpoint());
|
||||||
// 3.2 阿里云 OSS 格式:oss-cn-beijing.aliyuncs.com
|
}
|
||||||
if (host.contains(S3FileClientConfig.ENDPOINT_ALIYUN)) {
|
|
||||||
// 匹配 oss-{region}.aliyuncs.com 格式
|
/**
|
||||||
if (host.startsWith("oss-") && host.contains("." + S3FileClientConfig.ENDPOINT_ALIYUN)) {
|
* 解析 AWS 区域
|
||||||
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_ALIYUN));
|
* 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
||||||
if (StrUtil.isNotEmpty(regionPart)) {
|
*
|
||||||
return regionPart;
|
* @return 区域字符串
|
||||||
}
|
*/
|
||||||
}
|
private String resolveRegion() {
|
||||||
}
|
// 1. 如果配置了 region,直接使用
|
||||||
// 3.3 腾讯云 COS 格式:cos.ap-shanghai.myqcloud.com
|
if (StrUtil.isNotEmpty(config.getRegion())) {
|
||||||
if (host.contains(S3FileClientConfig.ENDPOINT_TENCENT)) {
|
return config.getRegion();
|
||||||
// 匹配 cos.{region}.myqcloud.com 格式
|
}
|
||||||
if (host.startsWith("cos.") && host.contains("." + S3FileClientConfig.ENDPOINT_TENCENT)) {
|
|
||||||
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_TENCENT));
|
// 2.1 尝试从 endpoint 中解析 region
|
||||||
if (StrUtil.isNotEmpty(regionPart)) {
|
String endpoint = config.getEndpoint();
|
||||||
return regionPart;
|
if (StrUtil.isEmpty(endpoint)) {
|
||||||
}
|
return "us-east-1";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 2.2 移除协议头(http:// 或 https://)
|
||||||
// 3.4 其他情况(MinIO、七牛云等)使用默认值
|
String host = endpoint;
|
||||||
return "us-east-1";
|
if (HttpUtil.isHttp(endpoint) || HttpUtil.isHttps(endpoint)) {
|
||||||
}
|
try {
|
||||||
|
host = URI.create(endpoint).getHost();
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
// 解析失败,使用默认值
|
||||||
|
return "us-east-1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StrUtil.isEmpty(host)) {
|
||||||
|
return "us-east-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.1 AWS S3 格式:s3.us-west-2.amazonaws.com 或 s3.amazonaws.com
|
||||||
|
if (host.contains("amazonaws.com")) {
|
||||||
|
// 匹配 s3.{region}.amazonaws.com 格式
|
||||||
|
if (host.startsWith("s3.") && host.contains(".amazonaws.com")) {
|
||||||
|
String regionPart = host.substring(3, host.indexOf(".amazonaws.com"));
|
||||||
|
if (StrUtil.isNotEmpty(regionPart) && !regionPart.equals("accelerate")) {
|
||||||
|
return regionPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// s3.amazonaws.com 或 s3-accelerate.amazonaws.com 使用默认值
|
||||||
|
return "us-east-1";
|
||||||
|
}
|
||||||
|
// 3.2 阿里云 OSS 格式:oss-cn-beijing.aliyuncs.com
|
||||||
|
if (host.contains(S3FileClientConfig.ENDPOINT_ALIYUN)) {
|
||||||
|
// 匹配 oss-{region}.aliyuncs.com 格式
|
||||||
|
if (host.startsWith("oss-") && host.contains("." + S3FileClientConfig.ENDPOINT_ALIYUN)) {
|
||||||
|
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_ALIYUN));
|
||||||
|
if (StrUtil.isNotEmpty(regionPart)) {
|
||||||
|
return regionPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3.3 腾讯云 COS 格式:cos.ap-shanghai.myqcloud.com
|
||||||
|
if (host.contains(S3FileClientConfig.ENDPOINT_TENCENT)) {
|
||||||
|
// 匹配 cos.{region}.myqcloud.com 格式
|
||||||
|
if (host.startsWith("cos.") && host.contains("." + S3FileClientConfig.ENDPOINT_TENCENT)) {
|
||||||
|
String regionPart = host.substring(4, host.indexOf("." + S3FileClientConfig.ENDPOINT_TENCENT));
|
||||||
|
if (StrUtil.isNotEmpty(regionPart)) {
|
||||||
|
return regionPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.4 其他情况(MinIO、七牛云等)使用默认值
|
||||||
|
return "us-east-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user