使用 minio client 替代 amazon 客户端,进行 S3 的对接

pull/2/head
YunaiV 2022-03-19 17:27:35 +08:00
parent 62f7d34952
commit 34a7399a65
10 changed files with 116 additions and 110 deletions

View File

@ -56,7 +56,7 @@
<commons-net.version>3.8.0</commons-net.version> <commons-net.version>3.8.0</commons-net.version>
<jsch.version>0.1.55</jsch.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-core.version>4.5.25</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.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> <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
@ -514,9 +514,9 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>software.amazon.awssdk</groupId> <groupId>io.minio</groupId>
<artifactId>s3</artifactId> <artifactId>minio</artifactId>
<version>${s3.version}</version> <version>${minio.version}</version>
</dependency> </dependency>
<!-- SMS SDK begin --> <!-- SMS SDK begin -->

View File

@ -63,8 +63,8 @@
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<dependency> <dependency>
<groupId>software.amazon.awssdk</groupId> <groupId>io.minio</groupId>
<artifactId>s3</artifactId> <artifactId>minio</artifactId>
</dependency> </dependency>
<!-- Test 测试相关 --> <!-- Test 测试相关 -->

View File

@ -20,15 +20,17 @@ public interface FileClient {
* @param content * @param content
* @param path * @param path
* @return HTTP 访 * @return HTTP 访
* @throws Exception Exception
*/ */
String upload(byte[] content, String path); String upload(byte[] content, String path) throws Exception;
/** /**
* *
* *
* @param path * @param path
* @throws Exception Exception
*/ */
void delete(String path); void delete(String path) throws Exception;
/** /**
* *
@ -36,6 +38,6 @@ public interface FileClient {
* @param path * @param path
* @return * @return
*/ */
byte[] getContent(String path); byte[] getContent(String path) throws Exception;
} }

View File

@ -1,19 +1,14 @@
package cn.iocoder.yudao.framework.file.core.client.s3; package cn.iocoder.yudao.framework.file.core.client.s3;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient; import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import io.minio.*;
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 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 * S3 MinIO
@ -24,7 +19,7 @@ import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.
*/ */
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> { public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
private S3Client client; private MinioClient client;
public S3FileClient(Long id, S3FileClientConfig config) { public S3FileClient(Long id, S3FileClientConfig config) {
super(id, config); super(id, config);
@ -34,34 +29,27 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
protected void doInit() { protected void doInit() {
// 补全 domain // 补全 domain
if (StrUtil.isEmpty(config.getDomain())) { if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(createDomain()); config.setDomain(buildDomain());
} }
// 初始化客户端 // 初始化客户端
client = S3Client.builder() client = MinioClient.builder()
.serviceConfiguration(sb -> sb.pathStyleAccessEnabled(false) // 关闭路径风格 .endpoint(buildEndpointURL()) // Endpoint URL
.chunkedEncodingEnabled(false)) // 禁用 chunk .region(buildRegion()) // Region
.endpointOverride(createURI()) // 上传地址 .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
.region(Region.of(config.getRegion())) // Region
.credentialsProvider(StaticCredentialsProvider.create( // 认证密钥
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret())))
.overrideConfiguration(cb -> cb.addExecutionInterceptor(new S3ModifyPathInterceptor(config.getBucket())))
.build(); .build();
} }
/** /**
* endpoint URI * endpoint URL
* *
* @return URI * @return URI
*/ */
private URI createURI() { private String buildEndpointURL() {
String uri; // 如果已经是 http 或者 https则不进行拼接.主要适配 MinIO
// 如果是七牛,无需拼接 bucket if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
if (config.getEndpoint().contains(ENDPOINT_QINIU)) { return config.getEndpoint();
uri = StrUtil.format("https://{}", config.getEndpoint());
} else {
uri = StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
} }
return URI.create(uri); return StrUtil.format("https://{}", config.getEndpoint());
} }
/** /**
@ -69,35 +57,56 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
* *
* @return Domain * @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()); 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 @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 必须传递 .bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key .object(path) // 相对路径作为 key
client.putObject(request.build(), RequestBody.fromBytes(content)); .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
.build());
// 拼接返回路径 // 拼接返回路径
return config.getDomain() + "/" + path; return config.getDomain() + "/" + path;
} }
@Override @Override
public void delete(String path) { public void delete(String path) throws Exception {
DeleteObjectRequest.Builder request = DeleteObjectRequest.builder() client.removeObject(RemoveObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递 .bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key .object(path) // 相对路径作为 key
client.deleteObject(request.build()); .build());
} }
@Override @Override
public byte[] getContent(String path) { public byte[] getContent(String path) throws Exception {
GetObjectRequest.Builder request = GetObjectRequest.builder() GetObjectResponse response = client.getObject(GetObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递 .bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key .object(path) // 相对路径作为 key
return client.getObjectAsBytes(request.build()).asByteArray(); .build());
return IoUtil.readBytes(response);
} }
} }

View File

@ -18,14 +18,15 @@ import javax.validation.constraints.NotNull;
public class S3FileClientConfig implements FileClientConfig { public class S3FileClientConfig implements FileClientConfig {
public static final String ENDPOINT_QINIU = "qiniucs.com"; public static final String ENDPOINT_QINIU = "qiniucs.com";
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
/** /**
* *
* 1. MinIO * 1. MinIO
* 2. https://help.aliyun.com/document_detail/31837.html * 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 * 4. https://developer.qiniu.com/kodo/4088/s3-access-domainname
* 5. * 5. https://developer.huaweicloud.com/endpoint?OBS
*/ */
@NotNull(message = "endpoint 不能为空") @NotNull(message = "endpoint 不能为空")
private String endpoint; private String endpoint;
@ -35,19 +36,15 @@ public class S3FileClientConfig implements FileClientConfig {
* 2. https://help.aliyun.com/document_detail/31836.html * 2. https://help.aliyun.com/document_detail/31836.html
* 3. https://cloud.tencent.com/document/product/436/11142 * 3. https://cloud.tencent.com/document/product/436/11142
* 4. https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name * 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 格式") @URL(message = "domain 必须是 URL 格式")
private String domain; 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; private String region;
/** /**
* Bucket * Bucket
@ -58,10 +55,10 @@ public class S3FileClientConfig implements FileClientConfig {
/** /**
* 访 Key * 访 Key
* 1. MinIO * 1. MinIO
* 2. * 2. https://ram.console.aliyun.com/manage/ak
* 3. https://console.cloud.tencent.com/cam/capi * 3. https://console.cloud.tencent.com/cam/capi
* 4. https://portal.qiniu.com/user/key * 4. https://portal.qiniu.com/user/key
* 5. * 5. https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
*/ */
@NotNull(message = "accessKey 不能为空") @NotNull(message = "accessKey 不能为空")
private String accessKey; private String accessKey;

View File

@ -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();
}
}

