refactor(system): 重构存储配置及文件上传相关代码

存储配置自动处理:domain 不能以 / 结尾,bucket 必须以 / 结尾
文件上传:path 自动处理

Closes #IC6V43
This commit is contained in:
2025-05-14 23:01:08 +08:00
parent cd4adcf7a2
commit bc057da265
9 changed files with 261 additions and 219 deletions

View File

@@ -16,26 +16,21 @@
package top.continew.admin.system.config.file;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.recorder.FileRecorder;
import org.dromara.x.file.storage.core.upload.FilePartInfo;
import org.springframework.stereotype.Component;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.system.enums.FileTypeEnum;
import top.continew.admin.system.mapper.FileMapper;
import top.continew.admin.system.mapper.StorageMapper;
import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.URLUtils;
import java.util.Optional;
@@ -52,53 +47,24 @@ public class FileRecorderImpl implements FileRecorder {
private final FileMapper fileMapper;
private final StorageMapper storageMapper;
private final IdentifierGenerator identifierGenerator;
/**
* 文件信息存储
*
* @param fileInfo 文件信息对象
* @return 是否保存成功
*/
@Override
public boolean save(FileInfo fileInfo) {
FileDO file = new FileDO();
Number id = identifierGenerator.nextId(fileInfo);
file.setId(id.longValue());
fileInfo.setId(String.valueOf(id.longValue()));
String originalFilename = EscapeUtil.unescape(fileInfo.getOriginalFilename());
file.setName(StrUtil.contains(originalFilename, StringConstants.DOT)
? StrUtil.subBefore(originalFilename, StringConstants.DOT, true)
: originalFilename);
// 保存文件信息
FileDO file = new FileDO(fileInfo);
StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false));
String filePath = StrUtil.appendIfMissing(fileInfo.getPath(), StringConstants.SLASH);
// 处理fileInfo中存储的地址
fileInfo.setUrl(URLUtil.normalize(storage.getDomain() + filePath + fileInfo.getFilename()));
fileInfo.setThUrl(URLUtil.normalize(storage.getDomain() + filePath + fileInfo.getThFilename()));
file.setUrl(fileInfo.getUrl());
file.setSize(fileInfo.getSize());
String absPath = fileInfo.getPath();
String tempAbsPath = absPath.length() > 1 ? StrUtil.removeSuffix(absPath, StringConstants.SLASH) : absPath;
String[] pathArr = tempAbsPath.split(StringConstants.SLASH);
if (pathArr.length > 1) {
file.setParentPath(pathArr[pathArr.length - 1]);
} else {
file.setParentPath(StringConstants.SLASH);
}
file.setAbsPath(tempAbsPath);
file.setExtension(fileInfo.getExt());
file.setType(FileTypeEnum.getByExtension(file.getExtension()));
file.setContentType(fileInfo.getContentType());
file.setSha256(fileInfo.getHashInfo().getSha256());
file.setMetadata(JSONUtil.toJsonStr(fileInfo.getMetadata()));
file.setThumbnailUrl(fileInfo.getThUrl());
file.setThumbnailSize(fileInfo.getThSize());
file.setThumbnailMetadata(JSONUtil.toJsonStr(fileInfo.getThMetadata()));
file.setStorageId(storage.getId());
file.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime()));
file.setUpdateUser(UserContextHolder.getUserId());
file.setUpdateTime(file.getCreateTime());
fileMapper.insert(file);
// 方便文件上传完成后获取文件信息
fileInfo.setId(String.valueOf(file.getId()));
if (!URLUtils.isHttpUrl(fileInfo.getUrl())) {
String prefix = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH);
String url = URLUtil.normalize(prefix + fileInfo.getUrl());
fileInfo.setUrl(url);
if (StrUtil.isNotBlank(fileInfo.getThUrl())) {
fileInfo.setThUrl(URLUtil.normalize(prefix + fileInfo.getThUrl()));
}
}
return true;
}

