refactor(system/file): 重构文件管理表结构,新增计算文件大小接口

This commit is contained in:
2025-05-16 23:02:38 +08:00
parent 37027c774b
commit 798182d120
20 changed files with 314 additions and 189 deletions

View File

@@ -29,11 +29,6 @@ public class ContainerConstants {
*/
public static final String USER_NICKNAME = "UserNickname";
/**
* 文件信息
*/
public static final String FILE_INFO = "FileInfo";
/**
* 用户角色 ID 列表
*/

View File

@@ -1,54 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.config.file;
import cn.crane4j.core.container.Container;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import top.continew.admin.common.constant.ContainerConstants;
import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.service.FileService;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 文件信息填充容器
*
* @author luoqiz
* @since 2025/3/12 18:11
*/
@Component
@RequiredArgsConstructor
public class FileInfoContainer implements Container<Long> {
private final FileService fileService;
@Override
public String getNamespace() {
return ContainerConstants.FILE_INFO;
}
@Override
public Map<Long, FileDO> get(Collection<Long> ids) {
List<FileDO> list = fileService.listByIds(ids);
return list.stream().collect(Collectors.toMap(FileDO::getId, Function.identity()));
}
}

View File

@@ -62,11 +62,11 @@ public class FileRecorderImpl implements FileRecorder {
// 方便文件上传完成后获取文件信息
fileInfo.setId(String.valueOf(file.getId()));
if (!URLUtils.isHttpUrl(fileInfo.getUrl())) {
String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint());
String url = URLUtil.completeUrl(prefix, fileInfo.getUrl());
String prefix = storage.getUrlPrefix();
String url = URLUtil.normalize(prefix + fileInfo.getUrl(), false, true);
fileInfo.setUrl(url);
if (StrUtil.isNotBlank(fileInfo.getThUrl())) {
fileInfo.setThUrl(URLUtil.completeUrl(prefix, fileInfo.getThUrl()));
fileInfo.setThUrl(URLUtil.normalize(prefix + fileInfo.getThUrl(), false, true));
}
}
return true;
@@ -117,8 +117,7 @@ public class FileRecorderImpl implements FileRecorder {
.eq(FileDO::getName, StrUtil.subAfter(url, StringConstants.SLASH, true));
// 非 HTTP URL 场景
if (!URLUtils.isHttpUrl(url)) {
return queryWrapper.eq(FileDO::getPath, StrUtil.prependIfMissing(StrUtil
.subBefore(url, StringConstants.SLASH, true), StringConstants.SLASH)).one();
return queryWrapper.eq(FileDO::getPath, StrUtil.prependIfMissing(url, StringConstants.SLASH)).one();
}
// HTTP URL 场景
List<FileDO> list = queryWrapper.list();
@@ -137,8 +136,7 @@ public class FileRecorderImpl implements FileRecorder {
String urlPrefix = StrUtil.subBefore(url, StringConstants.SLASH, true);
// http://localhost:8000/file/ + /user/avatar => http://localhost:8000/file/user/avatar
StorageDO storage = storageMap.get(file.getStorageId());
String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint());
return urlPrefix.equals(URLUtil.normalize(prefix + file.getPath(), false, true));
return urlPrefix.equals(URLUtil.normalize(storage.getUrlPrefix() + file.getParentPath(), false, true));
}).findFirst().orElse(null);
}
}

View File