View File

@ -3,11 +3,13 @@ package cn.iocoder.yudao.framework.file.core.client.ftp;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.ftp.FtpMode; import cn.hutool.extra.ftp.FtpMode;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class FtpFileClientTest { public class FtpFileClientTest {
@Test @Test
@Disabled
public void test() { public void test() {
// 创建客户端 // 创建客户端
FtpFileClientConfig config = new FtpFileClientConfig(); FtpFileClientConfig config = new FtpFileClientConfig();

View File

@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.local;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class LocalFileClientTest { public class LocalFileClientTest {
@Test @Test
@Disabled
public void test() { public void test() {
// 创建客户端 // 创建客户端
LocalFileClientConfig config = new LocalFileClientConfig(); LocalFileClientConfig config = new LocalFileClientConfig();

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.file.core.client.s3;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -11,9 +10,26 @@ import javax.validation.Validation;
public class S3FileClientTest { 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 @Test
@Disabled // 阿里云 OSS如果要集成测试可以注释本行 @Disabled // 阿里云 OSS如果要集成测试可以注释本行
public void testAliyun() { public void testAliyun() throws Exception {
S3FileClientConfig config = new S3FileClientConfig(); S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的 // 配置成你自己的
config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY")); config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
@ -29,7 +45,7 @@ public class S3FileClientTest {
@Test @Test
@Disabled // 腾讯云 COS如果要集成测试可以注释本行 @Disabled // 腾讯云 COS如果要集成测试可以注释本行
public void testQCloud() { public void testQCloud() throws Exception {
S3FileClientConfig config = new S3FileClientConfig(); S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的 // 配置成你自己的
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY")); config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
@ -38,7 +54,6 @@ public class S3FileClientTest {
config.setDomain(null); // 如果有自定义域名则可以设置。http://tengxun-oss.iocoder.cn config.setDomain(null); // 如果有自定义域名则可以设置。http://tengxun-oss.iocoder.cn
// 默认上海的 endpoint // 默认上海的 endpoint
config.setEndpoint("cos.ap-shanghai.myqcloud.com"); config.setEndpoint("cos.ap-shanghai.myqcloud.com");
config.setRegion("ap-shanghai");
// 执行上传 // 执行上传
testExecuteUpload(config); testExecuteUpload(config);
@ -46,7 +61,7 @@ public class S3FileClientTest {
@Test @Test
@Disabled // 七牛云存储,如果要集成测试,可以注释本行 @Disabled // 七牛云存储,如果要集成测试,可以注释本行
public void testQiniu() { public void testQiniu() throws Exception {
S3FileClientConfig config = new S3FileClientConfig(); S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的 // 配置成你自己的
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY")); // config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
@ -62,11 +77,24 @@ public class S3FileClientTest {
testExecuteUpload(config); testExecuteUpload(config);
} }
private void testExecuteUpload(S3FileClientConfig config) { @Test
// 补全配置 @Disabled // 华为云存储,如果要集成测试,可以注释本行
if (config.getRegion() == null) { public void testHuaweiCloud() throws Exception {
config.setRegion(StrUtil.subBefore(config.getEndpoint(), '.', false)); 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); ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
// 创建 Client // 创建 Client
S3FileClient client = new S3FileClient(0L, config); S3FileClient client = new S3FileClient(0L, config);
@ -77,9 +105,9 @@ public class S3FileClientTest {
String fullPath = client.upload(content, path); String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath); System.out.println("访问地址:" + fullPath);
// 读取文件 // 读取文件
if (false) { if (true) {
byte[] bytes = client.getContent(path); byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes); System.out.println("文件内容:" + bytes.length);
} }
// 删除文件 // 删除文件
if (false) { if (false) {

View File

@ -2,11 +2,13 @@ package cn.iocoder.yudao.framework.file.core.client.sftp;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class SftpFileClientTest { public class SftpFileClientTest {
@Test @Test
@Disabled
public void test() { public void test() {
// 创建客户端 // 创建客户端
SftpFileClientConfig config = new SftpFileClientConfig(); SftpFileClientConfig config = new SftpFileClientConfig();