refactor: 用 RustFS S3 替代 MinIO 并优化 OSS endpoint 协议处理
This commit is contained in:
9
pom.xml
9
pom.xml
@@ -38,7 +38,6 @@
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
|
||||
<!-- 对象存储 -->
|
||||
<minio.version>8.5.10</minio.version>
|
||||
<okhttp3.version>4.12.0</okhttp3.version>
|
||||
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
@@ -214,11 +213,11 @@
|
||||
<version>${fastexcel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO 对象存储 -->
|
||||
<!-- S3 对象存储 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>2.54.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云 OSS 对象存储 -->
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* 文件存储顶层配置:存储类型与上传限制。
|
||||
* <p>
|
||||
* minio / aliyun / local 子树由各自 {@code FileService} 实现类绑定,
|
||||
* rustfs / aliyun / local 子树由各自 {@code FileService} 实现类绑定,
|
||||
* 此处仅承载 {@code type} 与 {@code upload}。
|
||||
*/
|
||||
@Data
|
||||
@@ -20,7 +20,7 @@ import java.util.stream.Collectors;
|
||||
@ConfigurationProperties(prefix = "file-storage")
|
||||
public class FileStorageProperties {
|
||||
|
||||
/** 存储类型:minio | aliyun | local */
|
||||
/** 存储类型:rustfs | aliyun | local */
|
||||
private String type;
|
||||
|
||||
private Upload upload = new Upload();
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
@@ -28,7 +29,8 @@ import java.time.LocalDateTime;
|
||||
* 阿里云 OSS 文件存储服务。
|
||||
* <p>
|
||||
* 当 {@code file-storage.type=aliyun} 时生效。
|
||||
* 上传文件按日期分目录存储,URL 格式:{@code https://bucket.endpoint/yyyyMMdd/uuid.suffix}。
|
||||
* 上传文件按日期分目录存储,URL 格式:{@code 协议://bucket.endpoint/yyyyMMdd/uuid.suffix},
|
||||
* 协议取自 endpoint 配置(未指定时默认 https)。
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@@ -38,7 +40,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
public class AliyunFileServiceImpl implements FileService {
|
||||
|
||||
/** OSS 地域节点,如 {@code oss-cn-hangzhou.aliyuncs.com} */
|
||||
/** OSS 地域节点,如 {@code oss-cn-hangzhou.aliyuncs.com},兼容带 {@code http(s)://} 前缀写法 */
|
||||
private String endpoint;
|
||||
/** AccessKey ID */
|
||||
private String accessKeyId;
|
||||
@@ -51,11 +53,11 @@ public class AliyunFileServiceImpl implements FileService {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
aliyunOssClient = new OSSClientBuilder().build(endpointWithProtocol(), accessKeyId, accessKeySecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到阿里云 OSS,按日期分目录,返回 {@code https://bucket.endpoint/path} 格式的 URL。
|
||||
* 上传文件到阿里云 OSS,按日期分目录,返回 {@code 协议://bucket.endpoint/path} 格式的 URL。
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
@@ -73,7 +75,7 @@ public class AliyunFileServiceImpl implements FileService {
|
||||
log.error("文件上传失败", e);
|
||||
throw new BusinessException("文件上传失败", e);
|
||||
}
|
||||
String fileUrl = "https://" + bucket + "." + endpoint + "/" + fileName;
|
||||
String fileUrl = getFileHost() + "/" + fileName;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
fileInfo.setUrl(fileUrl);
|
||||
@@ -89,9 +91,33 @@ public class AliyunFileServiceImpl implements FileService {
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
Assert.notBlank(filePath, "删除文件路径不能为空");
|
||||
String fileHost = "https://" + bucket + "." + endpoint;
|
||||
String fileName = filePath.substring(fileHost.length() + 1);
|
||||
String fileName = filePath.substring(getFileHost().length() + 1);
|
||||
aliyunOssClient.deleteObject(bucket, fileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全 endpoint 协议前缀:已带 {@code http://} 或 {@code https://} 的仅去除尾部 {@code /},
|
||||
* 纯域名写法默认补 {@code https://},避免 OSS 客户端对无协议 endpoint 默认走 http 明文传输。
|
||||
*/
|
||||
private String endpointWithProtocol() {
|
||||
String trimmed = StrUtil.trim(endpoint);
|
||||
if (!StrUtil.startWithIgnoreCase(trimmed, "http://") && !StrUtil.startWithIgnoreCase(trimmed, "https://")) {
|
||||
trimmed = "https://" + trimmed;
|
||||
}
|
||||
return StrUtil.removeSuffix(trimmed, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问主机地址(含协议),如 {@code https://bucket.oss-cn-hangzhou.aliyuncs.com}。
|
||||
* <p>
|
||||
* 协议以 endpoint 配置为准,未指定时默认 https;上传拼接 URL 与删除解析 URL 共用,保证两处口径一致。
|
||||
*/
|
||||
private String getFileHost() {
|
||||
String endpointWithProtocol = endpointWithProtocol();
|
||||
int schemeEnd = endpointWithProtocol.indexOf("://");
|
||||
String protocol = endpointWithProtocol.substring(0, schemeEnd);
|
||||
String host = endpointWithProtocol.substring(schemeEnd + "://".length());
|
||||
return protocol + "://" + bucket + "." + host;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package com.youlai.boot.support.file.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.support.file.model.FileInfo;
|
||||
import com.youlai.boot.support.file.service.FileService;
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MinIO 文件存储服务。
|
||||
* <p>
|
||||
* 当 {@code file-storage.type=minio} 时生效。
|
||||
* 上传文件按日期分目录存储,路径格式:{@code yyyyMMdd/uuid.suffix}。
|
||||
* <ul>
|
||||
* <li>配置了 {@code file-storage.minio.domain} 时直接用域名拼接 URL</li>
|
||||
* <li>未配置则用 MinIO 预签名 URL(去掉 query 参数后的纯路径)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "file-storage.type", havingValue = "minio")
|
||||
@ConfigurationProperties(prefix = "file-storage.minio")
|
||||
@RequiredArgsConstructor
|
||||
@Data
|
||||
@Slf4j
|
||||
public class MinioFileServiceImpl implements FileService {
|
||||
|
||||
/** MinIO 服务端点,如 {@code http://localhost:9000} */
|
||||
private String endpoint;
|
||||
/** 访问密钥 */
|
||||
private String accessKey;
|
||||
/** 秘密密钥 */
|
||||
private String secretKey;
|
||||
/** 存储桶名称 */
|
||||
private String bucket;
|
||||
/** 自定义域名(可选),配置后不再使用预签名 URL */
|
||||
private String domain;
|
||||
|
||||
private MinioClient minioClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(accessKey, secretKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 MinIO,按日期分目录,自动创建存储桶,返回文件访问 URL。
|
||||
*/
|
||||
@Override
|
||||
public FileInfo uploadFile(MultipartFile file) {
|
||||
createBucketIfAbsent(bucket);
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd");
|
||||
String fileName = IdUtil.simpleUUID() + "." + suffix;
|
||||
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
|
||||
.bucket(bucket)
|
||||
.object(dateFolder + "/" + fileName)
|
||||
.contentType(file.getContentType())
|
||||
.stream(inputStream, inputStream.available(), -1)
|
||||
.build();
|
||||
minioClient.putObject(putObjectArgs);
|
||||
|
||||
String fileUrl;
|
||||
if (StrUtil.isBlank(domain)) {
|
||||
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
|
||||
.bucket(bucket)
|
||||
.object(dateFolder + "/" + fileName)
|
||||
.method(Method.GET)
|
||||
.build();
|
||||
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
|
||||
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
|
||||
} else {
|
||||
fileUrl = domain + "/" + bucket + "/" + dateFolder + "/" + fileName;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
fileInfo.setUrl(fileUrl);
|
||||
return fileInfo;
|
||||
} catch (Exception e) {
|
||||
log.error("上传文件失败", e);
|
||||
throw new BusinessException(ResultCode.UPLOAD_FILE_EXCEPTION, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件。
|
||||
* <p>
|
||||
* 从完整 URL 中提取文件名后调用 MinIO 删除。
|
||||
*
|
||||
* @param filePath 文件完整 URL
|
||||
* @return 删除成功返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
Assert.notBlank(filePath, "删除文件路径不能为空");
|
||||
try {
|
||||
String fileName;
|
||||
if (StrUtil.isNotBlank(domain)) {
|
||||
fileName = filePath.substring(domain.length() + 1 + bucket.length() + 1);
|
||||
} else {
|
||||
fileName = filePath.substring(endpoint.length() + 1 + bucket.length() + 1);
|
||||
}
|
||||
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||
.bucket(bucket).object(fileName).build());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("删除文件失败", e);
|
||||
throw new BusinessException(ResultCode.DELETE_FILE_EXCEPTION, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成公开读的存储桶策略 JSON */
|
||||
private static String publicBucketPolicy(String bucket) {
|
||||
return "{\"Version\":\"2012-10-17\","
|
||||
+ "\"Statement\":[{\"Effect\":\"Allow\","
|
||||
+ "\"Principal\":{\"AWS\":[\"*\"]},"
|
||||
+ "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],"
|
||||
+ "\"Resource\":[\"arn:aws:s3:::" + bucket + "\"]},"
|
||||
+ "{\"Effect\":\"Allow\",\"" + "\"Principal\":{\"AWS\":[\"*\"]},"
|
||||
+ "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],"
|
||||
+ "\"Resource\":[\"arn:aws:s3:::" + bucket + "/*\"]}]}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储桶不存在时自动创建,并设置为公开读策略。
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void createBucketIfAbsent(String bucket) {
|
||||
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
|
||||
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
|
||||
.bucket(bucket).config(publicBucketPolicy(bucket)).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.youlai.boot.support.file.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.support.file.model.FileInfo;
|
||||
import com.youlai.boot.support.file.service.FileService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
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.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutBucketPolicyRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.S3Exception;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* RustFS 文件存储服务(S3 协议)。
|
||||
* <p>
|
||||
* 当 {@code file-storage.type=rustfs} 时生效。通过 AWS SDK 的 S3Client
|
||||
* 连接 RustFS(S3 兼容对象存储),IP 端点必须走 path-style 寻址。
|
||||
* 上传文件按日期分目录存储,路径格式:{@code yyyyMMdd/uuid.suffix}。
|
||||
* <ul>
|
||||
* <li>配置了 {@code file-storage.rustfs.domain} 时直接用域名拼接 URL</li>
|
||||
* <li>未配置则用预签名 GET URL(去掉 query 参数后的纯路径)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author youlai.tech
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "file-storage.type", havingValue = "rustfs")
|
||||
@ConfigurationProperties(prefix = "file-storage.rustfs")
|
||||
@RequiredArgsConstructor
|
||||
@Data
|
||||
@Slf4j
|
||||
public class RustFSFileServiceImpl implements FileService {
|
||||
|
||||
/** S3 对象存储服务端点,如 {@code http://localhost:9000} */
|
||||
private String endpoint;
|
||||
/** 访问密钥 */
|
||||
private String accessKey;
|
||||
/** 秘密密钥 */
|
||||
private String secretKey;
|
||||
/** 存储桶名称 */
|
||||
private String bucket;
|
||||
/** 自定义域名(可选),配置后不再使用预签名 URL */
|
||||
private String domain;
|
||||
|
||||
private S3Client s3Client;
|
||||
private S3Presigner presigner;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// IP 端点必须走 path-style:桶名作为路径而不是子域名(public.IP 无法被 DNS 解析)
|
||||
S3Configuration s3Config = S3Configuration.builder()
|
||||
.pathStyleAccessEnabled(true)
|
||||
.build();
|
||||
s3Client = S3Client.builder()
|
||||
.endpointOverride(URI.create(endpoint))
|
||||
// 非 AWS 服务必填,值仅区分区域,任意即可
|
||||
.region(Region.US_EAST_1)
|
||||
.serviceConfiguration(s3Config)
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(accessKey, secretKey)))
|
||||
.build();
|
||||
presigner = S3Presigner.builder()
|
||||
.endpointOverride(URI.create(endpoint))
|
||||
.region(Region.US_EAST_1)
|
||||
.serviceConfiguration(s3Config)
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(accessKey, secretKey)))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 RustFS,按日期分目录,自动创建存储桶,返回文件访问 URL。
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public FileInfo uploadFile(MultipartFile file) {
|
||||
createBucketIfAbsent(bucket);
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd");
|
||||
String fileName = IdUtil.simpleUUID() + "." + suffix;
|
||||
|
||||
try {
|
||||
byte[] bytes = file.getBytes();
|
||||
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(dateFolder + "/" + fileName)
|
||||
.contentType(file.getContentType())
|
||||
.build();
|
||||
// 用固定长度的字节数组上传,避免流式分块签名在某些 S3 兼容服务上报校验和不匹配
|
||||
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(bytes));
|
||||
|
||||
String fileUrl;
|
||||
if (StrUtil.isBlank(domain)) {
|
||||
fileUrl = presignedUrl(dateFolder, fileName);
|
||||
} else {
|
||||
fileUrl = domain + "/" + bucket + "/" + dateFolder + "/" + fileName;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
fileInfo.setUrl(fileUrl);
|
||||
return fileInfo;
|
||||
} catch (Exception e) {
|
||||
log.error("上传文件失败", e);
|
||||
throw new BusinessException(ResultCode.UPLOAD_FILE_EXCEPTION, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成预签名 GET URL 并去掉 query 参数,路径直达对象。
|
||||
* 依赖桶已开匿名只读(详见 createBucketIfAbsent 的策略),签名参数在 URL 中无实际作用。
|
||||
*/
|
||||
private String presignedUrl(String dateFolder, String fileName) {
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(dateFolder + "/" + fileName)
|
||||
.build();
|
||||
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
|
||||
.signatureDuration(Duration.ofMinutes(10))
|
||||
.getObjectRequest(getObjectRequest)
|
||||
.build();
|
||||
String url = presigner.presignGetObject(presignRequest).url().toString();
|
||||
return url.substring(0, url.indexOf("?"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件。
|
||||
* <p>
|
||||
* 从完整 URL 中提取对象名后调用 S3 删除。
|
||||
*
|
||||
* @param filePath 文件完整 URL
|
||||
* @return 删除成功返回 true
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
Assert.notBlank(filePath, "删除文件路径不能为空");
|
||||
try {
|
||||
String fileName;
|
||||
if (StrUtil.isNotBlank(domain)) {
|
||||
fileName = filePath.substring(domain.length() + 1 + bucket.length() + 1);
|
||||
} else {
|
||||
fileName = filePath.substring(endpoint.length() + 1 + bucket.length() + 1);
|
||||
}
|
||||
s3Client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket).key(fileName).build());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("删除文件失败", e);
|
||||
throw new BusinessException(ResultCode.DELETE_FILE_EXCEPTION, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 公开只读策略:匿名可列桶与读对象,禁止任何匿名写入 */
|
||||
private static String publicBucketPolicy(String bucket) {
|
||||
return "{\"Version\":\"2012-10-17\","
|
||||
+ "\"Statement\":[{\"Effect\":\"Allow\","
|
||||
+ "\"Principal\":{\"AWS\":[\"*\"]},"
|
||||
+ "\"Action\":[\"s3:GetObject\",\"s3:ListBucket\"],"
|
||||
+ "\"Resource\":[\"arn:aws:s3:::" + bucket + "\",\"arn:aws:s3:::" + bucket + "/*\"]}]}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储桶不存在时自动创建,并设置为公开只读策略。
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void createBucketIfAbsent(String bucket) {
|
||||
try {
|
||||
s3Client.headBucket(HeadBucketRequest.builder().bucket(bucket).build());
|
||||
} catch (S3Exception e) {
|
||||
// 桶不存在(404)时才创建;其他错误(如权限不足)放行由调用方抛出
|
||||
if (e.statusCode() == 404) {
|
||||
s3Client.createBucket(CreateBucketRequest.builder().bucket(bucket).build());
|
||||
s3Client.putBucketPolicy(PutBucketPolicyRequest.builder()
|
||||
.bucket(bucket).policy(publicBucketPolicy(bucket)).build());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
* 角色分页列表
|
||||
*
|
||||
* @param queryParams 角色查询参数
|
||||
* @return {@link Page< RolePageVO >} – 角色分页列表
|
||||
* @return {@link Page<RolePageVO>} – 角色分页列表
|
||||
*/
|
||||
@Override
|
||||
public Page<RolePageVO> getRolePage(RoleQuery queryParams) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"name": "file-storage.type",
|
||||
"type": "java.lang.String",
|
||||
"description": "文件存储类型:minio | aliyun | local"
|
||||
"description": "文件存储类型:rustfs | aliyun | local"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.upload.max-file-size",
|
||||
@@ -16,29 +16,29 @@
|
||||
"description": "允许的文件扩展名白名单(置空表示不限制)"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.minio.endpoint",
|
||||
"name": "file-storage.rustfs.endpoint",
|
||||
"type": "java.lang.String",
|
||||
"description": "MinIO 服务 Endpoint"
|
||||
"description": "RustFS(S3) 服务 Endpoint"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.minio.access-key",
|
||||
"name": "file-storage.rustfs.access-key",
|
||||
"type": "java.lang.String",
|
||||
"description": "MinIO 访问凭据"
|
||||
"description": "RustFS 访问凭据"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.minio.secret-key",
|
||||
"name": "file-storage.rustfs.secret-key",
|
||||
"type": "java.lang.String",
|
||||
"description": "MinIO 凭据密钥"
|
||||
"description": "RustFS 凭据密钥"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.minio.bucket",
|
||||
"name": "file-storage.rustfs.bucket",
|
||||
"type": "java.lang.String",
|
||||
"description": "MinIO 存储桶名称"
|
||||
"description": "RustFS 存储桶名称"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.minio.domain",
|
||||
"name": "file-storage.rustfs.domain",
|
||||
"type": "java.lang.String",
|
||||
"description": "MinIO 自定义域名(文件 URL 域名)"
|
||||
"description": "RustFS 自定义域名(文件 URL 域名)"
|
||||
},
|
||||
{
|
||||
"name": "file-storage.aliyun.endpoint",
|
||||
|
||||
@@ -108,8 +108,8 @@ security:
|
||||
|
||||
# 文件存储
|
||||
file-storage:
|
||||
# 存储类型:minio | aliyun | local
|
||||
type: minio
|
||||
# 存储类型:rustfs | aliyun | local
|
||||
type: rustfs
|
||||
# 上传限制
|
||||
upload:
|
||||
# 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源)
|
||||
@@ -120,8 +120,8 @@ file-storage:
|
||||
- jpeg
|
||||
- png
|
||||
- gif
|
||||
# MinIO 对象存储
|
||||
minio:
|
||||
# RustFS(S3) 对象存储
|
||||
rustfs:
|
||||
endpoint: http://111.229.83.153:9000
|
||||
access-key: bybaddp7zyARpgNbEGKf
|
||||
secret-key: p9rBdQZPBIJcMH23iyFkZkXmmawbmwPlk3JLlaaj
|
||||
|
||||
@@ -107,8 +107,8 @@ security:
|
||||
|
||||
# 文件存储
|
||||
file-storage:
|
||||
# 存储类型:minio | aliyun | local
|
||||
type: minio
|
||||
# 存储类型:rustfs | aliyun | local
|
||||
type: rustfs
|
||||
# 上传限制
|
||||
upload:
|
||||
# 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源)
|
||||
@@ -119,11 +119,11 @@ file-storage:
|
||||
- jpeg
|
||||
- png
|
||||
- gif
|
||||
# MinIO 对象存储
|
||||
minio:
|
||||
# RustFS(S3) 对象存储
|
||||
rustfs:
|
||||
endpoint: http://localhost:9000
|
||||
access-key: minioadmin
|
||||
secret-key: minioadmin
|
||||
access-key: rustfs-admin
|
||||
secret-key: rustfs-admin
|
||||
bucket: public
|
||||
# 自定义域名:配置后文件 URL 走域名,留空则用 endpoint(IP)格式
|
||||
domain:
|
||||
|
||||
Reference in New Issue
Block a user