@@ -83,7 +83,7 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
* @param req 请求参数
*/
public void pretreatment(StorageReq req) {
// 域名需要以 “/” 结尾
// 域名需要以 “/” 结尾x-file-storage 在拼接路径时都是直接 + 拼接,所以规范要求每一级都要以 “/” 结尾,且后面路径不能以 “/” 开头)
if (StrUtil.isNotBlank(req.getDomain())) {
req.setDomain(StrUtil.appendIfMissing(req.getDomain(), StringConstants.SLASH));
}

View File

@@ -36,6 +36,6 @@ public interface FileMapper extends BaseMapper<FileDO> {
*
* @return 文件资源统计信息
*/
@Select("SELECT type, COUNT(1) number, SUM(size) size FROM sys_file GROUP BY type")
@Select("SELECT type, COUNT(1) number, SUM(size) size FROM sys_file WHERE type != 0 GROUP BY type")
List<FileStatisticsResp> statistics();
}

View File

@@ -60,7 +60,12 @@ public class FileDO extends BaseDO {
private Long size;
/**
* 存储路径
* 上级目录
*/
private String parentPath;
/**
* 路径
*/
private String path;
@@ -119,10 +124,11 @@ public class FileDO extends BaseDO {
this.originalName = fileInfo.getOriginalFilename();
this.size = fileInfo.getSize();
// 如果为空,则为 /;如果不为空,则调整格式为:/xxx
this.path = StrUtil.isEmpty(fileInfo.getPath())
this.parentPath = StrUtil.isEmpty(fileInfo.getPath())
? StringConstants.SLASH
: StrUtil.removeSuffix(StrUtil.prependIfMissing(fileInfo
.getPath(), StringConstants.SLASH), StringConstants.SLASH);
this.path = StrUtil.prependIfMissing(fileInfo.getUrl(), StringConstants.SLASH);
this.extension = fileInfo.getExt();
this.contentType = fileInfo.getContentType();
this.type = FileTypeEnum.getByExtension(this.extension);
@@ -148,15 +154,16 @@ public class FileDO extends BaseDO {
// 暂不使用,所以保持空
fileInfo.setBasePath(StringConstants.EMPTY);
fileInfo.setSize(this.size);
fileInfo.setPath(StringConstants.SLASH.equals(this.path)
fileInfo.setPath(StringConstants.SLASH.equals(this.parentPath)
? StringConstants.EMPTY
: StrUtil.appendIfMissing(StrUtil.removePrefix(this.path, StringConstants.SLASH), StringConstants.SLASH));
: StrUtil.appendIfMissing(StrUtil
.removePrefix(this.parentPath, StringConstants.SLASH), StringConstants.SLASH));
fileInfo.setExt(this.extension);
fileInfo.setContentType(this.contentType);
if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
fileInfo.setUrl(fileInfo.getPath() + fileInfo.getFilename());
fileInfo.setUrl(StrUtil.removePrefix(this.path, StringConstants.SLASH));
// 缩略图信息
fileInfo.setThFilename(this.thumbnailName);
fileInfo.setThSize(this.thumbnailSize);
@@ -166,4 +173,11 @@ public class FileDO extends BaseDO {
}
return fileInfo;
}
public void setParentPath(String parentPath) {
this.parentPath = parentPath;
this.path = StringConstants.SLASH.equals(parentPath)
? parentPath + this.name
: parentPath + StringConstants.SLASH + this.name;
}
}

View File

@@ -16,14 +16,20 @@
package top.continew.admin.system.model.entity;
import cn.hutool.core.lang.RegexPool;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.enums.StorageTypeEnum;
import top.continew.admin.common.model.entity.BaseDO;
import top.continew.admin.system.enums.StorageTypeEnum;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import java.io.Serial;
import java.net.URL;
/**
* 存储实体
@@ -99,4 +105,28 @@ public class StorageDO extends BaseDO {
* 状态
*/
private DisEnableStatusEnum status;
/**
* 获取 URL 前缀
* <p>
* LOCAL{@link #domain}/ <br />
* OSS域名不为空{@link #domain}/Endpoint 不是
* IPhttp(s)://{@link #bucketName}.{@link #endpoint}/;否则:{@link #endpoint}/{@link #bucketName}/
* </p>
*
* @return URL 前缀
*/
public String getUrlPrefix() {
if (StrUtil.isNotBlank(this.domain) || StorageTypeEnum.LOCAL.equals(this.type)) {
return StrUtil.appendIfMissing(this.domain, StringConstants.SLASH);
}
URL url = URLUtil.url(this.endpoint);
String host = url.getHost();
// IPMinIO 则拼接 BucketName
if (ReUtil.isMatch(RegexPool.IPV4, host) || ReUtil.isMatch(RegexPool.IPV6, host)) {
return StrUtil
.appendIfMissing(this.endpoint, StringConstants.SLASH) + this.bucketName + StringConstants.SLASH;
}
return "%s://%s.%s/".formatted(url.getProtocol(), this.bucketName, host);
}
}

View File

@@ -46,10 +46,10 @@ public class FileQuery implements Serializable {
private String originalName;
/**
* 存储路径
* 上级目录
*/
@Schema(description = "存储路径", example = "/")
private String path;
@Schema(description = "上级目录", example = "/")
private String parentPath;
/**
* 类型

View File

@@ -46,8 +46,8 @@ public class FileReq implements Serializable {
private String originalName;
/**
* 存储路径
* 上级目录
*/
@Schema(description = "存储路径", example = "/")
private String path;
@Schema(description = "上级目录", example = "/")
private String parentPath;
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.system.model.resp.file;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 文件夹计算大小响应参数
*
* @author Charles7c
* @since 2025/5/16 21:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "文件夹计算大小响应参数")
public class FileDirCalcSizeResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 大小(字节)
*/
@Schema(description = "大小(字节)", example = "4096")
private Long size;
}

