Commit 34a7399a65b5cb011a5df793c16a2cc911378bac

Authored by YunaiV
1 parent 62f7d349

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

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