diff --git a/pom.xml b/pom.xml index ebfa899..88def23 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,8 @@ 2.7.0 - 1.12.600 + 2.23.0 + 0.29.6 1.77 @@ -340,11 +341,23 @@ ${ip2region.version} - + - com.amazonaws - aws-java-sdk-s3 - ${aws-java-sdk-s3.version} + software.amazon.awssdk + s3 + ${aws.sdk.version} + + + + software.amazon.awssdk.crt + aws-crt + ${aws.crt.version} + + + + software.amazon.awssdk + s3-transfer-manager + ${aws.sdk.version} diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java index 6f11904..e4d3f8b 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java @@ -26,6 +26,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { public static final String SEPARATOR = ","; + public static final String SLASH = "/"; + /** 空字符串 */ private static final String NULLSTR = ""; diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml index 1d5bd98..8a86517 100644 --- a/ruoyi-common/ruoyi-common-oss/pom.xml +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -31,9 +31,44 @@ ruoyi-common-redis + - com.amazonaws - aws-java-sdk-s3 + software.amazon.awssdk + s3 + + + + software.amazon.awssdk + netty-nio-client + + + + software.amazon.awssdk + aws-crt-client + + + + software.amazon.awssdk + apache-client + + + + software.amazon.awssdk + url-connection-client + + + + + + + software.amazon.awssdk.crt + aws-crt + + + + + software.amazon.awssdk + s3-transfer-manager diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java index d22f540..aee7cfc 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java @@ -2,73 +2,114 @@ package com.ruoyi.common.oss.core; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.IdUtil; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.HttpMethod; -import com.amazonaws.Protocol; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.*; +import com.ruoyi.common.core.constant.Constants; import com.ruoyi.common.core.utils.DateUtils; import com.ruoyi.common.core.utils.StringUtils; +import com.ruoyi.common.core.utils.file.FileUtils; import com.ruoyi.common.oss.constant.OssConstant; import com.ruoyi.common.oss.entity.UploadResult; import com.ruoyi.common.oss.enumd.AccessPolicyType; import com.ruoyi.common.oss.enumd.PolicyType; import com.ruoyi.common.oss.exception.OssException; import com.ruoyi.common.oss.properties.OssProperties; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.*; +import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URL; -import java.util.Date; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; /** * S3 存储协议 所有兼容S3协议的云厂商均支持 * 阿里云 腾讯云 七牛云 minio * - * @author Lion Li + * @author AprilWind */ public class OssClient { + /** + * 服务商 + */ private final String configKey; + /** + * 配置属性 + */ private final OssProperties properties; - private final AmazonS3 client; + /** + * Amazon S3 异步客户端 + */ + private final S3AsyncClient client; + /** + * 用于管理 S3 数据传输的高级工具 + */ + private final S3TransferManager transferManager; + + /** + * AWS S3 预签名 URL 的生成器 + */ + private final S3Presigner presigner; + + /** + * 构造方法 + * + * @param configKey 配置键 + * @param ossProperties Oss配置属性 + */ public OssClient(String configKey, OssProperties ossProperties) { this.configKey = configKey; this.properties = ossProperties; try { - AwsClientBuilder.EndpointConfiguration endpointConfig = - new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion()); + // 创建 AWS 认证信息 + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); - AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey()); - AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); - ClientConfiguration clientConfig = new ClientConfiguration(); - if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) { - clientConfig.setProtocol(Protocol.HTTPS); - } else { - clientConfig.setProtocol(Protocol.HTTP); - } - AmazonS3ClientBuilder build = AmazonS3Client.builder() - .withEndpointConfiguration(endpointConfig) - .withClientConfiguration(clientConfig) - .withCredentials(credentialsProvider) - .disableChunkedEncoding(); - if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) { + //创建AWS基于 CRT 的 S3 客户端 + this.client = S3AsyncClient.crtBuilder() + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getEndpoint())) + .region(of()) + .targetThroughputInGbps(20.0) + .minimumPartSizeInBytes(10 * 1025 * 1024L) + .checksumValidationEnabled(false) + .build(); + + //AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端 + this.transferManager = S3TransferManager.builder().s3Client(this.client).build(); + + // 检查是否连接到 MinIO,MinIO 使用 HTTPS 限制使用域名访问,需要启用路径样式访问 + S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false) // minio 使用https限制使用域名访问 需要此配置 站点填域名 - build.enablePathStyleAccess(); - } - this.client = build.build(); + .pathStyleAccessEnabled(!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)).build(); + // 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL + this.presigner = S3Presigner.builder() + .region(of()) + .credentialsProvider(credentialsProvider) + .endpointOverride(URI.create(getDomain())) + .serviceConfiguration(config) + .build(); + + // 创建存储桶 createBucket(); } catch (Exception e) { if (e instanceof OssException) { @@ -78,126 +119,158 @@ public class OssClient { } } + /** + * 同步创建存储桶 + * 如果存储桶不存在,会进行创建;如果存储桶存在,不执行任何操作 + * + * @throws OssException 当创建存储桶时发生异常时抛出 + */ public void createBucket() { + String bucketName = properties.getBucketName(); try { - String bucketName = properties.getBucketName(); - if (client.doesBucketExistV2(bucketName)) { - return; + // 尝试获取存储桶的信息 + client.headBucket( + x -> x.bucket(bucketName) + .build()) + .join(); + } catch (Exception ex) { + if (ex.getCause() instanceof NoSuchBucketException) { + try { + // 存储桶不存在,尝试创建存储桶 + client.createBucket( + x -> x.bucket(bucketName)) + .join(); + + // 设置存储桶的访问策略(Bucket Policy) + client.putBucketPolicy( + x -> x.bucket(bucketName) + .policy(getPolicy(bucketName, getAccessPolicy().getPolicyType()))) + .join(); + } catch (S3Exception e) { + // 存储桶创建或策略设置失败 + throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); + } + } else { + throw new OssException("判断Bucket是否存在失败,请核对配置信息:[" + ex.getMessage() + "]"); } - CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); - AccessPolicyType accessPolicy = getAccessPolicy(); - createBucketRequest.setCannedAcl(accessPolicy.getAcl()); - client.createBucket(createBucketRequest); - client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType())); - } catch (Exception e) { - throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); } } - public UploadResult upload(byte[] data, String path, String contentType) { - return upload(new ByteArrayInputStream(data), path, contentType); + /** + * 上传文件到 Amazon S3,并返回上传结果 + * + * @param filePath 本地文件路径 + * @param key 在 Amazon S3 中的对象键 + * @param md5Digest 本地文件的 MD5 哈希值(可选) + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult upload(Path filePath, String key, String md5Digest) { + try { + // 构建上传请求对象 + FileUpload fileUpload = transferManager.uploadFile( + x -> x.putObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(key) + .contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null) + .build()) + .addTransferListener(LoggingTransferListener.create()) + .source(filePath).build()); + + // 等待上传完成并获取上传结果 + CompletedFileUpload uploadResult = fileUpload.completionFuture().join(); + String eTag = uploadResult.response().eTag(); + + // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 + return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); + } catch (Exception e) { + // 捕获异常并抛出自定义异常 + throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); + } finally { + // 无论上传是否成功,最终都会删除临时文件 + FileUtils.del(filePath); + } } - public UploadResult upload(InputStream inputStream, String path, String contentType) { + /** + * 上传 InputStream 到 Amazon S3 + * + * @param inputStream 要上传的输入流 + * @param key 在 Amazon S3 中的对象键 + * @param length 输入流的长度 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult upload(InputStream inputStream, String key, Long length) { + // 如果输入流不是 ByteArrayInputStream,则将其读取为字节数组再创建 ByteArrayInputStream if (!(inputStream instanceof ByteArrayInputStream)) { inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); } try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(inputStream.available()); - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); + // 创建异步请求体(length如果为空会报错) + BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length); + + // 使用 transferManager 进行上传 + Upload upload = transferManager.upload( + x -> x.requestBody(body) + .putObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(key) + .build()) + .build()); + + // 将输入流写入请求体 + body.writeInputStream(inputStream); + + // 等待文件上传操作完成 + CompletedUpload uploadResult = upload.completionFuture().join(); + String eTag = uploadResult.response().eTag(); + + // 提取上传结果中的 ETag,并构建一个自定义的 UploadResult 对象 + return UploadResult.builder().url(getUrl() + StringUtils.SLASH + key).filename(key).eTag(eTag).build(); } catch (Exception e) { throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); } - return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build(); - } - - public UploadResult upload(File file, String path) { - try { - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); - } catch (Exception e) { - throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build(); - } - - public void delete(String path) { - path = path.replace(getUrl() + "/", ""); - try { - client.deleteObject(properties.getBucketName(), path); - } catch (Exception e) { - throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - } - - public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) { - return upload(data, getPath(properties.getPrefix(), suffix), contentType); - } - - public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) { - return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType); - } - - public UploadResult uploadSuffix(File file, String suffix) { - return upload(file, getPath(properties.getPrefix(), suffix)); } /** - * 获取文件元数据 + * 下载文件从 Amazon S3 到临时目录 * - * @param path 完整文件路径 + * @param path 文件在 Amazon S3 中的对象键 + * @return 下载后的文件在本地的临时路径 + * @throws OssException 如果下载失败,抛出自定义异常 */ - public ObjectMetadata getObjectMetadata(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectMetadata(); + public Path fileDownload(String path) { + // 构建临时文件 + Path tempFilePath = FileUtils.createTempFile().toPath(); + // 使用 S3TransferManager 下载文件 + FileDownload downloadFile = transferManager.downloadFile( + x -> x.getObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(removeBaseUrl(path)) + .build()) + .addTransferListener(LoggingTransferListener.create()) + .destination(tempFilePath) + .build()); + // 等待文件下载操作完成 + downloadFile.completionFuture().join(); + return tempFilePath; } - public InputStream getObjectContent(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectContent(); - } - - public String getUrl() { - String domain = properties.getDomain(); - String endpoint = properties.getEndpoint(); - String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; - // 云服务商直接返回 - if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { - if (StringUtils.isNotBlank(domain)) { - return header + domain; - } - return header + properties.getBucketName() + "." + endpoint; + /** + * 删除云存储服务中指定路径下文件 + * + * @param path 指定路径 + */ + public void delete(String path) { + try { + client.deleteObject( + x -> x.bucket(properties.getBucketName()) + .key(removeBaseUrl(path)) + .build()); + } catch (Exception e) { + throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); } - // minio 单独处理 - if (StringUtils.isNotBlank(domain)) { - return header + domain + "/" + properties.getBucketName(); - } - return header + endpoint + "/" + properties.getBucketName(); - } - - public String getPath(String prefix, String suffix) { - // 生成uuid - String uuid = IdUtil.fastSimpleUUID(); - // 文件路径 - String path = DateUtils.datePath() + "/" + uuid; - if (StringUtils.isNotBlank(prefix)) { - path = prefix + "/" + path; - } - return path + suffix; - } - - - public String getConfigKey() { - return configKey; } /** @@ -207,14 +280,189 @@ public class OssClient { * @param second 授权时间 */ public String getPrivateUrl(String objectKey, Integer second) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey) - .withMethod(HttpMethod.GET) - .withExpiration(new Date(System.currentTimeMillis() + 1000L * second)); - URL url = client.generatePresignedUrl(generatePresignedUrlRequest); + // 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL + URL url = presigner.presignGetObject( + x -> x.signatureDuration(Duration.ofSeconds(second)) + .getObjectRequest( + y -> y.bucket(properties.getBucketName()) + .key(objectKey) + .build()) + .build()) + .url(); return url.toString(); } + /** + * 上传 byte[] 数据到 Amazon S3,使用指定的后缀构造对象键。 + * + * @param data 要上传的 byte[] 数据 + * @param suffix 对象键的后缀 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(byte[] data, String suffix) { + return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length)); + } + + /** + * 上传 InputStream 到 Amazon S3,使用指定的后缀构造对象键。 + * + * @param inputStream 要上传的输入流 + * @param suffix 对象键的后缀 + * @param length 输入流的长度 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) { + return upload(inputStream, getPath(properties.getPrefix(), suffix), length); + } + + /** + * 上传文件到 Amazon S3,使用指定的后缀构造对象键 + * + * @param file 要上传的文件 + * @param suffix 对象键的后缀 + * @return UploadResult 包含上传后的文件信息 + * @throws OssException 如果上传失败,抛出自定义异常 + */ + public UploadResult uploadSuffix(File file, String suffix) { + return upload(file.toPath(), getPath(properties.getPrefix(), suffix), null); + } + + /** + * 获取文件输入流 + * + * @param path 完整文件路径 + * @return 输入流 + */ + public InputStream getObjectContent(String path) throws IOException { + // 下载文件到临时目录 + Path tempFilePath = fileDownload(path); + // 创建输入流 + InputStream inputStream = Files.newInputStream(tempFilePath); + // 删除临时文件 + FileUtils.del(tempFilePath); + // 返回对象内容的输入流 + return inputStream; + } + + /** + * 获取 S3 客户端的终端点 URL + * + * @return 终端点 URL + */ + public String getEndpoint() { + // 根据配置文件中的是否使用 HTTPS,设置协议头部 + String header = getIsHttps(); + // 拼接协议头部和终端点,得到完整的终端点 URL + return header + properties.getEndpoint(); + } + + /** + * 获取 S3 客户端的终端点 URL(自定义域名) + * + * @return 终端点 URL + */ + public String getDomain() { + // 从配置中获取域名、终端点、是否使用 HTTPS 等信息 + String domain = properties.getDomain(); + String endpoint = properties.getEndpoint(); + String header = getIsHttps(); + + // 如果是云服务商,直接返回域名或终端点 + if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { + return StringUtils.isNotEmpty(domain) ? header + domain : header + endpoint; + } + + // 如果是 MinIO,处理域名并返回 + if (StringUtils.isNotEmpty(domain)) { + return domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP) ? domain : header + domain; + } + + // 返回终端点 + return header + endpoint; + } + + /** + * 根据传入的 region 参数返回相应的 AWS 区域 + * 如果 region 参数非空,使用 Region.of 方法创建并返回对应的 AWS 区域对象 + * 如果 region 参数为空,返回一个默认的 AWS 区域(例如,us-east-1),作为广泛支持的区域 + * + * @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1) + */ + public Region of() { + //AWS 区域字符串 + String region = properties.getRegion(); + // 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域 + return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1; + } + + /** + * 获取云存储服务的URL + * + * @return 文件路径 + */ + public String getUrl() { + String domain = properties.getDomain(); + String endpoint = properties.getEndpoint(); + String header = getIsHttps(); + // 云服务商直接返回 + if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { + return header + (StringUtils.isNotEmpty(domain) ? domain : properties.getBucketName() + "." + endpoint); + } + // MinIO 单独处理 + if (StringUtils.isNotEmpty(domain)) { + // 如果 domain 以 "https://" 或 "http://" 开头 + return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ? + domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName(); + } + return header + endpoint + StringUtils.SLASH + properties.getBucketName(); + } + + /** + * 生成一个符合特定规则的、唯一的文件路径。通过使用日期、UUID、前缀和后缀等元素的组合,确保了文件路径的独一无二性 + * + * @param prefix 前缀 + * @param suffix 后缀 + * @return 文件路径 + */ + public String getPath(String prefix, String suffix) { + // 生成uuid + String uuid = IdUtil.fastSimpleUUID(); + // 生成日期路径 + String datePath = DateUtils.datePath(); + // 拼接路径 + String path = StringUtils.isNotEmpty(prefix) ? + prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid; + return path + suffix; + } + + /** + * 移除路径中的基础URL部分,得到相对路径 + * + * @param path 完整的路径,包括基础URL和相对路径 + * @return 去除基础URL后的相对路径 + */ + public String removeBaseUrl(String path) { + return path.replace(getUrl() + StringUtils.SLASH, ""); + } + + /** + * 服务商 + */ + public String getConfigKey() { + return configKey; + } + + /** + * 获取是否使用 HTTPS 的配置,并返回相应的协议头部。 + * + * @return 协议头部,根据是否使用 HTTPS 返回 "https://" 或 "http://" + */ + public String getIsHttps() { + return OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? Constants.HTTPS : Constants.HTTP; + } + /** * 检查配置是否相同 */ @@ -231,32 +479,77 @@ public class OssClient { return AccessPolicyType.getByType(properties.getAccessPolicy()); } + /** + * 生成 AWS S3 存储桶访问策略 + * + * @param bucketName 存储桶 + * @param policyType 桶策略类型 + * @return 符合 AWS S3 存储桶访问策略格式的字符串 + */ private static String getPolicy(String bucketName, PolicyType policyType) { - StringBuilder builder = new StringBuilder(); - builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n"); - builder.append(switch (policyType) { - case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n"; - case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n"; - default -> "\"s3:GetBucketLocation\"\n"; - }); - builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - if (policyType == PolicyType.READ) { - builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - } - builder.append("{\n\"Action\": "); - builder.append(switch (policyType) { - case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"; - default -> "\"s3:GetObject\",\n"; - }); - builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n"); - return builder.toString(); + String policy = switch (policyType) { + case WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [] + } + """; + case READ_WRITE -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:GetBucketLocation", + "s3:ListBucket", + "s3:ListBucketMultipartUploads" + ], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:AbortMultipartUpload", + "s3:DeleteObject", + "s3:GetObject", + "s3:ListMultipartUploadParts", + "s3:PutObject" + ], + "Resource": "arn:aws:s3:::bucketName/*" + } + ] + } + """; + case READ -> """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetBucketLocation"], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Deny", + "Principal": "*", + "Action": ["s3:ListBucket"], + "Resource": "arn:aws:s3:::bucketName" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::bucketName/*" + } + ] + } + """; + }; + return policy.replaceAll("bucketName", bucketName); } } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java index fd2e7fc..dc5b977 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java @@ -21,4 +21,9 @@ public class UploadResult { * 文件名 */ private String filename; + + /** + * 已上传对象的实体标记(用来校验文件) + */ + private String eTag; } diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java index 140f67a..2784630 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java @@ -1,8 +1,9 @@ package com.ruoyi.common.oss.enumd; -import com.amazonaws.services.s3.model.CannedAccessControlList; import lombok.AllArgsConstructor; import lombok.Getter; +import software.amazon.awssdk.services.s3.model.BucketCannedACL; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; /** * 桶访问策略配置 @@ -16,27 +17,32 @@ public enum AccessPolicyType { /** * private */ - PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE), + PRIVATE("0", BucketCannedACL.PRIVATE, ObjectCannedACL.PRIVATE, PolicyType.WRITE), /** * public */ - PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ), + PUBLIC("1", BucketCannedACL.PUBLIC_READ_WRITE, ObjectCannedACL.PUBLIC_READ_WRITE, PolicyType.READ_WRITE), /** * custom */ - CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ); + CUSTOM("2", BucketCannedACL.PUBLIC_READ, ObjectCannedACL.PUBLIC_READ, PolicyType.READ); /** - * 桶 权限类型 + * 桶 权限类型(数据库值) */ private final String type; + /** + * 桶 权限类型 + */ + private final BucketCannedACL bucketCannedACL; + /** * 文件对象 权限类型 */ - private final CannedAccessControlList acl; + private final ObjectCannedACL objectCannedACL; /** * 桶策略类型 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java index 9151225..578f986 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java @@ -145,7 +145,7 @@ public class SysOssServiceImpl extends BaseServiceImpl imp OssClient storage = OssFactory.instance(); UploadResult uploadResult; try { - uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); + uploadResult = storage.uploadSuffix(file.getBytes(), suffix); } catch (IOException e) { throw new ServiceException(e.getMessage()); }