mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-09 20:57:21 +08:00
refactor(system): 重构存储配置及文件上传相关代码
存储配置自动处理:domain 不能以 / 结尾,bucket 必须以 / 结尾 文件上传:path 自动处理 Closes #IC6V43
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user