View File

@@ -24,8 +24,8 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.query.StorageQuery;
import top.continew.admin.system.model.req.StorageReq;
import top.continew.admin.system.model.resp.StorageResp;
import top.continew.admin.system.service.StorageService;
@@ -52,6 +52,6 @@ public class FileStorageConfigLoader implements ApplicationRunner {
if (CollUtil.isEmpty(storageList)) {
return;
}
storageList.forEach(s -> storageService.load(BeanUtil.copyProperties(s, StorageReq.class)));
storageList.forEach(storage -> storageService.load(BeanUtil.copyProperties(storage, StorageDO.class)));
}
}

View File

@@ -16,9 +16,15 @@
package top.continew.admin.system.enums;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.admin.system.model.req.StorageReq;
import top.continew.admin.system.validation.ValidationGroup;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.enums.BaseEnum;
import top.continew.starter.core.util.URLUtils;
import top.continew.starter.core.validation.ValidationUtils;
/**
* 存储类型枚举
@@ -33,13 +39,48 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
/**
* 本地存储
*/
LOCAL(1, "本地存储"),
LOCAL(1, "本地存储") {
@Override
public void validate(StorageReq req) {
ValidationUtils.validate(req, ValidationGroup.Storage.Local.class);
ValidationUtils.throwIf(!URLUtils.isHttpUrl(req.getDomain()), "访问路径格式不正确");
}
@Override
public void pretreatment(StorageReq req) {
super.pretreatment(req);
req.setBucketName(StrUtil.appendIfMissing(req.getBucketName()
.replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH));
}
},
/**
* 对象存储
*/
OSS(2, "对象存储");
OSS(2, "对象存储") {
@Override
public void validate(StorageReq req) {
ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class);
ValidationUtils.throwIf(!URLUtils.isHttpUrl(req.getDomain()), "域名格式不正确");
}
};
private final Integer value;
private final String description;
/**
* 校验
*
* @param req 请求参数
*/
public abstract void validate(StorageReq req);
/**
* 处理参数
*
* @param req 请求参数
*/
public void pretreatment(StorageReq req) {
req.setDomain(StrUtil.removeSuffix(req.getDomain(), StringConstants.SLASH));
}
}

View File

