使用 minio client 替代 amazon 客户端,进行 S3 的对接
This commit is contained in:
parent
62f7d34952
commit
34a7399a65
|
@ -56,7 +56,7 @@
|
|||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<jsch.version>0.1.55</jsch.version>
|
||||
<!-- 三方云服务相关 -->
|
||||
<s3.version>2.17.147</s3.version>
|
||||
<minio.version>8.2.2</minio.version>
|
||||
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
|
||||
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
|
||||
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
||||
|
@ -514,9 +514,9 @@
|
|||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>${s3.version}</version>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SMS SDK begin -->
|
||||
|
|
|
@ -63,8 +63,8 @@
|
|||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
|
|
|
@ -20,15 +20,17 @@ public interface FileClient {
|
|||
* @param content 文件流
|
||||
* @param path 相对路径
|
||||
* @return 完整路径,即 HTTP 访问地址
|
||||
* @throws Exception 上传文件时,抛出 Exception 异常
|
||||
*/
|
||||
String upload(byte[] content, String path);
|
||||
String upload(byte[] content, String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @throws Exception 删除文件时,抛出 Exception 异常
|
||||
*/
|
||||
void delete(String path);
|
||||
void delete(String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件的内容
|
||||
|
@ -36,6 +38,6 @@ public interface FileClient {
|
|||
* @param path 相对路径
|
||||
* @return 文件的内容
|
||||
*/
|
||||
byte[] getContent(String path);
|
||||
byte[] getContent(String path) throws Exception;
|
||||
|
||||
}
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
import io.minio.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_QINIU;
|
||||
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
|
||||
|
||||
/**
|
||||
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
||||
|
@ -24,7 +19,7 @@ import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.
|
|||
*/
|
||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
private S3Client client;
|
||||
private MinioClient client;
|
||||
|
||||
public S3FileClient(Long id, S3FileClientConfig config) {
|
||||
super(id, config);
|
||||
|
@ -34,34 +29,27 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
protected void doInit() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(createDomain());
|
||||
config.setDomain(buildDomain());
|
||||
}
|
||||
// 初始化客户端
|
||||
client = S3Client.builder()
|
||||
.serviceConfiguration(sb -> sb.pathStyleAccessEnabled(false) // 关闭路径风格
|
||||
.chunkedEncodingEnabled(false)) // 禁用 chunk
|
||||
.endpointOverride(createURI()) // 上传地址
|
||||
.region(Region.of(config.getRegion())) // Region
|
||||
.credentialsProvider(StaticCredentialsProvider.create( // 认证密钥
|
||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())))
|
||||
.overrideConfiguration(cb -> cb.addExecutionInterceptor(new S3ModifyPathInterceptor(config.getBucket())))
|
||||
client = MinioClient.builder()
|
||||
.endpoint(buildEndpointURL()) // Endpoint URL
|
||||
.region(buildRegion()) // Region
|
||||
.credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 endpoint 构建调用云服务的 URI 地址
|
||||
* 基于 endpoint 构建调用云服务的 URL 地址
|
||||
*
|
||||
* @return URI 地址
|
||||
*/
|
||||
private URI createURI() {
|
||||
String uri;
|
||||
// 如果是七牛,无需拼接 bucket
|
||||
if (config.getEndpoint().contains(ENDPOINT_QINIU)) {
|
||||
uri = StrUtil.format("https://{}", config.getEndpoint());
|
||||
} else {
|
||||
uri = StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
private String buildEndpointURL() {
|
||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||
return config.getEndpoint();
|
||||
}
|
||||
return URI.create(uri);
|
||||
return StrUtil.format("https://{}", config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,35 +57,56 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
*
|
||||
* @return Domain 地址
|
||||
*/
|
||||
private String createDomain() {
|
||||
private String buildDomain() {
|
||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
|
||||
}
|
||||
// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
|
||||
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 bucket 构建 region 地区
|
||||
*
|
||||
* @return region 地区
|
||||
*/
|
||||
private String buildRegion() {
|
||||
// 阿里云必须有 region,否则会报错
|
||||
if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
|
||||
return StrUtil.subBefore(config.getEndpoint(), '.', false)
|
||||
.replaceAll("-internal", ""); // 去除内网 Endpoint 的后缀
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
public String upload(byte[] content, String path) throws Exception {
|
||||
// 执行上传
|
||||
PutObjectRequest.Builder request = PutObjectRequest.builder()
|
||||
client.putObject(PutObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
client.putObject(request.build(), RequestBody.fromBytes(content));
|
||||
.object(path) // 相对路径作为 key
|
||||
.stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
|
||||
.build());
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
DeleteObjectRequest.Builder request = DeleteObjectRequest.builder()
|
||||
public void delete(String path) throws Exception {
|
||||
client.removeObject(RemoveObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
client.deleteObject(request.build());
|
||||
.object(path) // 相对路径作为 key
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
GetObjectRequest.Builder request = GetObjectRequest.builder()
|
||||
public byte[] getContent(String path) throws Exception {
|
||||
GetObjectResponse response = client.getObject(GetObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.key(path); // 相对路径作为 key
|
||||
return client.getObjectAsBytes(request.build()).asByteArray();
|
||||
.object(path) // 相对路径作为 key
|
||||
.build());
|
||||
return IoUtil.readBytes(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,14 +18,15 @@ import javax.validation.constraints.NotNull;
|
|||
public class S3FileClientConfig implements FileClientConfig {
|
||||
|
||||
public static final String ENDPOINT_QINIU = "qiniucs.com";
|
||||
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
|
||||
|
||||
/**
|
||||
* 节点地址
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
|
||||
* 3. 腾讯云:
|
||||
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
|
||||
* 5. 华为云:
|
||||
* 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS
|
||||
*/
|
||||
@NotNull(message = "endpoint 不能为空")
|
||||
private String endpoint;
|
||||
|
@ -35,19 +36,15 @@ public class S3FileClientConfig implements FileClientConfig {
|
|||
* 2. 阿里云:https://help.aliyun.com/document_detail/31836.html
|
||||
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
|
||||
* 5. 华为云:
|
||||
* 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html
|
||||
*/
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
/**
|
||||
* 区域
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
|
||||
* 3. 腾讯云:
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
|
||||
* 5. 华为云:
|
||||
*/
|
||||
@NotNull(message = "region 不能为空")
|
||||
// @NotNull(message = "region 不能为空")
|
||||
@Deprecated
|
||||
private String region;
|
||||
/**
|
||||
* 存储 Bucket
|
||||
|
@ -58,10 +55,10 @@ public class S3FileClientConfig implements FileClientConfig {
|
|||
/**
|
||||
* 访问 Key
|
||||
* 1. MinIO:
|
||||
* 2. 阿里云:
|
||||
* 2. 阿里云:https://ram.console.aliyun.com/manage/ak
|
||||
* 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
|
||||
* 4. 七牛云:https://portal.qiniu.com/user/key
|
||||
* 5. 华为云:
|
||||
* 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
|
||||
*/
|
||||
@NotNull(message = "accessKey 不能为空")
|
||||
private String accessKey;
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import software.amazon.awssdk.core.interceptor.Context;
|
||||
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
|
||||
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
|
||||
import software.amazon.awssdk.http.SdkHttpRequest;
|
||||
|
||||
/**
|
||||
* S3 修改路径的拦截器,移除多余的 Bucket 前缀。
|
||||
* 如果不使用该拦截器,希望上传的路径是 /tudou.jpg 时,会被添加成 /bucket/tudou.jpg
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class S3ModifyPathInterceptor implements ExecutionInterceptor {
|
||||
|
||||
private final String bucket;
|
||||
|
||||
public S3ModifyPathInterceptor(String bucket) {
|
||||
this.bucket = "/" + bucket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
|
||||
SdkHttpRequest request = context.httpRequest();
|
||||
SdkHttpRequest.Builder rb = SdkHttpRequest.builder().protocol(request.protocol()).host(request.host()).port(request.port())
|
||||
.method(request.method()).headers(request.headers()).rawQueryParameters(request.rawQueryParameters());
|
||||
// 移除 path 前的 bucket 路径
|
||||
if (request.encodedPath().startsWith(bucket)) {
|
||||
rb.encodedPath(request.encodedPath().substring(bucket.length()));
|
||||
} else {
|
||||
rb.encodedPath(request.encodedPath());
|
||||
}
|
||||
return rb.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -3,11 +3,13 @@ package cn.iocoder.yudao.framework.file.core.client.ftp;
|
|||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.extra.ftp.FtpMode;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FtpFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
FtpFileClientConfig config = new FtpFileClientConfig();
|
||||
|
|
|
@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.local;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LocalFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
LocalFileClientConfig config = new LocalFileClientConfig();
|
||||
|
|
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.file.core.client.s3;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -11,9 +10,26 @@ import javax.validation.Validation;
|
|||
|
||||
public class S3FileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled // MinIO,如果要集成测试,可以注释本行
|
||||
public void testMinIO() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey("admin");
|
||||
config.setAccessSecret("password");
|
||||
config.setBucket("yudaoyuanma");
|
||||
config.setDomain(null);
|
||||
// 默认 9000 endpoint
|
||||
config.setEndpoint("http://127.0.0.1:9000");
|
||||
config.setRegion("us-east-1");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
|
||||
public void testAliyun() {
|
||||
public void testAliyun() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
|
||||
|
@ -29,7 +45,7 @@ public class S3FileClientTest {
|
|||
|
||||
@Test
|
||||
@Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
|
||||
public void testQCloud() {
|
||||
public void testQCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
|
||||
|
@ -38,7 +54,6 @@ public class S3FileClientTest {
|
|||
config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("cos.ap-shanghai.myqcloud.com");
|
||||
config.setRegion("ap-shanghai");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
|
@ -46,7 +61,7 @@ public class S3FileClientTest {
|
|||
|
||||
@Test
|
||||
@Disabled // 七牛云存储,如果要集成测试,可以注释本行
|
||||
public void testQiniu() {
|
||||
public void testQiniu() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
|
||||
|
@ -62,11 +77,24 @@ public class S3FileClientTest {
|
|||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
private void testExecuteUpload(S3FileClientConfig config) {
|
||||
// 补全配置
|
||||
if (config.getRegion() == null) {
|
||||
config.setRegion(StrUtil.subBefore(config.getEndpoint(), '.', false));
|
||||
}
|
||||
@Test
|
||||
@Disabled // 华为云存储,如果要集成测试,可以注释本行
|
||||
public void testHuaweiCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
|
||||
config.setBucket("yudao");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
private void testExecuteUpload(S3FileClientConfig config) throws Exception {
|
||||
// 校验配置
|
||||
ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
|
||||
// 创建 Client
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
|
@ -77,9 +105,9 @@ public class S3FileClientTest {
|
|||
String fullPath = client.upload(content, path);
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
// 读取文件
|
||||
if (false) {
|
||||
if (true) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
System.out.println("文件内容:" + bytes.length);
|
||||
}
|
||||
// 删除文件
|
||||
if (false) {
|
||||
|
|
|
@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.sftp;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SftpFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
SftpFileClientConfig config = new SftpFileClientConfig();
|
||||
|
|
Loading…
Reference in New Issue