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; package top.continew.admin.system.config.file;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil; import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.recorder.FileRecorder; import org.dromara.x.file.storage.core.recorder.FileRecorder;
import org.dromara.x.file.storage.core.upload.FilePartInfo; import org.dromara.x.file.storage.core.upload.FilePartInfo;
import org.springframework.stereotype.Component; 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.FileMapper;
import top.continew.admin.system.mapper.StorageMapper; import top.continew.admin.system.mapper.StorageMapper;
import top.continew.admin.system.model.entity.FileDO; import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.model.entity.StorageDO; import top.continew.admin.system.model.entity.StorageDO;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.URLUtils;
import java.util.Optional; import java.util.Optional;
@@ -52,53 +47,24 @@ public class FileRecorderImpl implements FileRecorder {
private final FileMapper fileMapper; private final FileMapper fileMapper;
private final StorageMapper storageMapper; private final StorageMapper storageMapper;
private final IdentifierGenerator identifierGenerator;
/**
* 文件信息存储
*
* @param fileInfo 文件信息对象
* @return 是否保存成功
*/
@Override @Override
public boolean save(FileInfo fileInfo) { public boolean save(FileInfo fileInfo) {
FileDO file = new FileDO(); // 保存文件信息
Number id = identifierGenerator.nextId(fileInfo); FileDO file = new FileDO(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);
StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false)); 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.setStorageId(storage.getId());
file.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime()));
file.setUpdateUser(UserContextHolder.getUserId());
file.setUpdateTime(file.getCreateTime());
fileMapper.insert(file); 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; return true;
} }

View File