View File

@@ -63,9 +63,15 @@ public class FileResp extends BaseDetailResp {
private String url;
/**
* 存储路径
* 上级目录
*/
@Schema(description = "上级目录", example = "/2025/2/25")
private String parentPath;
/**
* 路径
*/
@Schema(description = "路径", example = "/2025/2/25/6824afe8408da079832dcfb6.jpg")
private String path;
/**

View File

@@ -47,39 +47,39 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
* @throws IOException /
*/
default FileInfo upload(MultipartFile file) throws IOException {
return upload(file, getDefaultFilePath(), null);
return upload(file, getDefaultParentPath(), null);
}
/**
* 上传到默认存储
*
* @param file 文件信息
* @param path 文件路径
* @param parentPath 上级目录
* @return 文件信息
* @throws IOException /
*/
default FileInfo upload(MultipartFile file, String path) throws IOException {
return upload(file, path, null);
default FileInfo upload(MultipartFile file, String parentPath) throws IOException {
return upload(file, parentPath, null);
}
/**
* 上传到指定存储
*
* @param file 文件信息
* @param path 文件路径
* @param parentPath 上级目录
* @param storageCode 存储编码
* @return 文件信息
* @throws IOException /
*/
FileInfo upload(MultipartFile file, String path, String storageCode) throws IOException;
FileInfo upload(MultipartFile file, String parentPath, String storageCode) throws IOException;
/**
* 根据存储 ID 列表查询
* 创建目录
*
* @param storageIds 存储 ID 列表
* @return 文件数量
* @param req 请求参数
* @return ID
*/
Long countByStorageIds(List<Long> storageIds);
Long createDir(FileReq req);
/**
* 查询文件资源统计信息
@@ -97,23 +97,31 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
FileResp check(String fileHash);
/**
* 创建目录
* 计算文件夹大小
*
* @param req 请求参数
* @return ID
* @param id ID
* @return 文件夹大小(字节)
*/
Long createDir(FileReq req);
Long calcDirSize(Long id);
/**
* 获取默认文件路径
* 根据存储 ID 列表查询
*
* @param storageIds 存储 ID 列表
* @return 文件数量
*/
Long countByStorageIds(List<Long> storageIds);
/**
* 获取默认上级目录
*
* <p>
* 默认文件路径yyyy/MM/dd/
* 默认上级目录yyyy/MM/dd/
* </p>
*
* @return 默认文件路径
* @return 默认上级目录
*/
default String getDefaultFilePath() {
default String getDefaultParentPath() {
LocalDate today = LocalDate.now();
return today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today
.getDayOfMonth() + StringConstants.SLASH;

View File

@@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
@@ -45,10 +44,10 @@ import top.continew.admin.system.service.StorageService;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.StrUtils;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -80,11 +79,9 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
fileStorageService.delete(fileInfo);
} else {
// 不允许删除非空文件夹
String separator = StringConstants.SLASH.equals(file.getPath())
? StringConstants.EMPTY
: StringConstants.SLASH;
boolean exists = baseMapper.lambdaQuery()
.eq(FileDO::getPath, file.getPath() + separator + file.getName())
.eq(FileDO::getParentPath, file.getPath())
.eq(FileDO::getStorageId, entry.getKey())
.exists();
CheckUtils.throwIf(exists, "文件夹 [{}] 不为空,请先删除文件夹下的内容", file.getName());
}
@@ -93,7 +90,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
}
@Override
public FileInfo upload(MultipartFile file, String path, String storageCode) throws IOException {
public FileInfo upload(MultipartFile file, String parentPath, String storageCode) throws IOException {
// 校验文件格式
String extName = FileNameUtil.extName(file.getOriginalFilename());
List<String> allExtensions = FileTypeEnum.getAllExtensions();
@@ -106,7 +103,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
.setPlatform(storage.getCode())
.setHashCalculatorSha256(true)
.putAttr(ClassUtil.getClassName(StorageDO.class, false), storage)
.setPath(this.pretreatmentPath(path));
.setPath(this.pretreatmentPath(parentPath));
// 图片文件生成缩略图
if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) {
uploadPretreatment.setIgnoreThumbnailException(true, true);
@@ -129,17 +126,40 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
}
});
// 创建父级目录
this.createDir(path, storage);
this.createParentDir(parentPath, storage);
// 上传
return uploadPretreatment.upload();
}
@Override
public Long countByStorageIds(List<Long> storageIds) {
if (CollUtil.isEmpty(storageIds)) {
return 0L;
public Long createDir(FileReq req) {
String parentPath = req.getParentPath();
FileDO file = baseMapper.lambdaQuery()
.eq(FileDO::getParentPath, parentPath)
.eq(FileDO::getName, req.getOriginalName())
.eq(FileDO::getType, FileTypeEnum.DIR)
.one();
CheckUtils.throwIfNotNull(file, "文件夹已存在");
// 存储引擎需要一致
StorageDO storage = storageService.getDefaultStorage();
if (!StringConstants.SLASH.equals(parentPath)) {
FileDO parentFile = baseMapper.lambdaQuery()
.eq(FileDO::getPath, parentPath)
.eq(FileDO::getType, FileTypeEnum.DIR)
.one();
CheckUtils.throwIfNull(parentFile, "父级文件夹不存在");
CheckUtils.throwIfNotEqual(parentFile.getStorageId(), storage.getId(), "文件夹和父级文件夹存储引擎不一致");
}
return baseMapper.lambdaQuery().in(FileDO::getStorageId, storageIds).count();
// 创建文件夹
FileDO dirFile = new FileDO();
String originalName = req.getOriginalName();
dirFile.setName(originalName);
dirFile.setOriginalName(originalName);
dirFile.setParentPath(parentPath);
dirFile.setType(FileTypeEnum.DIR);
dirFile.setStorageId(storage.getId());
baseMapper.insert(dirFile);
return dirFile.getId();
}
@Override
@@ -165,17 +185,31 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
}
@Override
public Long createDir(FileReq req) {
StorageDO storage = storageService.getDefaultStorage();
FileDO file = new FileDO();
file.setName(req.getOriginalName());
file.setOriginalName(req.getOriginalName());
file.setSize(0L);
file.setPath(req.getPath());
file.setType(FileTypeEnum.DIR);
file.setStorageId(storage.getId());
baseMapper.insert(file);
return file.getId();
public Long calcDirSize(Long id) {
FileDO dirFile = super.getById(id);
ValidationUtils.throwIfNotEqual(dirFile.getType(), FileTypeEnum.DIR, "ID 为 [{}] 的不是文件夹,不支持计算大小", id);
// 查询当前文件夹下的所有子文件和子文件夹
List<FileDO> children = baseMapper.lambdaQuery().eq(FileDO::getParentPath, dirFile.getPath()).list();
if (CollUtil.isEmpty(children)) {
return 0L;
}
// 累加子文件大小和递归计算子文件夹大小
return children.stream().mapToLong(child -> {
if (FileTypeEnum.DIR.equals(child.getType())) {
// 递归计算子文件夹大小
return calcDirSize(child.getId());
} else {
return child.getSize();
}
}).sum();
}
@Override
public Long countByStorageIds(List<Long> storageIds) {
if (CollUtil.isEmpty(storageIds)) {
return 0L;
}
return baseMapper.lambdaQuery().in(FileDO::getStorageId, storageIds).count();
}
@Override
@@ -183,12 +217,14 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
super.fill(obj);
if (obj instanceof FileResp fileResp) {
StorageDO storage = storageService.getById(fileResp.getStorageId());
String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint());
String path = fileResp.getPath();
String url = URLUtil.normalize(prefix + path + StringConstants.SLASH + fileResp.getName(), false, true);
String prefix = storage.getUrlPrefix();
String url = URLUtil.normalize(prefix + fileResp.getPath(), false, true);
fileResp.setUrl(url);
String parentPath = StringConstants.SLASH.equals(fileResp.getParentPath())
? StringConstants.EMPTY
: fileResp.getParentPath();
String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailName(), url, thName -> URLUtil
.normalize(prefix + path + StringConstants.SLASH + thName, false, true));
.normalize(prefix + parentPath + StringConstants.SLASH + thName, false, true));
fileResp.setThumbnailUrl(thumbnailUrl);
fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode()));
}
@@ -198,7 +234,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
* 处理路径
*
* <p>
* 1.如果 path 为空,则使用 {@link FileService#getDefaultFilePath()} 作为默认值 <br />
* 1.如果 path 为空,则使用 {@link FileService#getDefaultParentPath()} 作为默认值 <br />
* 2.如果 path 为 {@code /},则设置为空 <br />
* 3.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br />
* 4.如果 path 以 {@code /} 开头,则移除前缀 {@code /} <br />
@@ -210,7 +246,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
*/
private String pretreatmentPath(String path) {
if (StrUtil.isBlank(path)) {
return this.getDefaultFilePath();
return this.getDefaultParentPath();
}
if (StringConstants.SLASH.equals(path)) {
return StringConstants.EMPTY;
@@ -219,47 +255,48 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
}
/**
* 创建文件夹(支持多级)
* 创建上级文件夹(支持多级)
*
* <p>
* user/avatar/ => userpath/、avatarpath/user
* user/avatar/ => userpath/user、avatarpath/user/avatar
* </p>
*
* @param dirPath 路径
* @param parentPath 上级目录
* @param storage 存储配置
*/
private void createDir(String dirPath, StorageDO storage) {
if (StrUtil.isBlank(dirPath) || StringConstants.SLASH.equals(dirPath)) {
private void createParentDir(String parentPath, StorageDO storage) {
if (StrUtil.isBlank(parentPath) || StringConstants.SLASH.equals(parentPath)) {
return;
}
// user/avatar/ => user/、avatarpath/user
String[] paths = StrUtil.split(dirPath, StringConstants.SLASH, false, true).toArray(String[]::new);
Map<String, String> pathMap = MapUtil.newHashMap(paths.length, true);
for (int i = 0; i < paths.length; i++) {
String key = paths[i];
String path = (i == 0)
? StringConstants.SLASH
: StringConstants.SLASH + String.join(StringConstants.SLASH, Arrays.copyOfRange(paths, 0, i));
pathMap.put(key, path);
// user/avatar/ => user、avatar
String[] parentPathParts = StrUtil.split(parentPath, StringConstants.SLASH, false, true).toArray(String[]::new);
String lastPath = StringConstants.SLASH;
StringBuilder currentPathBuilder = new StringBuilder();
for (int i = 0; i < parentPathParts.length; i++) {
String parentPathPart = parentPathParts[i];
if (i > 0) {
lastPath = currentPathBuilder.toString();
}
// 创建文件夹
for (Map.Entry<String, String> entry : pathMap.entrySet()) {
String key = entry.getKey();
String path = entry.getValue();
if (!baseMapper.lambdaQuery()
.eq(FileDO::getPath, path)
.eq(FileDO::getName, key)
// /user、/user/avatar
currentPathBuilder.append(StringConstants.SLASH).append(parentPathPart);
String currentPath = currentPathBuilder.toString();
// 文件夹和文件存储引擎需要一致
FileDO dirFile = baseMapper.lambdaQuery()
.eq(FileDO::getPath, currentPath)
.eq(FileDO::getType, FileTypeEnum.DIR)
.exists()) {
.one();
if (dirFile != null) {
CheckUtils.throwIfNotEqual(dirFile.getStorageId(), storage.getId(), "文件夹和上传文件存储引擎不一致");
continue;
}
FileDO file = new FileDO();
file.setName(key);
file.setOriginalName(key);
file.setSize(0L);
file.setPath(path);
file.setName(parentPathPart);
file.setOriginalName(parentPathPart);
file.setPath(currentPath);
file.setParentPath(lastPath);
file.setType(FileTypeEnum.DIR);
file.setStorageId(storage.getId());
baseMapper.insert(file);
}
}
}
}

View File

@@ -16,6 +16,7 @@
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;
@@ -78,13 +79,9 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code);
// 需要独立操作来指定默认存储
req.setIsDefault(false);
}
@Override
public void afterCreate(StorageReq req, StorageDO entity) {
// 加载存储引擎
if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) {
this.load(entity);
if (DisEnableStatusEnum.ENABLE.equals(req.getStatus())) {
this.load(BeanUtil.copyProperties(req, StorageDO.class));
}
}
@@ -107,19 +104,16 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
storageType.pretreatment(req);
// 卸载存储引擎
this.unload(oldStorage);
}
@Override
public void afterUpdate(StorageReq req, StorageDO entity) {
// 加载存储引擎
if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) {
this.load(entity);
if (DisEnableStatusEnum.ENABLE.equals(newStatus)) {
BeanUtil.copyProperties(req, oldStorage);
this.load(oldStorage);
}
}
@Override
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();
storageList.forEach(storage -> {
CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许删除", storage.getName());
@@ -204,7 +198,7 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
config.setSecretKey(storage.getSecretKey());
config.setEndPoint(storage.getEndpoint());
config.setBucketName(storage.getBucketName());
config.setDomain(storage.getDomain());
config.setDomain(StrUtil.emptyIfNull(storage.getDomain()));
fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
.singletonList(config), null));
}

View File

@@ -67,9 +67,10 @@ public class CommonController {
@Operation(summary = "上传文件", description = "上传文件")
@PostMapping("/file")
public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file, String path) throws IOException {
public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file,
String parentPath) throws IOException {
ValidationUtils.throwIf(file::isEmpty, "文件不能为空");
FileInfo fileInfo = fileService.upload(file, path);
FileInfo fileInfo = fileService.upload(file, parentPath);
return FileUploadResp.builder()
.id(fileInfo.getId())
.url(fileInfo.getUrl())

View File

@@ -19,19 +19,27 @@ package top.continew.admin.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.common.controller.BaseController;
import top.continew.admin.system.model.query.FileQuery;
import top.continew.admin.system.model.req.FileReq;
import top.continew.admin.system.model.resp.file.FileDirCalcSizeResp;
import top.continew.admin.system.model.resp.file.FileResp;
import top.continew.admin.system.model.resp.file.FileStatisticsResp;
import top.continew.admin.system.model.resp.file.FileUploadResp;
import top.continew.admin.system.service.FileService;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import top.continew.starter.extension.crud.model.resp.IdResp;
import top.continew.starter.log.annotation.Log;
import java.io.IOException;
/**
* 文件管理 API
*
@@ -44,6 +52,46 @@ import top.continew.starter.log.annotation.Log;
@CrudRequestMapping(value = "/system/file", api = {Api.PAGE, Api.UPDATE, Api.DELETE})
public class FileController extends BaseController<FileService, FileResp, FileResp, FileQuery, FileReq> {
/**
* 上传文件
* <p>
* 公共上传文件请使用 {@link top.continew.admin.controller.common.CommonController#upload}
* </p>
*
* @param file 文件
* @param parentPath 上级目录
* @return 文件上传响应参数
* @throws IOException /
*/
@SaCheckPermission("system:file:upload")
@Operation(summary = "上传文件", description = "上传文件")
@PostMapping("/upload")
public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file,
String parentPath) throws IOException {
ValidationUtils.throwIf(file::isEmpty, "文件不能为空");
FileInfo fileInfo = baseService.upload(file, parentPath);
return FileUploadResp.builder()
.id(fileInfo.getId())
.url(fileInfo.getUrl())
.thUrl(fileInfo.getThUrl())
.metadata(fileInfo.getMetadata())
.build();
}
@Operation(summary = "创建文件夹", description = "创建文件夹")
@SaCheckPermission("system:file:createDir")
@PostMapping("/dir")
public IdResp<Long> createDir(@RequestBody FileReq req) {
return new IdResp<>(baseService.createDir(req));
}
@Operation(summary = "计算文件夹大小", description = "计算文件夹大小")
@SaCheckPermission("system:file:calcDirSize")
@GetMapping("/dir/{id}/size")
public FileDirCalcSizeResp calcDirSize(@PathVariable Long id) {
return new FileDirCalcSizeResp(baseService.calcDirSize(id));
}
@Log(ignore = true)
@Operation(summary = "查询文件资源统计", description = "查询文件资源统计")
@SaCheckPermission("system:file:list")
@@ -59,10 +107,4 @@ public class FileController extends BaseController<FileService, FileResp, FileRe
public FileResp checkFile(String fileHash) {
return baseService.check(fileHash);
}
@Operation(summary = "创建文件夹", description = "创建文件夹")
@PostMapping("/dir")
public IdResp<Long> createDir(@RequestBody FileReq req) {
return new IdResp<>(baseService.createDir(req));
}
}

View File

@@ -58,6 +58,8 @@ VALUES
(1114, '修改', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:update', 4, 1, 1, NOW()),
(1115, '删除', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:delete', 5, 1, 1, NOW()),
(1116, '下载', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()),
(1117, '创建文件夹', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:createDir', 7, 1, 1, NOW()),
(1118, '计算文件夹大小', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:calcDirSize', 8, 1, 1, NOW()),
(1130, '字典管理', 1000, 2, '/system/dict', 'SystemDict', 'system/dict/index', NULL, 'bookmark', b'0', b'0', b'0', NULL, 7, 1, 1, NOW()),
(1131, '列表', 1130, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:dict:list', 1, 1, 1, NOW()),

View File

@@ -280,8 +280,9 @@ CREATE TABLE IF NOT EXISTS `sys_file` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '名称',
`original_name` varchar(255) NOT NULL COMMENT '原始名称',
`size` bigint(20) NOT NULL COMMENT '大小(字节)',
`path` varchar(512) NOT NULL COMMENT '存储路径',
`size` bigint(20) DEFAULT NULL COMMENT '大小(字节)',
`parent_path` varchar(512) NOT NULL DEFAULT '/' COMMENT '上级目录',
`path` varchar(512) NOT NULL COMMENT '路径',
`extension` varchar(32) DEFAULT NULL COMMENT '扩展名',
`content_type` varchar(255) DEFAULT NULL COMMENT '内容类型',
`type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型0: 目录1其他2图片3文档4视频5音频',

View File

@@ -58,6 +58,8 @@ VALUES
(1114, '修改', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:update', 4, 1, 1, NOW()),
(1115, '删除', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:delete', 5, 1, 1, NOW()),
(1116, '下载', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()),
(1117, '创建文件夹', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:createDir', 7, 1, 1, NOW()),
(1118, '计算文件夹大小', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:calcDirSize', 8, 1, 1, NOW()),
(1130, '字典管理', 1000, 2, '/system/dict', 'SystemDict', 'system/dict/index', NULL, 'bookmark', false, false, false, NULL, 7, 1, 1, NOW()),
(1131, '列表', 1130, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:dict:list', 1, 1, 1, NOW()),

View File

@@ -466,7 +466,8 @@ CREATE TABLE IF NOT EXISTS "sys_file" (
"id" int8 NOT NULL,
"name" varchar(255) NOT NULL,
"original_name" varchar(255) NOT NULL,
"size" int8 NOT NULL,
"size" int8 DEFAULT NULL,
"parent_path" varchar(512) NOT NULL DEFAULT '/',
"path" varchar(512) NOT NULL,
"extension" varchar(100) DEFAULT NULL,
"content_type" varchar(255) DEFAULT NULL,
@@ -491,7 +492,8 @@ COMMENT ON COLUMN "sys_file"."id" IS 'ID';
COMMENT ON COLUMN "sys_file"."name" IS '名称';
COMMENT ON COLUMN "sys_file"."original_name" IS '原始名称';
COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)';
COMMENT ON COLUMN "sys_file"."path" IS '存储路径';
COMMENT ON COLUMN "sys_file"."parent_path" IS '上级目录';
COMMENT ON COLUMN "sys_file"."path" IS '路径';
COMMENT ON COLUMN "sys_file"."extension" IS '扩展名';
COMMENT ON COLUMN "sys_file"."content_type" IS '内容类型';
COMMENT ON COLUMN "sys_file"."type" IS '类型0: 目录1其他2图片3文档4视频5音频';