diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index ada5d4ddf..895d19c13 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -56,7 +56,7 @@
3.8.0
0.1.55
- 2.17.147
+ 8.2.2
4.5.25
2.1.0
1.2.7
@@ -514,9 +514,9 @@
${revision}
- software.amazon.awssdk
- s3
- ${s3.version}
+ io.minio
+ minio
+ ${minio.version}
diff --git a/yudao-framework/yudao-spring-boot-starter-file/pom.xml b/yudao-framework/yudao-spring-boot-starter-file/pom.xml
index 09758bec0..72eafba58 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/pom.xml
+++ b/yudao-framework/yudao-spring-boot-starter-file/pom.xml
@@ -63,8 +63,8 @@
- software.amazon.awssdk
- s3
+ io.minio
+ minio
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java
index 60e0fc51b..178c27d4c 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java
+++ b/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;
}
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java
index 4fcc674d5..8de178bfc 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java
+++ b/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 {
- private S3Client client;
+ private MinioClient client;
public S3FileClient(Long id, S3FileClientConfig config) {
super(id, config);
@@ -34,34 +29,27 @@ public class S3FileClient extends AbstractFileClient {
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 {
*
* @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);
}
}
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java
index e35e38cef..e337bd89d 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java
+++ b/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;
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3ModifyPathInterceptor.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3ModifyPathInterceptor.java
deleted file mode 100644
index 1b7550dd7..000000000
--- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3ModifyPathInterceptor.java
+++ /dev/null
@@ -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();
- }
-
-}
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java
index ee0d74078..00a3a268e 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/ftp/FtpFileClientTest.java
+++ b/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();
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java
index 60f781b01..2062d63d7 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/local/LocalFileClientTest.java
+++ b/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();
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java
index 47c5e76e4..636f146fa 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientTest.java
+++ b/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) {
diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java b/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java
index cc8e59ede..412df1ea8 100644
--- a/yudao-framework/yudao-spring-boot-starter-file/src/test/java/cn/iocoder/yudao/framework/file/core/client/sftp/SftpFileClientTest.java
+++ b/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();