@@ -24,8 +24,8 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.continew.admin.common.enums.DisEnableStatusEnum; 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.query.StorageQuery;
import top.continew.admin.system.model.req.StorageReq;
import top.continew.admin.system.model.resp.StorageResp; import top.continew.admin.system.model.resp.StorageResp;
import top.continew.admin.system.service.StorageService; import top.continew.admin.system.service.StorageService;
@@ -52,6 +52,6 @@ public class FileStorageConfigLoader implements ApplicationRunner {
if (CollUtil.isEmpty(storageList)) { if (CollUtil.isEmpty(storageList)) {
return; 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; package top.continew.admin.system.enums;
import cn.hutool.core.util.StrUtil;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; 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.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 Integer value;
private final String description; 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; 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.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.SneakyThrows; import lombok.NoArgsConstructor;
import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileInfo;
import top.continew.admin.common.model.entity.BaseDO; import top.continew.admin.common.model.entity.BaseDO;
import top.continew.admin.system.enums.FileTypeEnum; 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.constant.StringConstants;
import top.continew.starter.core.util.StrUtils; import top.continew.starter.core.util.StrUtils;
import java.io.Serial; import java.io.Serial;
import java.net.URL;
import java.util.Map; import java.util.Map;
/** /**
@@ -39,6 +40,7 @@ import java.util.Map;
* @since 2023/12/23 10:38 * @since 2023/12/23 10:38
*/ */
@Data @Data
@NoArgsConstructor
@TableName("sys_file") @TableName("sys_file")
public class FileDO extends BaseDO { public class FileDO extends BaseDO {
@@ -116,65 +118,63 @@ public class FileDO extends BaseDO {
private Long storageId; private Long storageId;
/** /**
* 转换为 X-File-Storage 文件信息对象 * 基于 {@link FileInfo} 构建文件信息对象
* *
* @param storageDO 存储桶信息 * @param fileInfo {@link FileInfo} 文件信息
* @return X-File-Storage 文件信息对象
*/ */
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 fileInfo = new FileInfo();
fileInfo.setUrl(this.url); fileInfo.setPlatform(storage.getCode());
fileInfo.setSize(this.size);
fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH) fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH)
? StrUtil.subAfter(this.url, StringConstants.SLASH, true) ? StrUtil.subAfter(this.url, StringConstants.SLASH, true)
: this.url); : this.url);
fileInfo.setOriginalFilename(StrUtils fileInfo.setOriginalFilename(StrUtils
.blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex)); .blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex));
fileInfo.setBasePath(StringConstants.EMPTY); fileInfo.setBasePath(StringConstants.EMPTY);
// 优化 path 处理 fileInfo.setSize(this.size);
fileInfo.setPath(extractRelativePath(this.url, storageDO)); fileInfo.setUrl(this.url);
fileInfo.setPath(StringConstants.SLASH.equals(this.absPath)
? StringConstants.EMPTY
: StrUtil.removePrefix(this.absPath, StringConstants.SLASH));
fileInfo.setExt(this.extension); fileInfo.setExt(this.extension);
fileInfo.setPlatform(storageDO.getCode()); if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setThUrl(this.thumbnailUrl); fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
// 缩略图信息
fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH) fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH)
? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true) ? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true)
: this.thumbnailUrl); : this.thumbnailUrl);
fileInfo.setThSize(this.thumbnailSize); fileInfo.setThSize(this.thumbnailSize);
fileInfo.setThUrl(this.thumbnailUrl);
if (StrUtil.isNotBlank(this.thumbnailMetadata)) { if (StrUtil.isNotBlank(this.thumbnailMetadata)) {
fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class)); fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class));
} }
if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
return fileInfo; 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(); 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 默认文件路径 * @return 默认文件路径
*/ */
default String getDefaultFilePath() { 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 return today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today
.getDayOfMonth() + StringConstants.SLASH; .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(); StorageDO getDefaultStorage();
/** /**
* 根据编码查询 * 根据编码查询(如果编码为空,则返回默认存储)
* *
* @param code 编码 * @param code 编码
* @return 存储信息 * @return 存储配置
*/ */
StorageDO getByCode(String code); 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.dromara.x.file.storage.core.upload.UploadPretreatment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; 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.enums.FileTypeEnum;
import top.continew.admin.system.mapper.FileMapper; import top.continew.admin.system.mapper.FileMapper;
import top.continew.admin.system.model.entity.FileDO; import top.continew.admin.system.model.entity.FileDO;
@@ -68,7 +69,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
private StorageService storageService; private StorageService storageService;
@Override @Override
protected void beforeDelete(List<Long> ids) { public void beforeDelete(List<Long> ids) {
List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list();
Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId));
for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) { 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(); List<String> allExtensions = FileTypeEnum.getAllExtensions();
CheckUtils.throwIf(!allExtensions.contains(extName), "不支持的文件类型,仅支持 {} 格式的文件", String CheckUtils.throwIf(!allExtensions.contains(extName), "不支持的文件类型,仅支持 {} 格式的文件", String
.join(StringConstants.CHINESE_COMMA, allExtensions)); .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) UploadPretreatment uploadPretreatment = fileStorageService.of(file)
.setPlatform(storage.getCode()) .setPlatform(storage.getCode())
.setHashCalculatorSha256(true) .setHashCalculatorSha256(true)
.putAttr(ClassUtil.getClassName(StorageDO.class, false), storage) .putAttr(ClassUtil.getClassName(StorageDO.class, false), storage)
.setPath(path); .setPath(this.pretreatmentPath(path));
// 图片文件生成缩略图 // 图片文件生成缩略图
if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) { if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) {
uploadPretreatment.setIgnoreThumbnailException(true, true);
uploadPretreatment.thumbnail(img -> img.size(100, 100)); uploadPretreatment.thumbnail(img -> img.size(100, 100));
} }
uploadPretreatment.setProgressMonitor(new ProgressListener() { uploadPretreatment.setProgressMonitor(new ProgressListener() {
@@ -122,6 +117,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
log.info("上传结束"); log.info("上传结束");
} }
}); });
// 上传
return uploadPretreatment.upload(); return uploadPretreatment.upload();
} }
@@ -194,4 +190,28 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); 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; package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil; 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.model.resp.StorageResp;
import top.continew.admin.system.service.FileService; import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService; 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.constant.StringConstants;
import top.continew.starter.core.util.ExceptionUtils; 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.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils; import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.service.BaseServiceImpl; import top.continew.starter.extension.crud.service.BaseServiceImpl;
@@ -68,33 +65,55 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
@Override @Override
public void beforeCreate(StorageReq req) { 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(); String code = req.getCode();
CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code);
// 单独指定默认存储 // 需要独立操作来指定默认存储
req.setIsDefault(false); 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 @Override
public void beforeUpdate(StorageReq req, Long id) { public void beforeUpdate(StorageReq req, Long id) {
// 解密密钥
StorageDO oldStorage = super.getById(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(), "不允许修改存储类型"); CheckUtils.throwIfNotEqual(req.getType(), oldStorage.getType(), "不允许修改存储类型");
this.decodeSecretKey(req, oldStorage); CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码");
DisEnableStatusEnum newStatus = req.getStatus(); DisEnableStatusEnum newStatus = req.getStatus();
CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE
.equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName()); .equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName());
// 重新加载配置 // 指定配置参数校验及预处理
// 先卸载 StorageTypeEnum storageType = req.getType();
if (fileStorageService.getFileStorage(req.getCode()) != null) { storageType.validate(req);
this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class)); storageType.pretreatment(req);
} // 卸载存储引擎
// 再加载 this.unload(oldStorage);
if (DisEnableStatusEnum.ENABLE.equals(newStatus)) { }
this.load(req);
@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) { public void beforeDelete(List<Long> ids) {
CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试"); CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试");
List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list(); List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list();
storageList.forEach(s -> { storageList.forEach(storage -> {
CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储,不允许删除", s.getName()); CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许删除", storage.getName());
// 卸载启用状态的存储 // 卸载存储引擎
if (DisEnableStatusEnum.ENABLE.equals(s.getStatus())) { this.unload(storage);
this.unload(BeanUtil.copyProperties(s, StorageReq.class));
}
}); });
} }
@@ -123,14 +140,13 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
// 修改状态 // 修改状态
baseMapper.lambdaUpdate().eq(StorageDO::getId, id).set(StorageDO::getStatus, newStatus).update(); baseMapper.lambdaUpdate().eq(StorageDO::getId, id).set(StorageDO::getStatus, newStatus).update();
// 加载、卸载存储引擎 // 加载、卸载存储引擎
StorageReq storageReq = BeanUtil.copyProperties(storage, StorageReq.class);
switch (newStatus) { switch (newStatus) {
case ENABLE: case ENABLE:
this.load(storageReq); this.load(storage);
break; break;
case DISABLE: case DISABLE:
CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许禁用", storage.getName()); CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许禁用", storage.getName());
this.unload(storageReq); this.unload(storage);
break; break;
default: default:
break; break;
@@ -152,87 +168,86 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
@Override @Override
public StorageDO getDefaultStorage() { 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 @Override
public StorageDO getByCode(String code) { 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 @Override
public void load(StorageReq req) { public void load(StorageDO storage) {
CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList(); CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
String domain = req.getDomain(); switch (storage.getType()) {
ValidationUtils.throwIf(!URLUtils.isHttpUrl(domain), "域名格式不正确"); case LOCAL -> {
String bucketName = req.getBucketName(); FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig();
StorageTypeEnum type = req.getType(); config.setPlatform(storage.getCode());
if (StorageTypeEnum.LOCAL.equals(type)) { config.setStoragePath(storage.getBucketName());
ValidationUtils.validate(req, ValidationGroup.Storage.Local.class); fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
req.setBucketName(StrUtil.appendIfMissing(bucketName .singletonList(config)));
.replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH)); SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage
FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig(); .getBucketName()));
config.setPlatform(req.getCode()); }
config.setStoragePath(bucketName); case OSS -> {
fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
.singletonList(config))); config.setPlatform(storage.getCode());
SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), bucketName)); config.setAccessKey(storage.getAccessKey());
} else if (StorageTypeEnum.OSS.equals(type)) { config.setSecretKey(storage.getSecretKey());
ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class); config.setEndPoint(storage.getEndpoint());
FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config(); config.setBucketName(storage.getBucketName());
config.setPlatform(req.getCode()); config.setDomain(storage.getDomain());
config.setAccessKey(req.getAccessKey()); fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
config.setSecretKey(req.getSecretKey()); .singletonList(config), null));
config.setEndPoint(req.getEndpoint()); }
config.setBucketName(bucketName); default -> throw new IllegalArgumentException("不支持的存储类型:%s".formatted(storage.getType()));
config.setDomain(domain);
fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
.singletonList(config), null));
} }
} }
@Override @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(); CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList();
FileStorage fileStorage = fileStorageService.getFileStorage(req.getCode());
fileStorageList.remove(fileStorage); fileStorageList.remove(fileStorage);
fileStorage.close(); 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 * 解密 SecretKey
* *
* @param req 请求参数 * @param encryptSecretKey 加密的 SecretKey
* @param storage 存储信息 * @param storage 存储信息
* @return 解密后的 SecretKey
*/ */
private void decodeSecretKey(StorageReq req, StorageDO storage) { private String decryptSecretKey(String encryptSecretKey, StorageDO storage) {
if (!StorageTypeEnum.OSS.equals(req.getType())) {
return;
}
// 修改时,如果 SecretKey 不修改,需要手动修正 // 修改时,如果 SecretKey 不修改,需要手动修正
String newSecretKey = req.getSecretKey(); if (null != storage) {
boolean isSecretKeyNotUpdate = StrUtil.isBlank(newSecretKey) || newSecretKey.contains(StringConstants.ASTERISK); boolean isSecretKeyNotUpdate = StrUtil.isBlank(encryptSecretKey) || encryptSecretKey
if (null != storage && isSecretKeyNotUpdate) { .contains(StringConstants.ASTERISK);
req.setSecretKey(storage.getSecretKey()); if (isSecretKeyNotUpdate) {
return; return storage.getSecretKey();
}
} }
// 新增时或修改了 SecretKey // 新增场景,直接解密 SecretKey
String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(newSecretKey)); String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(encryptSecretKey));
ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败"); ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败");
ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符"); ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符");
req.setSecretKey(secretKey); return secretKey;
}
/**
* 默认存储是否存在
*
* @param id ID
* @return 是否存在
*/
private boolean isDefaultExists(Long id) {
return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).ne(null != id, StorageDO::getId, id).exists();
} }
/** /**

View File

@@ -35,8 +35,6 @@ import top.continew.admin.system.enums.OptionCategoryEnum;
import top.continew.admin.system.model.query.*; import top.continew.admin.system.model.query.*;
import top.continew.admin.system.model.resp.file.FileUploadResp; import top.continew.admin.system.model.resp.file.FileUploadResp;
import top.continew.admin.system.service.*; import top.continew.admin.system.service.*;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.StrUtils;
import top.continew.starter.core.validation.ValidationUtils; import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.LabelValueResp;
@@ -71,9 +69,7 @@ public class CommonController {
@PostMapping("/file") @PostMapping("/file")
public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file, String path) throws IOException { public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file, String path) throws IOException {
ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); ValidationUtils.throwIf(file::isEmpty, "文件不能为空");
String fixedPath = StrUtils.blankToDefault(path, StringConstants.SLASH, p -> StrUtil FileInfo fileInfo = fileService.upload(file, path);
.appendIfMissing(p, StringConstants.SLASH));
FileInfo fileInfo = fileService.upload(file, fixedPath);
return FileUploadResp.builder() return FileUploadResp.builder()
.id(fileInfo.getId()) .id(fileInfo.getId())
.url(fileInfo.getUrl()) .url(fileInfo.getUrl())