@@ -16,20 +16,21 @@
package top.continew.admin.system.model.entity;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.NoArgsConstructor;
import org.dromara.x.file.storage.core.FileInfo;
import top.continew.admin.common.model.entity.BaseDO;
import top.continew.admin.system.enums.FileTypeEnum;
import top.continew.admin.system.enums.StorageTypeEnum;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.StrUtils;
import java.io.Serial;
import java.net.URL;
import java.util.Map;
/**
@@ -39,6 +40,7 @@ import java.util.Map;
* @since 2023/12/23 10:38
*/
@Data
@NoArgsConstructor
@TableName("sys_file")
public class FileDO extends BaseDO {
@@ -116,65 +118,63 @@ public class FileDO extends BaseDO {
private Long storageId;
/**
* 转换为 X-File-Storage 文件信息对象
* 基于 {@link FileInfo} 构建文件信息对象
*
* @param storageDO 存储桶信息
* @return X-File-Storage 文件信息对象
* @param fileInfo {@link FileInfo} 文件信息
*/
public FileInfo toFileInfo(StorageDO storageDO) {
public FileDO(FileInfo fileInfo) {
this.name = FileNameUtil.getPrefix(EscapeUtil.unescape(fileInfo.getOriginalFilename()));
this.size = fileInfo.getSize();
this.url = fileInfo.getUrl();
this.absPath = StrUtil.isEmpty(fileInfo.getPath())
? StringConstants.SLASH
: StrUtil.prependIfMissing(fileInfo.getPath(), StringConstants.SLASH);
String[] pathAttr = this.absPath.split(StringConstants.SLASH);
this.parentPath = pathAttr.length > 1 ? pathAttr[pathAttr.length - 1] : StringConstants.SLASH;
this.extension = fileInfo.getExt();
this.contentType = fileInfo.getContentType();
this.type = FileTypeEnum.getByExtension(this.extension);
this.sha256 = fileInfo.getHashInfo().getSha256();
this.metadata = JSONUtil.toJsonStr(fileInfo.getMetadata());
this.thumbnailSize = fileInfo.getThSize();
this.thumbnailUrl = fileInfo.getThUrl();
this.thumbnailMetadata = JSONUtil.toJsonStr(fileInfo.getThMetadata());
this.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime()));
}
/**
* 转换为 {@link FileInfo} 文件信息对象
*
* @param storage 存储配置信息
* @return {@link FileInfo} 文件信息对象
*/
public FileInfo toFileInfo(StorageDO storage) {
FileInfo fileInfo = new FileInfo();
fileInfo.setUrl(this.url);
fileInfo.setSize(this.size);
fileInfo.setPlatform(storage.getCode());
fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH)
? StrUtil.subAfter(this.url, StringConstants.SLASH, true)
: this.url);
fileInfo.setOriginalFilename(StrUtils
.blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex));
fileInfo.setBasePath(StringConstants.EMPTY);
// 优化 path 处理
fileInfo.setPath(extractRelativePath(this.url, storageDO));
fileInfo.setSize(this.size);
fileInfo.setUrl(this.url);
fileInfo.setPath(StringConstants.SLASH.equals(this.absPath)
? StringConstants.EMPTY
: StrUtil.removePrefix(this.absPath, StringConstants.SLASH));
fileInfo.setExt(this.extension);
fileInfo.setPlatform(storageDO.getCode());
fileInfo.setThUrl(this.thumbnailUrl);
if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
// 缩略图信息
fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH)
? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true)
: this.thumbnailUrl);
fileInfo.setThSize(this.thumbnailSize);
fileInfo.setThUrl(this.thumbnailUrl);
if (StrUtil.isNotBlank(this.thumbnailMetadata)) {
fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class));
}
if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
return fileInfo;
}
/**
* 将文件路径处理成资源路径
* 例如:
* http://domain.cn/bucketName/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/
* http://bucketName.domain.cn/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/
*
* @param url 文件路径
* @param storageDO 存储桶信息
* @return
*/
@SneakyThrows
private static String extractRelativePath(String url, StorageDO storageDO) {
url = StrUtil.subBefore(url, StringConstants.SLASH, true) + StringConstants.SLASH;
if (storageDO.getType().equals(StorageTypeEnum.LOCAL)) {
return url;
}
// 提取 URL 中的路径部分
String fullPath = new URL(url).getPath();
// 移除开头的斜杠
String relativePath = fullPath.startsWith(StringConstants.SLASH) ? fullPath.substring(1) : fullPath;
// 如果路径以 bucketName 开头,则移除 bucketName 例如: bucketName/2024/11/27/ -> 2024/11/27/
if (relativePath.startsWith(storageDO.getBucketName())) {
return StrUtil.subAfter(relativePath, storageDO.getBucketName(), false);
}
return relativePath;
}
}

View File

@@ -89,9 +89,29 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
*/
FileStatisticsResp statistics();
/**
* 检查文件是否存在
*
* @param fileHash 文件 Hash
* @return 响应参数
*/
FileResp check(String fileHash);
/**
* 创建目录
*
* @param req 请求参数
* @return ID
*/
IdResp<Long> createDir(FileReq req);
/**
* 获取默认文件路径
*
* <p>
* 默认文件路径yyyy/MM/dd/
* </p>
*
* @return 默认文件路径
*/
default String getDefaultFilePath() {
@@ -99,20 +119,4 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
return today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today
.getDayOfMonth() + StringConstants.SLASH;
}
/**
* 检查文件是否存在
*
* @param fileHash 文件 Hash
* @return 响应参数
*/
FileResp check(String fileHash);
/**
* 创建目录
*
* @param req 请求参数
* @return ID
*/
IdResp<Long> createDir(FileReq req);
}

