mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-11 06:57:12 +08:00
refactor(system/file): 重构文件管理表结构,新增计算文件大小接口
This commit is contained in:
@@ -29,11 +29,6 @@ public class ContainerConstants {
|
||||
*/
|
||||
public static final String USER_NICKNAME = "UserNickname";
|
||||
|
||||
/**
|
||||
* 文件信息
|
||||
*/
|
||||
public static final String FILE_INFO = "FileInfo";
|
||||
|
||||
/**
|
||||
* 用户角色 ID 列表
|
||||
*/
|
||||
|
@@ -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()));
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 不是
|
||||
* IP:http(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();
|
||||
// IP(MinIO) 则拼接 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);
|
||||
}
|
||||
}
|
@@ -46,10 +46,10 @@ public class FileQuery implements Serializable {
|
||||
private String originalName;
|
||||
|
||||
/**
|
||||
* 存储路径
|
||||
* 上级目录
|
||||
*/
|
||||
@Schema(description = "存储路径", example = "/")
|
||||
private String path;
|
||||
@Schema(description = "上级目录", example = "/")
|
||||
private String parentPath;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
|
@@ -46,8 +46,8 @@ public class FileReq implements Serializable {
|
||||
private String originalName;
|
||||
|
||||
/**
|
||||
* 存储路径
|
||||
* 上级目录
|
||||
*/
|
||||
@Schema(description = "存储路径", example = "/")
|
||||
private String path;
|
||||
@Schema(description = "上级目录", example = "/")
|
||||
private String parentPath;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
|
@@ -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/ => user(path:/)、avatar(path:/user)
|
||||
* user/avatar/ => user(path:/user)、avatar(path:/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:/、avatar:path:/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
@@ -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())
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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()),
|
||||
|
@@ -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:音频)',
|
||||
|
@@ -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()),
|
||||
|
@@ -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:音频)';
|
||||
|
Reference in New Issue
Block a user