Răsfoiți Sursa

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

YunaiV 3 ani în urmă
părinte
comite
34a7399a65

+ 4 - 4
yudao-dependencies/pom.xml

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

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-file/pom.xml

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

+ 5 - 3
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 {
      * @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;
 
 }

+ 51 - 42
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java

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

+ 8 - 11
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;
 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;

+ 0 - 36
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3ModifyPathInterceptor.java

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

+ 2 - 0
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;
 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 - 0
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;
 
 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();

+ 40 - 12
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;
 
 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 - 0
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;
 
 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();