View File

@@ -50,29 +50,29 @@ public interface StorageService extends BaseService<StorageResp, StorageResp, St
/**
* 查询默认存储
*
* @return 存储信息
* @return 存储配置
*/
StorageDO getDefaultStorage();
/**
* 根据编码查询
* 根据编码查询(如果编码为空,则返回默认存储)
*
* @param code 编码
* @return 存储信息
* @return 存储配置
*/
StorageDO getByCode(String code);
/**
* 加载存储
* 加载存储引擎
*
* @param req 存储信息
* @param storage 存储配置
*/
void load(StorageReq req);
void load(StorageDO storage);
/**
* 卸载存储
* 卸载存储引擎
*
* @param req 存储信息
* @param storage 存储配置
*/
void unload(StorageReq req);
void unload(StorageDO storage);
}

View File

@@ -30,6 +30,7 @@ import org.dromara.x.file.storage.core.ProgressListener;
import org.dromara.x.file.storage.core.upload.UploadPretreatment;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.enums.FileTypeEnum;
import top.continew.admin.system.mapper.FileMapper;
import top.continew.admin.system.model.entity.FileDO;
@@ -68,7 +69,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
private StorageService storageService;
@Override
protected void beforeDelete(List<Long> ids) {
public void beforeDelete(List<Long> ids) {
List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list();
Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId));
for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) {
@@ -87,23 +88,17 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
List<String> allExtensions = FileTypeEnum.getAllExtensions();
CheckUtils.throwIf(!allExtensions.contains(extName), "不支持的文件类型,仅支持 {} 格式的文件", String
.join(StringConstants.CHINESE_COMMA, allExtensions));
// 获取存储信息
StorageDO storage;
if (StrUtil.isBlank(storageCode)) {
storage = storageService.getDefaultStorage();
CheckUtils.throwIfNull(storage, "请先指定默认存储");
} else {
storage = storageService.getByCode(storageCode);
CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", storageCode);
}
// 构建上传预处理对象
StorageDO storage = storageService.getByCode(storageCode);
CheckUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(storage.getStatus()), "请先启用存储 [{}]", storage.getCode());
UploadPretreatment uploadPretreatment = fileStorageService.of(file)
.setPlatform(storage.getCode())
.setHashCalculatorSha256(true)
.putAttr(ClassUtil.getClassName(StorageDO.class, false), storage)
.setPath(path);
.setPath(this.pretreatmentPath(path));
// 图片文件生成缩略图
if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) {
uploadPretreatment.setIgnoreThumbnailException(true, true);
uploadPretreatment.thumbnail(img -> img.size(100, 100));
}
uploadPretreatment.setProgressMonitor(new ProgressListener() {
@@ -122,6 +117,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
log.info("上传结束");
}
});
// 上传
return uploadPretreatment.upload();
}
@@ -194,4 +190,28 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode()));
}
}
/**
* 处理路径
*
* <p>
* 1.如果 path 为空,则使用 {@link FileService#getDefaultFilePath()} 作为默认值 <br />
* 2.如果 path 为 {@code /},则设置为空 <br />
* 3.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br />
* 4.如果 path 以 {@code /} 开头,则移除前缀 {@code /} <br />
* 示例yyyy/MM/dd/
* </p>
*
* @param path 路径
* @return 处理路径
*/
private String pretreatmentPath(String path) {
if (StrUtil.isBlank(path)) {
return this.getDefaultFilePath();
}
if (StringConstants.SLASH.equals(path)) {
return StringConstants.EMPTY;
}
return StrUtil.appendIfMissing(StrUtil.removePrefix(path, StringConstants.SLASH), StringConstants.SLASH);
}
}

