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

This commit is contained in:
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>
<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 -->

View File

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

View File

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

View File

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

View File

@ -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;

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.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();

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.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();

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.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) {

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.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();