feat: 新增heritage-plugins存储插件(第4部分)
- 新增heritage-oss:对象存储插件 - 支持本地存储、阿里云OSS、MinIO三种存储方式 - 提供统一的存储接口,可灵活切换存储类型 - 支持文件上传、下载、删除等操作
This commit is contained in:
parent
db6f4b1922
commit
9c1937b4fb
56
heritage-plugins/heritage-oss/pom.xml
Normal file
56
heritage-plugins/heritage-oss/pom.xml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.leocoder.heritage</groupId>
|
||||||
|
<artifactId>heritage-plugins</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>heritage-oss</name>
|
||||||
|
<artifactId>heritage-oss</artifactId>
|
||||||
|
<description>阿里云OSS对象存储模块</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- 全局公共模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.leocoder.heritage</groupId>
|
||||||
|
<artifactId>heritage-common</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 阿里云OSS SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun.oss</groupId>
|
||||||
|
<artifactId>aliyun-sdk-oss</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MinIO SDK -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Configuration Processor -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Starter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.leocoder.heritage.oss.annotation;
|
||||||
|
|
||||||
|
import org.leocoder.heritage.oss.config.OssAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用Coder OSS插件注解
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Import(OssAutoConfiguration.class)
|
||||||
|
public @interface EnableCoderOss {
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package org.leocoder.heritage.oss.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO配置类
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "coder.minio")
|
||||||
|
public class MinioConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO服务端点
|
||||||
|
*/
|
||||||
|
private String endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Key
|
||||||
|
*/
|
||||||
|
private String accessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret Key
|
||||||
|
*/
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储桶名称
|
||||||
|
*/
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义域名
|
||||||
|
*/
|
||||||
|
private String domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径前缀
|
||||||
|
*/
|
||||||
|
private String pathPrefix = "coder-files";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long connectTimeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long writeTimeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long readTimeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用MinIO存储
|
||||||
|
*/
|
||||||
|
private Boolean enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域设置(可选)
|
||||||
|
*/
|
||||||
|
private String region;
|
||||||
|
}
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
package org.leocoder.heritage.oss.config;
|
||||||
|
|
||||||
|
import com.aliyun.oss.ClientBuilderConfiguration;
|
||||||
|
import com.aliyun.oss.OSS;
|
||||||
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.oss.service.LocalStorageService;
|
||||||
|
import org.leocoder.heritage.oss.service.MinioStorageService;
|
||||||
|
import org.leocoder.heritage.oss.service.OssStorageService;
|
||||||
|
import org.leocoder.heritage.oss.service.StorageServiceFactory;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSS自动配置类
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties({OssConfig.class, MinioConfig.class})
|
||||||
|
@Slf4j
|
||||||
|
public class OssAutoConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [OSS客户端配置]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public OSS ossClient(OssConfig ossConfig) {
|
||||||
|
log.info("初始化OSS客户端: endpoint={}, bucketName={}",
|
||||||
|
ossConfig.getEndpoint(), ossConfig.getBucketName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建ClientBuilderConfiguration
|
||||||
|
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
|
||||||
|
|
||||||
|
// 设置连接超时时间
|
||||||
|
clientBuilderConfiguration.setConnectionTimeout(ossConfig.getConnectTimeout().intValue());
|
||||||
|
|
||||||
|
// 设置读取超时时间
|
||||||
|
clientBuilderConfiguration.setSocketTimeout(ossConfig.getReadTimeout().intValue());
|
||||||
|
|
||||||
|
// 设置连接池大小
|
||||||
|
clientBuilderConfiguration.setMaxConnections(200);
|
||||||
|
|
||||||
|
// 设置请求超时时间
|
||||||
|
clientBuilderConfiguration.setRequestTimeout(30000);
|
||||||
|
|
||||||
|
// 设置失败重试次数
|
||||||
|
clientBuilderConfiguration.setMaxErrorRetry(3);
|
||||||
|
|
||||||
|
// 创建OSS客户端
|
||||||
|
OSS ossClient = new OSSClientBuilder().build(
|
||||||
|
ossConfig.getEndpoint(),
|
||||||
|
ossConfig.getAccessKeyId(),
|
||||||
|
ossConfig.getAccessKeySecret(),
|
||||||
|
clientBuilderConfiguration
|
||||||
|
);
|
||||||
|
|
||||||
|
// 验证Bucket是否存在
|
||||||
|
if (!ossClient.doesBucketExist(ossConfig.getBucketName())) {
|
||||||
|
log.warn("OSS Bucket不存在: {}", ossConfig.getBucketName());
|
||||||
|
} else {
|
||||||
|
log.info("OSS客户端初始化成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ossClient;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OSS客户端初始化失败", e);
|
||||||
|
throw new RuntimeException("OSS客户端初始化失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [OSS存储服务]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public OssStorageService ossStorageService(OSS ossClient, OssConfig ossConfig) {
|
||||||
|
log.info("初始化OSS存储服务");
|
||||||
|
return new OssStorageService(ossClient, ossConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [本地存储服务(始终可用)]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public LocalStorageService localStorageService(Environment environment) {
|
||||||
|
log.info("初始化本地存储服务");
|
||||||
|
return new LocalStorageService(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [MinIO客户端配置]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public MinioClient minioClient(MinioConfig minioConfig) {
|
||||||
|
log.info("初始化MinIO客户端: endpoint={}, bucketName={}",
|
||||||
|
minioConfig.getEndpoint(), minioConfig.getBucketName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
MinioClient.Builder builder = MinioClient.builder()
|
||||||
|
.endpoint(minioConfig.getEndpoint())
|
||||||
|
.credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey());
|
||||||
|
|
||||||
|
// 如果配置了区域,则设置区域
|
||||||
|
if (minioConfig.getRegion() != null && !minioConfig.getRegion().trim().isEmpty()) {
|
||||||
|
builder.region(minioConfig.getRegion());
|
||||||
|
}
|
||||||
|
|
||||||
|
MinioClient minioClient = builder.build();
|
||||||
|
|
||||||
|
// 设置超时时间
|
||||||
|
minioClient.setTimeout(
|
||||||
|
minioConfig.getConnectTimeout(),
|
||||||
|
minioConfig.getWriteTimeout(),
|
||||||
|
minioConfig.getReadTimeout()
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("MinIO客户端初始化成功");
|
||||||
|
return minioClient;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MinIO客户端初始化失败", e);
|
||||||
|
throw new RuntimeException("MinIO客户端初始化失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [MinIO存储服务]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public MinioStorageService minioStorageService(MinioClient minioClient, MinioConfig minioConfig) {
|
||||||
|
log.info("初始化MinIO存储服务");
|
||||||
|
return new MinioStorageService(minioClient, minioConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [存储服务工厂(始终可用)]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public StorageServiceFactory storageServiceFactory(ApplicationContext applicationContext) {
|
||||||
|
log.info("初始化存储服务工厂");
|
||||||
|
return new StorageServiceFactory(applicationContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package org.leocoder.heritage.oss.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSS配置类
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "coder.oss")
|
||||||
|
public class OssConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSS服务端点
|
||||||
|
*/
|
||||||
|
private String endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Key ID
|
||||||
|
*/
|
||||||
|
private String accessKeyId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Key Secret
|
||||||
|
*/
|
||||||
|
private String accessKeySecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储桶名称
|
||||||
|
*/
|
||||||
|
private String bucketName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义域名
|
||||||
|
*/
|
||||||
|
private String domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径前缀
|
||||||
|
*/
|
||||||
|
private String pathPrefix = "coder-files";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用HTTPS
|
||||||
|
*/
|
||||||
|
private Boolean https = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long connectTimeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取超时时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long readTimeout = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用OSS存储
|
||||||
|
*/
|
||||||
|
private Boolean enabled = false;
|
||||||
|
}
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
package org.leocoder.heritage.oss.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.common.constants.CoderConstants;
|
||||||
|
import org.leocoder.heritage.common.exception.BusinessException;
|
||||||
|
import org.leocoder.heritage.common.utils.file.FileUtil;
|
||||||
|
import org.leocoder.heritage.common.utils.file.UploadUtil;
|
||||||
|
import org.leocoder.heritage.common.utils.ip.IpUtil;
|
||||||
|
import org.leocoder.heritage.common.utils.ip.ServletUtil;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地存储服务实现
|
||||||
|
* 基于现有的本地文件存储逻辑进行封装
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class LocalStorageService implements StorageService {
|
||||||
|
|
||||||
|
private final Environment env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取本地存储基本路径]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
private String getBasePath() {
|
||||||
|
return env.getProperty("coder.filePath", "/tmp/coder-files");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [上传文件到本地存储]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||||||
|
try {
|
||||||
|
log.info("本地存储上传文件: fileName={}, folderPath={}", fileName, folderPath);
|
||||||
|
|
||||||
|
// 构建完整的本地存储路径
|
||||||
|
String fullPath = getBasePath() + "/" + folderPath;
|
||||||
|
|
||||||
|
// 使用现有的上传工具类
|
||||||
|
Map<String, Object> fileMap = UploadUtil.coderSingleFile(file, fullPath, 2);
|
||||||
|
|
||||||
|
// UploadUtil已经返回了正确的相对路径,不需要再次构建URL
|
||||||
|
|
||||||
|
log.info("本地存储上传成功: {}", fileMap.get("filePath"));
|
||||||
|
return fileMap;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("本地存储上传失败", e);
|
||||||
|
throw new BusinessException(500, "文件上传失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [从本地存储删除文件]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean deleteFile(String filePath) {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.hasText(filePath)) {
|
||||||
|
log.warn("文件路径为空,跳过删除");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("本地存储删除文件: {}", filePath);
|
||||||
|
boolean result = FileUtil.deleteFile(filePath);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
log.info("本地存储删除成功: {}", filePath);
|
||||||
|
} else {
|
||||||
|
log.warn("本地存储删除失败: {}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("本地存储删除文件异常: {}", filePath, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取文件访问URL]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFileUrl(String filePath) {
|
||||||
|
if (!StringUtils.hasText(filePath)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果已经是完整URL,直接返回
|
||||||
|
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整URL
|
||||||
|
String protocol = IpUtil.getProtocol(ServletUtil.getRequest());
|
||||||
|
if (!StringUtils.hasText(protocol)) {
|
||||||
|
protocol = "http";
|
||||||
|
}
|
||||||
|
String hostIp = IpUtil.getHostIp(ServletUtil.getRequest());
|
||||||
|
String hostPort = StringUtils.hasText(env.getProperty("server.port")) ?
|
||||||
|
env.getProperty("server.port") : "18099";
|
||||||
|
|
||||||
|
// 处理文件路径,确保以/开头
|
||||||
|
String normalizedPath = filePath.startsWith("/") ? filePath : "/" + filePath;
|
||||||
|
|
||||||
|
return protocol + "://" + hostIp + ":" + hostPort + normalizedPath;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成文件访问URL失败: {}", filePath, e);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [检查文件是否存在]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean fileExists(String filePath) {
|
||||||
|
if (!StringUtils.hasText(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File file = new File(filePath);
|
||||||
|
return file.exists() && file.isFile();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查文件是否存在失败: {}", filePath, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取存储服务类型]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getStorageType() {
|
||||||
|
// 本地存储对应数据库中的"1"
|
||||||
|
return CoderConstants.ONE_STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
package org.leocoder.heritage.oss.service;
|
||||||
|
|
||||||
|
import io.minio.*;
|
||||||
|
import io.minio.errors.ErrorResponseException;
|
||||||
|
import io.minio.http.Method;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.common.constants.CoderConstants;
|
||||||
|
import org.leocoder.heritage.common.exception.BusinessException;
|
||||||
|
import org.leocoder.heritage.common.utils.file.FileTypeUtil;
|
||||||
|
import org.leocoder.heritage.oss.config.MinioConfig;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO对象存储服务实现
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
|
||||||
|
public class MinioStorageService implements StorageService {
|
||||||
|
|
||||||
|
private final MinioClient minioClient;
|
||||||
|
private final MinioConfig minioConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [上传文件到MinIO]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||||||
|
try {
|
||||||
|
// 确保存储桶存在
|
||||||
|
ensureBucketExists();
|
||||||
|
|
||||||
|
// 构建对象名称
|
||||||
|
String objectName = buildObjectName(folderPath, fileName);
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
minioClient.putObject(
|
||||||
|
PutObjectArgs.builder()
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.object(objectName)
|
||||||
|
.stream(file.getInputStream(), file.getSize(), -1)
|
||||||
|
.contentType(file.getContentType())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 构建返回结果Map(保持与现有接口兼容)
|
||||||
|
Map<String, Object> fileMap = new HashMap<>();
|
||||||
|
fileMap.put("fileName", file.getOriginalFilename());
|
||||||
|
fileMap.put("newName", fileName);
|
||||||
|
fileMap.put("fileSize", FileTypeUtil.getFileSize(file));
|
||||||
|
fileMap.put("suffixName", FileTypeUtil.getFileType(file.getOriginalFilename()));
|
||||||
|
// MinIO对象名,用于删除操作
|
||||||
|
fileMap.put("filePath", objectName);
|
||||||
|
// 完整的访问URL
|
||||||
|
fileMap.put("fileUploadPath", getFileUrl(objectName));
|
||||||
|
|
||||||
|
log.info("MinIO文件上传成功: {}", fileName);
|
||||||
|
return fileMap;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MinIO文件上传失败", e);
|
||||||
|
throw new BusinessException(500, "文件上传失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [从MinIO删除文件]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean deleteFile(String objectName) {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.hasText(objectName)) {
|
||||||
|
log.warn("MinIO对象名为空,跳过删除");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("MinIO删除文件: {}", objectName);
|
||||||
|
minioClient.removeObject(
|
||||||
|
RemoveObjectArgs.builder()
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.object(objectName)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("MinIO删除成功: {}", objectName);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MinIO文件删除失败: {}", objectName, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取文件访问URL]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFileUrl(String objectName) {
|
||||||
|
if (!StringUtils.hasText(objectName)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果配置了自定义域名,构建直接访问URL
|
||||||
|
if (StringUtils.hasText(minioConfig.getDomain())) {
|
||||||
|
String cleanDomain = minioConfig.getDomain().replaceAll("/$", "");
|
||||||
|
String cleanObjectName = objectName.startsWith("/") ? objectName.substring(1) : objectName;
|
||||||
|
String directUrl = cleanDomain + "/" + minioConfig.getBucketName() + "/" + cleanObjectName;
|
||||||
|
return directUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有自定义域名,使用MinIO的预签名URL(有效期24小时)
|
||||||
|
String presignedUrl = minioClient.getPresignedObjectUrl(
|
||||||
|
GetPresignedObjectUrlArgs.builder()
|
||||||
|
.method(Method.GET)
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.object(objectName)
|
||||||
|
.expiry(24 * 60 * 60)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
return presignedUrl;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成MinIO文件访问URL失败: {}", objectName, e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [检查文件是否存在]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean fileExists(String objectName) {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.hasText(objectName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
minioClient.statObject(
|
||||||
|
StatObjectArgs.builder()
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.object(objectName)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (ErrorResponseException e) {
|
||||||
|
if ("NoSuchKey".equals(e.errorResponse().code())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.error("检查MinIO文件是否存在失败: {}", objectName, e);
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查MinIO文件是否存在失败: {}", objectName, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取存储服务类型]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getStorageType() {
|
||||||
|
// MinIO存储对应数据库中的"2"
|
||||||
|
return CoderConstants.TWO_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [构建MinIO对象名称]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
private String buildObjectName(String folderPath, String fileName) {
|
||||||
|
StringBuilder nameBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// 添加路径前缀
|
||||||
|
if (StringUtils.hasText(minioConfig.getPathPrefix())) {
|
||||||
|
nameBuilder.append(minioConfig.getPathPrefix()).append("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件夹路径
|
||||||
|
if (StringUtils.hasText(folderPath)) {
|
||||||
|
// 确保路径以/结尾
|
||||||
|
String normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
|
||||||
|
nameBuilder.append(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件名
|
||||||
|
nameBuilder.append(fileName);
|
||||||
|
|
||||||
|
return nameBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [确保存储桶存在]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
private void ensureBucketExists() throws Exception {
|
||||||
|
boolean bucketExists = minioClient.bucketExists(
|
||||||
|
BucketExistsArgs.builder()
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!bucketExists) {
|
||||||
|
log.info("创建MinIO存储桶: {}", minioConfig.getBucketName());
|
||||||
|
minioClient.makeBucket(
|
||||||
|
MakeBucketArgs.builder()
|
||||||
|
.bucket(minioConfig.getBucketName())
|
||||||
|
.region(minioConfig.getRegion())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
package org.leocoder.heritage.oss.service;
|
||||||
|
|
||||||
|
import com.aliyun.oss.OSS;
|
||||||
|
import com.aliyun.oss.model.ObjectMetadata;
|
||||||
|
import com.aliyun.oss.model.PutObjectRequest;
|
||||||
|
import com.aliyun.oss.model.PutObjectResult;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.common.constants.CoderConstants;
|
||||||
|
import org.leocoder.heritage.common.exception.BusinessException;
|
||||||
|
import org.leocoder.heritage.common.utils.file.FileTypeUtil;
|
||||||
|
import org.leocoder.heritage.oss.config.OssConfig;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云OSS存储服务实现
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
|
||||||
|
public class OssStorageService implements StorageService {
|
||||||
|
|
||||||
|
private final OSS ossClient;
|
||||||
|
private final OssConfig ossConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [上传文件到阿里云OSS]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||||||
|
try {
|
||||||
|
log.info("OSS上传文件: fileName={}, folderPath={}", fileName, folderPath);
|
||||||
|
|
||||||
|
// 构建对象键
|
||||||
|
String objectKey = buildObjectKey(folderPath, fileName);
|
||||||
|
|
||||||
|
// 设置元数据
|
||||||
|
ObjectMetadata metadata = new ObjectMetadata();
|
||||||
|
metadata.setContentLength(file.getSize());
|
||||||
|
metadata.setContentType(file.getContentType());
|
||||||
|
metadata.setContentDisposition("inline");
|
||||||
|
|
||||||
|
// 创建上传请求
|
||||||
|
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||||
|
ossConfig.getBucketName(),
|
||||||
|
objectKey,
|
||||||
|
file.getInputStream(),
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行上传
|
||||||
|
PutObjectResult result = ossClient.putObject(putObjectRequest);
|
||||||
|
|
||||||
|
// 构建返回结果Map(保持与现有接口兼容)
|
||||||
|
Map<String, Object> fileMap = new HashMap<>();
|
||||||
|
fileMap.put("fileName", file.getOriginalFilename());
|
||||||
|
fileMap.put("newName", fileName);
|
||||||
|
fileMap.put("fileSize", FileTypeUtil.getFileSize(file));
|
||||||
|
fileMap.put("suffixName", FileTypeUtil.getFileType(file.getOriginalFilename()));
|
||||||
|
// OSS对象键,用于删除操作
|
||||||
|
fileMap.put("filePath", objectKey);
|
||||||
|
// 完整的访问URL
|
||||||
|
fileMap.put("fileUploadPath", getFileUrl(objectKey));
|
||||||
|
|
||||||
|
log.info("OSS上传成功: objectKey={}, url={}", objectKey, fileMap.get("fileUploadPath"));
|
||||||
|
return fileMap;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OSS文件上传失败", e);
|
||||||
|
throw new BusinessException(500, "文件上传失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [从阿里云OSS删除文件]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean deleteFile(String objectKey) {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.hasText(objectKey)) {
|
||||||
|
log.warn("OSS对象键为空,跳过删除");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("OSS删除文件: {}", objectKey);
|
||||||
|
ossClient.deleteObject(ossConfig.getBucketName(), objectKey);
|
||||||
|
|
||||||
|
log.info("OSS删除成功: {}", objectKey);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OSS文件删除失败: {}", objectKey, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取文件访问URL]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFileUrl(String objectKey) {
|
||||||
|
if (!StringUtils.hasText(objectKey)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果配置了自定义域名,使用自定义域名
|
||||||
|
if (StringUtils.hasText(ossConfig.getDomain())) {
|
||||||
|
return ossConfig.getDomain() + "/" + objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认的OSS域名
|
||||||
|
String protocol = ossConfig.getHttps() ? "https" : "http";
|
||||||
|
return protocol + "://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint() + "/" + objectKey;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成OSS文件访问URL失败: {}", objectKey, e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [检查文件是否存在]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean fileExists(String objectKey) {
|
||||||
|
try {
|
||||||
|
if (!StringUtils.hasText(objectKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ossClient.doesObjectExist(ossConfig.getBucketName(), objectKey);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查OSS文件是否存在失败: {}", objectKey, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取存储服务类型]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getStorageType() {
|
||||||
|
// OSS存储对应数据库中的"3"
|
||||||
|
return "3";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [构建OSS对象键]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
private String buildObjectKey(String folderPath, String fileName) {
|
||||||
|
StringBuilder keyBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// 添加路径前缀
|
||||||
|
if (StringUtils.hasText(ossConfig.getPathPrefix())) {
|
||||||
|
keyBuilder.append(ossConfig.getPathPrefix()).append("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件夹路径
|
||||||
|
if (StringUtils.hasText(folderPath)) {
|
||||||
|
// 确保路径以/结尾
|
||||||
|
String normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
|
||||||
|
keyBuilder.append(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文件名
|
||||||
|
keyBuilder.append(fileName);
|
||||||
|
|
||||||
|
return keyBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package org.leocoder.heritage.oss.service;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储服务接口
|
||||||
|
* 提供统一的文件存储操作接口,支持多种存储类型(本地、OSS等)
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public interface StorageService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [上传文件]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
Map<String, Object> uploadFile(MultipartFile file, String fileName, String folderPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [删除文件]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
boolean deleteFile(String filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取文件访问URL]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
String getFileUrl(String filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [检查文件是否存在]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
boolean fileExists(String filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取存储服务类型]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
String getStorageType();
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package org.leocoder.heritage.oss.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.common.exception.BusinessException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储服务工厂
|
||||||
|
* 根据配置类型获取对应的存储服务实现
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class StorageServiceFactory {
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [根据存储类型获取存储服务]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public StorageService getStorageService(String storageType) {
|
||||||
|
if (storageType == null || storageType.trim().isEmpty()) {
|
||||||
|
throw new BusinessException(400, "存储类型不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取所有StorageService实现
|
||||||
|
Map<String, StorageService> storageServiceMap = applicationContext.getBeansOfType(StorageService.class);
|
||||||
|
|
||||||
|
for (StorageService service : storageServiceMap.values()) {
|
||||||
|
if (isMatchingStorageType(service, storageType.toLowerCase())) {
|
||||||
|
log.debug("获取存储服务: {} -> {}", storageType, service.getClass().getSimpleName());
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException(404, "未找到对应的存储服务实现: " + storageType);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取存储服务失败: {}", storageType, e);
|
||||||
|
throw new BusinessException(500, "获取存储服务失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取默认存储服务(MinIO优先,然后OSS,最后本地存储)]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public StorageService getDefaultStorageService() {
|
||||||
|
try {
|
||||||
|
// 优先尝试获取MinIO服务
|
||||||
|
return getStorageService("minio");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("MinIO服务不可用,尝试OSS服务", e);
|
||||||
|
try {
|
||||||
|
// 尝试获取OSS服务
|
||||||
|
return getStorageService("oss");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("OSS服务不可用,使用本地存储作为降级方案", ex);
|
||||||
|
try {
|
||||||
|
return getStorageService("local");
|
||||||
|
} catch (Exception localEx) {
|
||||||
|
log.error("本地存储服务也不可用", localEx);
|
||||||
|
throw new BusinessException(500, "没有可用的存储服务");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [检查存储服务是否匹配指定类型]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
private boolean isMatchingStorageType(StorageService service, String storageType) {
|
||||||
|
String serviceClassName = service.getClass().getSimpleName().toLowerCase();
|
||||||
|
|
||||||
|
switch (storageType) {
|
||||||
|
case "local":
|
||||||
|
return serviceClassName.contains("local");
|
||||||
|
case "oss":
|
||||||
|
return serviceClassName.contains("oss");
|
||||||
|
case "minio":
|
||||||
|
return serviceClassName.contains("minio");
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package org.leocoder.heritage.oss.utils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.leocoder.heritage.common.utils.date.DateUtil;
|
||||||
|
import org.leocoder.heritage.common.utils.file.FileTypeUtil;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSS工具类
|
||||||
|
*
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OssUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [生成唯一文件名]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public static String generateUniqueFileName(String originalFilename) {
|
||||||
|
if (!StringUtils.hasText(originalFilename)) {
|
||||||
|
throw new IllegalArgumentException("原始文件名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
String extension = FileTypeUtil.getFileType(originalFilename);
|
||||||
|
|
||||||
|
// 生成时间戳
|
||||||
|
String timeStamp = DateUtil.getCurrentDate("yyyyMMddHHmmss");
|
||||||
|
|
||||||
|
// 生成6位UUID
|
||||||
|
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 6);
|
||||||
|
|
||||||
|
// 组合文件名
|
||||||
|
return timeStamp + "-" + uuid + "." + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [构建文件夹路径]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public static String buildFolderPath(String folderName, String username) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
StringBuilder pathBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
if (StringUtils.hasText(folderName)) {
|
||||||
|
pathBuilder.append(folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(username)) {
|
||||||
|
pathBuilder.append("/").append(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加日期目录结构
|
||||||
|
pathBuilder.append("/")
|
||||||
|
.append(now.getYear())
|
||||||
|
.append("/")
|
||||||
|
.append(String.format("%02d", now.getMonthValue()))
|
||||||
|
.append("/")
|
||||||
|
.append(String.format("%02d", now.getDayOfMonth()));
|
||||||
|
|
||||||
|
return pathBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [验证OSS对象键格式]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public static boolean isValidObjectKey(String objectKey) {
|
||||||
|
if (!StringUtils.hasText(objectKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSS对象名不能以/开头
|
||||||
|
if (objectKey.startsWith("/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectKey.contains("//")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSS对象名长度限制
|
||||||
|
if (objectKey.length() > 1023) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [规范化对象键]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public static String normalizeObjectKey(String objectKey) {
|
||||||
|
if (!StringUtils.hasText(objectKey)) {
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = objectKey;
|
||||||
|
|
||||||
|
// 移除开头的/
|
||||||
|
while (normalized.startsWith("/")) {
|
||||||
|
normalized = normalized.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换连续的//为/
|
||||||
|
while (normalized.contains("//")) {
|
||||||
|
normalized = normalized.replace("//", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description [获取文件类型编码]
|
||||||
|
* @author Leocoder
|
||||||
|
*/
|
||||||
|
public static String getFileTypeCode(String filename) {
|
||||||
|
return FileTypeUtil.checkFileExtension(FileTypeUtil.getFileType(filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user