View File

@@ -16,7 +16,6 @@
package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
@@ -39,10 +38,8 @@ import top.continew.admin.system.model.req.StorageReq;
import top.continew.admin.system.model.resp.StorageResp;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService;
import top.continew.admin.system.validation.ValidationGroup;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ExceptionUtils;
import top.continew.starter.core.util.URLUtils;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
@@ -68,33 +65,55 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
@Override
public void beforeCreate(StorageReq req) {
this.decodeSecretKey(req, null);
// 解密密钥
if (StorageTypeEnum.OSS.equals(req.getType())) {
req.setSecretKey(this.decryptSecretKey(req.getSecretKey(), null));
}
// 指定配置参数校验及预处理
StorageTypeEnum storageType = req.getType();
storageType.validate(req);
storageType.pretreatment(req);
// 校验存储编码
String code = req.getCode();
CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code);
// 单独指定默认存储
// 需要独立操作来指定默认存储
req.setIsDefault(false);
if (DisEnableStatusEnum.ENABLE.equals(req.getStatus())) {
this.load(req);
}
@Override
public void afterCreate(StorageReq req, StorageDO entity) {
// 加载存储引擎
if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) {
this.load(entity);
}
}
@Override
public void beforeUpdate(StorageReq req, Long id) {
// 解密密钥
StorageDO oldStorage = super.getById(id);
CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码");
if (StorageTypeEnum.OSS.equals(req.getType())) {
req.setSecretKey(this.decryptSecretKey(req.getSecretKey(), oldStorage));
}
// 校验存储编码、存储类型、状态
CheckUtils.throwIfNotEqual(req.getType(), oldStorage.getType(), "不允许修改存储类型");
this.decodeSecretKey(req, oldStorage);
CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码");
DisEnableStatusEnum newStatus = req.getStatus();
CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE
.equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName());
// 重新加载配置
// 先卸载
if (fileStorageService.getFileStorage(req.getCode()) != null) {
this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class));
}
// 再加载
if (DisEnableStatusEnum.ENABLE.equals(newStatus)) {
this.load(req);
// 指定配置参数校验及预处理
StorageTypeEnum storageType = req.getType();
storageType.validate(req);
storageType.pretreatment(req);
// 卸载存储引擎
this.unload(oldStorage);
}
@Override
public void afterUpdate(StorageReq req, StorageDO entity) {
// 加载存储引擎
if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) {
this.load(entity);
}
}
@@ -102,12 +121,10 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
public void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试");
List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list();
storageList.forEach(s -> {
CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储,不允许删除", s.getName());
// 卸载启用状态的存储
if (DisEnableStatusEnum.ENABLE.equals(s.getStatus())) {
this.unload(BeanUtil.copyProperties(s, StorageReq.class));
}
storageList.forEach(storage -> {
CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许删除", storage.getName());
// 卸载存储引擎
this.unload(storage);
});
}
@@ -123,14 +140,13 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
// 修改状态
baseMapper.lambdaUpdate().eq(StorageDO::getId, id).set(StorageDO::getStatus, newStatus).update();
// 加载、卸载存储引擎
StorageReq storageReq = BeanUtil.copyProperties(storage, StorageReq.class);
switch (newStatus) {
case ENABLE:
this.load(storageReq);
this.load(storage);
break;
case DISABLE:
CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许禁用", storage.getName());
this.unload(storageReq);
this.unload(storage);
break;
default:
break;
@@ -152,87 +168,86 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
@Override
public StorageDO getDefaultStorage() {
return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).eq(StorageDO::getStatus, DisEnableStatusEnum.ENABLE).one();
StorageDO storage = baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).one();
CheckUtils.throwIfNull(storage, "请先指定默认存储");
return storage;
}
@Override
public StorageDO getByCode(String code) {
return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one();
if (StrUtil.isBlank(code)) {
return this.getDefaultStorage();
}
StorageDO storage = baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one();
CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", code);
return storage;
}
@Override
public void load(StorageReq req) {
public void load(StorageDO storage) {
CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
String domain = req.getDomain();
ValidationUtils.throwIf(!URLUtils.isHttpUrl(domain), "域名格式不正确");
String bucketName = req.getBucketName();
StorageTypeEnum type = req.getType();
if (StorageTypeEnum.LOCAL.equals(type)) {
ValidationUtils.validate(req, ValidationGroup.Storage.Local.class);
req.setBucketName(StrUtil.appendIfMissing(bucketName
.replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH));
FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig();
config.setPlatform(req.getCode());
config.setStoragePath(bucketName);
fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
.singletonList(config)));
SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), bucketName));
} else if (StorageTypeEnum.OSS.equals(type)) {
ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class);
FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
config.setPlatform(req.getCode());
config.setAccessKey(req.getAccessKey());
config.setSecretKey(req.getSecretKey());
config.setEndPoint(req.getEndpoint());
config.setBucketName(bucketName);
config.setDomain(domain);
fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
.singletonList(config), null));
switch (storage.getType()) {
case LOCAL -> {
FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig();
config.setPlatform(storage.getCode());
config.setStoragePath(storage.getBucketName());
fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
.singletonList(config)));
SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage
.getBucketName()));
}
case OSS -> {
FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
config.setPlatform(storage.getCode());
config.setAccessKey(storage.getAccessKey());
config.setSecretKey(storage.getSecretKey());
config.setEndPoint(storage.getEndpoint());
config.setBucketName(storage.getBucketName());
config.setDomain(storage.getDomain());
fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
.singletonList(config), null));
}
default -> throw new IllegalArgumentException("不支持的存储类型:%s".formatted(storage.getType()));
}
}
@Override
public void unload(StorageReq req) {
public void unload(StorageDO storage) {
FileStorage fileStorage = fileStorageService.getFileStorage(storage.getCode());
if (fileStorage == null) {
return;
}
CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
FileStorage fileStorage = fileStorageService.getFileStorage(req.getCode());
fileStorageList.remove(fileStorage);
fileStorage.close();
SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), req
.getBucketName()));
// 本地存储引擎需要移除资源映射
if (StorageTypeEnum.LOCAL.equals(storage.getType())) {
SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage
.getBucketName()));
}
}
/**
* 解密 SecretKey
*
* @param req 请求参数
* @param storage 存储信息
* @param encryptSecretKey 加密的 SecretKey
* @param storage 存储信息
* @return 解密后的 SecretKey
*/
private void decodeSecretKey(StorageReq req, StorageDO storage) {
if (!StorageTypeEnum.OSS.equals(req.getType())) {
return;
}
private String decryptSecretKey(String encryptSecretKey, StorageDO storage) {
// 修改时,如果 SecretKey 不修改,需要手动修正
String newSecretKey = req.getSecretKey();
boolean isSecretKeyNotUpdate = StrUtil.isBlank(newSecretKey) || newSecretKey.contains(StringConstants.ASTERISK);
if (null != storage && isSecretKeyNotUpdate) {
req.setSecretKey(storage.getSecretKey());
return;
if (null != storage) {
boolean isSecretKeyNotUpdate = StrUtil.isBlank(encryptSecretKey) || encryptSecretKey
.contains(StringConstants.ASTERISK);
if (isSecretKeyNotUpdate) {
return storage.getSecretKey();
}
}
// 新增时或修改了 SecretKey
String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(newSecretKey));
// 新增场景,直接解密 SecretKey
String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(encryptSecretKey));
ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败");
ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符");
req.setSecretKey(secretKey);
}
/**
* 默认存储是否存在
*
* @param id ID
* @return 是否存在
*/
private boolean isDefaultExists(Long id) {
return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).ne(null != id, StorageDO::getId, id).exists();
return secretKey;
}
/**