diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index ada5d4ddfc..895d19c130 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 09758bec07..72eafba586 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 60e0fc51b2..178c27d4c2 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 4fcc674d5c..8de178bfcc 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 e35e38cef6..e337bd89db 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 1b7550dd7d..0000000000 --- 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 ee0d740780..00a3a268ed 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 60f781b01d..2062d63d79 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 47c5e76e42..636f146fa2 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 cc8e59edec..412df1ea8f 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();