mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +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 USER_NICKNAME = "UserNickname"; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 文件信息 |  | ||||||
|      */ |  | ||||||
|     public static final String FILE_INFO = "FileInfo"; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 用户角色 ID 列表 |      * 用户角色 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())); |         fileInfo.setId(String.valueOf(file.getId())); | ||||||
|         if (!URLUtils.isHttpUrl(fileInfo.getUrl())) { |         if (!URLUtils.isHttpUrl(fileInfo.getUrl())) { | ||||||
|             String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint()); |             String prefix = storage.getUrlPrefix(); | ||||||
|             String url = URLUtil.completeUrl(prefix, fileInfo.getUrl()); |             String url = URLUtil.normalize(prefix + fileInfo.getUrl(), false, true); | ||||||
|             fileInfo.setUrl(url); |             fileInfo.setUrl(url); | ||||||
|             if (StrUtil.isNotBlank(fileInfo.getThUrl())) { |             if (StrUtil.isNotBlank(fileInfo.getThUrl())) { | ||||||
|                 fileInfo.setThUrl(URLUtil.completeUrl(prefix, fileInfo.getThUrl())); |                 fileInfo.setThUrl(URLUtil.normalize(prefix + fileInfo.getThUrl(), false, true)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return true; |         return true; | ||||||
| @@ -117,8 +117,7 @@ public class FileRecorderImpl implements FileRecorder { | |||||||
|             .eq(FileDO::getName, StrUtil.subAfter(url, StringConstants.SLASH, true)); |             .eq(FileDO::getName, StrUtil.subAfter(url, StringConstants.SLASH, true)); | ||||||
|         // 非 HTTP URL 场景 |         // 非 HTTP URL 场景 | ||||||
|         if (!URLUtils.isHttpUrl(url)) { |         if (!URLUtils.isHttpUrl(url)) { | ||||||
|             return queryWrapper.eq(FileDO::getPath, StrUtil.prependIfMissing(StrUtil |             return queryWrapper.eq(FileDO::getPath, StrUtil.prependIfMissing(url, StringConstants.SLASH)).one(); | ||||||
|                 .subBefore(url, StringConstants.SLASH, true), StringConstants.SLASH)).one(); |  | ||||||
|         } |         } | ||||||
|         // HTTP URL 场景 |         // HTTP URL 场景 | ||||||
|         List<FileDO> list = queryWrapper.list(); |         List<FileDO> list = queryWrapper.list(); | ||||||
| @@ -137,8 +136,7 @@ public class FileRecorderImpl implements FileRecorder { | |||||||
|             String urlPrefix = StrUtil.subBefore(url, StringConstants.SLASH, true); |             String urlPrefix = StrUtil.subBefore(url, StringConstants.SLASH, true); | ||||||
|             // http://localhost:8000/file/ + /user/avatar => http://localhost:8000/file/user/avatar |             // http://localhost:8000/file/ + /user/avatar => http://localhost:8000/file/user/avatar | ||||||
|             StorageDO storage = storageMap.get(file.getStorageId()); |             StorageDO storage = storageMap.get(file.getStorageId()); | ||||||
|             String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint()); |             return urlPrefix.equals(URLUtil.normalize(storage.getUrlPrefix() + file.getParentPath(), false, true)); | ||||||
|             return urlPrefix.equals(URLUtil.normalize(prefix + file.getPath(), false, true)); |  | ||||||
|         }).findFirst().orElse(null); |         }).findFirst().orElse(null); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -83,7 +83,7 @@ public enum StorageTypeEnum implements BaseEnum<Integer> { | |||||||
|      * @param req 请求参数 |      * @param req 请求参数 | ||||||
|      */ |      */ | ||||||
|     public void pretreatment(StorageReq req) { |     public void pretreatment(StorageReq req) { | ||||||
|         // 域名需要以 “/” 结尾 |         // 域名需要以 “/” 结尾(x-file-storage 在拼接路径时都是直接 + 拼接,所以规范要求每一级都要以 “/” 结尾,且后面路径不能以 “/” 开头) | ||||||
|         if (StrUtil.isNotBlank(req.getDomain())) { |         if (StrUtil.isNotBlank(req.getDomain())) { | ||||||
|             req.setDomain(StrUtil.appendIfMissing(req.getDomain(), StringConstants.SLASH)); |             req.setDomain(StrUtil.appendIfMissing(req.getDomain(), StringConstants.SLASH)); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -36,6 +36,6 @@ public interface FileMapper extends BaseMapper<FileDO> { | |||||||
|      * |      * | ||||||
|      * @return 文件资源统计信息 |      * @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(); |     List<FileStatisticsResp> statistics(); | ||||||
| } | } | ||||||
| @@ -60,7 +60,12 @@ public class FileDO extends BaseDO { | |||||||
|     private Long size; |     private Long size; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 存储路径 |      * 上级目录 | ||||||
|  |      */ | ||||||
|  |     private String parentPath; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 路径 | ||||||
|      */ |      */ | ||||||
|     private String path; |     private String path; | ||||||
|  |  | ||||||
| @@ -119,10 +124,11 @@ public class FileDO extends BaseDO { | |||||||
|         this.originalName = fileInfo.getOriginalFilename(); |         this.originalName = fileInfo.getOriginalFilename(); | ||||||
|         this.size = fileInfo.getSize(); |         this.size = fileInfo.getSize(); | ||||||
|         // 如果为空,则为 /;如果不为空,则调整格式为:/xxx |         // 如果为空,则为 /;如果不为空,则调整格式为:/xxx | ||||||
|         this.path = StrUtil.isEmpty(fileInfo.getPath()) |         this.parentPath = StrUtil.isEmpty(fileInfo.getPath()) | ||||||
|             ? StringConstants.SLASH |             ? StringConstants.SLASH | ||||||
|             : StrUtil.removeSuffix(StrUtil.prependIfMissing(fileInfo |             : StrUtil.removeSuffix(StrUtil.prependIfMissing(fileInfo | ||||||
|                 .getPath(), StringConstants.SLASH), StringConstants.SLASH); |                 .getPath(), StringConstants.SLASH), StringConstants.SLASH); | ||||||
|  |         this.path = StrUtil.prependIfMissing(fileInfo.getUrl(), StringConstants.SLASH); | ||||||
|         this.extension = fileInfo.getExt(); |         this.extension = fileInfo.getExt(); | ||||||
|         this.contentType = fileInfo.getContentType(); |         this.contentType = fileInfo.getContentType(); | ||||||
|         this.type = FileTypeEnum.getByExtension(this.extension); |         this.type = FileTypeEnum.getByExtension(this.extension); | ||||||
| @@ -148,15 +154,16 @@ public class FileDO extends BaseDO { | |||||||
|         // 暂不使用,所以保持空 |         // 暂不使用,所以保持空 | ||||||
|         fileInfo.setBasePath(StringConstants.EMPTY); |         fileInfo.setBasePath(StringConstants.EMPTY); | ||||||
|         fileInfo.setSize(this.size); |         fileInfo.setSize(this.size); | ||||||
|         fileInfo.setPath(StringConstants.SLASH.equals(this.path) |         fileInfo.setPath(StringConstants.SLASH.equals(this.parentPath) | ||||||
|             ? StringConstants.EMPTY |             ? 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.setExt(this.extension); | ||||||
|         fileInfo.setContentType(this.contentType); |         fileInfo.setContentType(this.contentType); | ||||||
|         if (StrUtil.isNotBlank(this.metadata)) { |         if (StrUtil.isNotBlank(this.metadata)) { | ||||||
|             fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class)); |             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.setThFilename(this.thumbnailName); | ||||||
|         fileInfo.setThSize(this.thumbnailSize); |         fileInfo.setThSize(this.thumbnailSize); | ||||||
| @@ -166,4 +173,11 @@ public class FileDO extends BaseDO { | |||||||
|         } |         } | ||||||
|         return fileInfo; |         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; | 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 com.baomidou.mybatisplus.annotation.TableName; | ||||||
| import lombok.Data; | import lombok.Data; | ||||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | 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.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 top.continew.starter.security.crypto.annotation.FieldEncrypt; | ||||||
|  |  | ||||||
| import java.io.Serial; | import java.io.Serial; | ||||||
|  | import java.net.URL; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 存储实体 |  * 存储实体 | ||||||
| @@ -99,4 +105,28 @@ public class StorageDO extends BaseDO { | |||||||
|      * 状态 |      * 状态 | ||||||
|      */ |      */ | ||||||
|     private DisEnableStatusEnum status; |     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; |     private String originalName; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 存储路径 |      * 上级目录 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "存储路径", example = "/") |     @Schema(description = "上级目录", example = "/") | ||||||
|     private String path; |     private String parentPath; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 类型 |      * 类型 | ||||||
|   | |||||||
| @@ -46,8 +46,8 @@ public class FileReq implements Serializable { | |||||||
|     private String originalName; |     private String originalName; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 存储路径 |      * 上级目录 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "存储路径", example = "/") |     @Schema(description = "上级目录", example = "/") | ||||||
|     private String path; |     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; |     private String url; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 存储路径 |      * 上级目录 | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "上级目录", example = "/2025/2/25") |     @Schema(description = "上级目录", example = "/2025/2/25") | ||||||
|  |     private String parentPath; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 路径 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "路径", example = "/2025/2/25/6824afe8408da079832dcfb6.jpg") | ||||||
|     private String path; |     private String path; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -47,39 +47,39 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery, | |||||||
|      * @throws IOException / |      * @throws IOException / | ||||||
|      */ |      */ | ||||||
|     default FileInfo upload(MultipartFile file) throws IOException { |     default FileInfo upload(MultipartFile file) throws IOException { | ||||||
|         return upload(file, getDefaultFilePath(), null); |         return upload(file, getDefaultParentPath(), null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 上传到默认存储 |      * 上传到默认存储 | ||||||
|      * |      * | ||||||
|      * @param file 文件信息 |      * @param file       文件信息 | ||||||
|      * @param path 文件路径 |      * @param parentPath 上级目录 | ||||||
|      * @return 文件信息 |      * @return 文件信息 | ||||||
|      * @throws IOException / |      * @throws IOException / | ||||||
|      */ |      */ | ||||||
|     default FileInfo upload(MultipartFile file, String path) throws IOException { |     default FileInfo upload(MultipartFile file, String parentPath) throws IOException { | ||||||
|         return upload(file, path, null); |         return upload(file, parentPath, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 上传到指定存储 |      * 上传到指定存储 | ||||||
|      * |      * | ||||||
|      * @param file        文件信息 |      * @param file        文件信息 | ||||||
|      * @param path        文件路径 |      * @param parentPath  上级目录 | ||||||
|      * @param storageCode 存储编码 |      * @param storageCode 存储编码 | ||||||
|      * @return 文件信息 |      * @return 文件信息 | ||||||
|      * @throws IOException / |      * @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 列表 |      * @param req 请求参数 | ||||||
|      * @return 文件数量 |      * @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); |     FileResp check(String fileHash); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 创建目录 |      * 计算文件夹大小 | ||||||
|      * |      * | ||||||
|      * @param req 请求参数 |      * @param id ID | ||||||
|      * @return ID |      * @return 文件夹大小(字节) | ||||||
|      */ |      */ | ||||||
|     Long createDir(FileReq req); |     Long calcDirSize(Long id); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 获取默认文件路径 |      * 根据存储 ID 列表查询 | ||||||
|  |      * | ||||||
|  |      * @param storageIds 存储 ID 列表 | ||||||
|  |      * @return 文件数量 | ||||||
|  |      */ | ||||||
|  |     Long countByStorageIds(List<Long> storageIds); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取默认上级目录 | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * 默认文件路径:yyyy/MM/dd/ |      * 默认上级目录:yyyy/MM/dd/ | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @return 默认文件路径 |      * @return 默认上级目录 | ||||||
|      */ |      */ | ||||||
|     default String getDefaultFilePath() { |     default String getDefaultParentPath() { | ||||||
|         LocalDate today = LocalDate.now(); |         LocalDate today = LocalDate.now(); | ||||||
|         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; | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ package top.continew.admin.system.service.impl; | |||||||
|  |  | ||||||
| import cn.hutool.core.collection.CollUtil; | import cn.hutool.core.collection.CollUtil; | ||||||
| import cn.hutool.core.io.file.FileNameUtil; | import cn.hutool.core.io.file.FileNameUtil; | ||||||
| import cn.hutool.core.map.MapUtil; |  | ||||||
| import cn.hutool.core.util.ClassUtil; | import cn.hutool.core.util.ClassUtil; | ||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
| import cn.hutool.core.util.URLUtil; | 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.constant.StringConstants; | ||||||
| import top.continew.starter.core.util.StrUtils; | import top.continew.starter.core.util.StrUtils; | ||||||
| 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.extension.crud.service.BaseServiceImpl; | import top.continew.starter.extension.crud.service.BaseServiceImpl; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| @@ -80,11 +79,9 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|                     fileStorageService.delete(fileInfo); |                     fileStorageService.delete(fileInfo); | ||||||
|                 } else { |                 } else { | ||||||
|                     // 不允许删除非空文件夹 |                     // 不允许删除非空文件夹 | ||||||
|                     String separator = StringConstants.SLASH.equals(file.getPath()) |  | ||||||
|                         ? StringConstants.EMPTY |  | ||||||
|                         : StringConstants.SLASH; |  | ||||||
|                     boolean exists = baseMapper.lambdaQuery() |                     boolean exists = baseMapper.lambdaQuery() | ||||||
|                         .eq(FileDO::getPath, file.getPath() + separator + file.getName()) |                         .eq(FileDO::getParentPath, file.getPath()) | ||||||
|  |                         .eq(FileDO::getStorageId, entry.getKey()) | ||||||
|                         .exists(); |                         .exists(); | ||||||
|                     CheckUtils.throwIf(exists, "文件夹 [{}] 不为空,请先删除文件夹下的内容", file.getName()); |                     CheckUtils.throwIf(exists, "文件夹 [{}] 不为空,请先删除文件夹下的内容", file.getName()); | ||||||
|                 } |                 } | ||||||
| @@ -93,7 +90,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @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()); |         String extName = FileNameUtil.extName(file.getOriginalFilename()); | ||||||
|         List<String> allExtensions = FileTypeEnum.getAllExtensions(); |         List<String> allExtensions = FileTypeEnum.getAllExtensions(); | ||||||
| @@ -106,7 +103,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|             .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(this.pretreatmentPath(path)); |             .setPath(this.pretreatmentPath(parentPath)); | ||||||
|         // 图片文件生成缩略图 |         // 图片文件生成缩略图 | ||||||
|         if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) { |         if (FileTypeEnum.IMAGE.getExtensions().contains(extName)) { | ||||||
|             uploadPretreatment.setIgnoreThumbnailException(true, true); |             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(); |         return uploadPretreatment.upload(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Long countByStorageIds(List<Long> storageIds) { |     public Long createDir(FileReq req) { | ||||||
|         if (CollUtil.isEmpty(storageIds)) { |         String parentPath = req.getParentPath(); | ||||||
|             return 0L; |         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 |     @Override | ||||||
| @@ -165,17 +185,31 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Long createDir(FileReq req) { |     public Long calcDirSize(Long id) { | ||||||
|         StorageDO storage = storageService.getDefaultStorage(); |         FileDO dirFile = super.getById(id); | ||||||
|         FileDO file = new FileDO(); |         ValidationUtils.throwIfNotEqual(dirFile.getType(), FileTypeEnum.DIR, "ID 为 [{}] 的不是文件夹,不支持计算大小", id); | ||||||
|         file.setName(req.getOriginalName()); |         // 查询当前文件夹下的所有子文件和子文件夹 | ||||||
|         file.setOriginalName(req.getOriginalName()); |         List<FileDO> children = baseMapper.lambdaQuery().eq(FileDO::getParentPath, dirFile.getPath()).list(); | ||||||
|         file.setSize(0L); |         if (CollUtil.isEmpty(children)) { | ||||||
|         file.setPath(req.getPath()); |             return 0L; | ||||||
|         file.setType(FileTypeEnum.DIR); |         } | ||||||
|         file.setStorageId(storage.getId()); |         // 累加子文件大小和递归计算子文件夹大小 | ||||||
|         baseMapper.insert(file); |         return children.stream().mapToLong(child -> { | ||||||
|         return file.getId(); |             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 |     @Override | ||||||
| @@ -183,12 +217,14 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|         super.fill(obj); |         super.fill(obj); | ||||||
|         if (obj instanceof FileResp fileResp) { |         if (obj instanceof FileResp fileResp) { | ||||||
|             StorageDO storage = storageService.getById(fileResp.getStorageId()); |             StorageDO storage = storageService.getById(fileResp.getStorageId()); | ||||||
|             String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint()); |             String prefix = storage.getUrlPrefix(); | ||||||
|             String path = fileResp.getPath(); |             String url = URLUtil.normalize(prefix + fileResp.getPath(), false, true); | ||||||
|             String url = URLUtil.normalize(prefix + path + StringConstants.SLASH + fileResp.getName(), false, true); |  | ||||||
|             fileResp.setUrl(url); |             fileResp.setUrl(url); | ||||||
|  |             String parentPath = StringConstants.SLASH.equals(fileResp.getParentPath()) | ||||||
|  |                 ? StringConstants.EMPTY | ||||||
|  |                 : fileResp.getParentPath(); | ||||||
|             String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailName(), url, thName -> URLUtil |             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.setThumbnailUrl(thumbnailUrl); | ||||||
|             fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); |             fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); | ||||||
|         } |         } | ||||||
| @@ -198,7 +234,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|      * 处理路径 |      * 处理路径 | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * 1.如果 path 为空,则使用 {@link FileService#getDefaultFilePath()} 作为默认值 <br /> |      * 1.如果 path 为空,则使用 {@link FileService#getDefaultParentPath()} 作为默认值 <br /> | ||||||
|      * 2.如果 path 为 {@code /},则设置为空 <br /> |      * 2.如果 path 为 {@code /},则设置为空 <br /> | ||||||
|      * 3.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br /> |      * 3.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br /> | ||||||
|      * 4.如果 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) { |     private String pretreatmentPath(String path) { | ||||||
|         if (StrUtil.isBlank(path)) { |         if (StrUtil.isBlank(path)) { | ||||||
|             return this.getDefaultFilePath(); |             return this.getDefaultParentPath(); | ||||||
|         } |         } | ||||||
|         if (StringConstants.SLASH.equals(path)) { |         if (StringConstants.SLASH.equals(path)) { | ||||||
|             return StringConstants.EMPTY; |             return StringConstants.EMPTY; | ||||||
| @@ -219,47 +255,48 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 创建文件夹(支持多级) |      * 创建上级文件夹(支持多级) | ||||||
|      * |      * | ||||||
|      * <p> |      * <p> | ||||||
|      * user/avatar/ => user(path:/)、avatar(path:/user) |      * user/avatar/ => user(path:/user)、avatar(path:/user/avatar) | ||||||
|      * </p> |      * </p> | ||||||
|      * |      * | ||||||
|      * @param dirPath 路径 |      * @param parentPath 上级目录 | ||||||
|      * @param storage 存储配置 |      * @param storage    存储配置 | ||||||
|      */ |      */ | ||||||
|     private void createDir(String dirPath, StorageDO storage) { |     private void createParentDir(String parentPath, StorageDO storage) { | ||||||
|         if (StrUtil.isBlank(dirPath) || StringConstants.SLASH.equals(dirPath)) { |         if (StrUtil.isBlank(parentPath) || StringConstants.SLASH.equals(parentPath)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         // user/avatar/ => user:/、avatar:path:/user |         // user/avatar/ => user、avatar | ||||||
|         String[] paths = StrUtil.split(dirPath, StringConstants.SLASH, false, true).toArray(String[]::new); |         String[] parentPathParts = StrUtil.split(parentPath, StringConstants.SLASH, false, true).toArray(String[]::new); | ||||||
|         Map<String, String> pathMap = MapUtil.newHashMap(paths.length, true); |         String lastPath = StringConstants.SLASH; | ||||||
|         for (int i = 0; i < paths.length; i++) { |         StringBuilder currentPathBuilder = new StringBuilder(); | ||||||
|             String key = paths[i]; |         for (int i = 0; i < parentPathParts.length; i++) { | ||||||
|             String path = (i == 0) |             String parentPathPart = parentPathParts[i]; | ||||||
|                 ? StringConstants.SLASH |             if (i > 0) { | ||||||
|                 : StringConstants.SLASH + String.join(StringConstants.SLASH, Arrays.copyOfRange(paths, 0, i)); |                 lastPath = currentPathBuilder.toString(); | ||||||
|             pathMap.put(key, path); |  | ||||||
|         } |  | ||||||
|         // 创建文件夹 |  | ||||||
|         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) |  | ||||||
|                 .eq(FileDO::getType, FileTypeEnum.DIR) |  | ||||||
|                 .exists()) { |  | ||||||
|                 FileDO file = new FileDO(); |  | ||||||
|                 file.setName(key); |  | ||||||
|                 file.setOriginalName(key); |  | ||||||
|                 file.setSize(0L); |  | ||||||
|                 file.setPath(path); |  | ||||||
|                 file.setType(FileTypeEnum.DIR); |  | ||||||
|                 file.setStorageId(storage.getId()); |  | ||||||
|                 baseMapper.insert(file); |  | ||||||
|             } |             } | ||||||
|  |             // /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) | ||||||
|  |                 .one(); | ||||||
|  |             if (dirFile != null) { | ||||||
|  |                 CheckUtils.throwIfNotEqual(dirFile.getStorageId(), storage.getId(), "文件夹和上传文件存储引擎不一致"); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             FileDO file = new FileDO(); | ||||||
|  |             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; | 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; | ||||||
| @@ -78,13 +79,9 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO | |||||||
|         CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); |         CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); | ||||||
|         // 需要独立操作来指定默认存储 |         // 需要独立操作来指定默认存储 | ||||||
|         req.setIsDefault(false); |         req.setIsDefault(false); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void afterCreate(StorageReq req, StorageDO entity) { |  | ||||||
|         // 加载存储引擎 |         // 加载存储引擎 | ||||||
|         if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) { |         if (DisEnableStatusEnum.ENABLE.equals(req.getStatus())) { | ||||||
|             this.load(entity); |             this.load(BeanUtil.copyProperties(req, StorageDO.class)); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -107,19 +104,16 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO | |||||||
|         storageType.pretreatment(req); |         storageType.pretreatment(req); | ||||||
|         // 卸载存储引擎 |         // 卸载存储引擎 | ||||||
|         this.unload(oldStorage); |         this.unload(oldStorage); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void afterUpdate(StorageReq req, StorageDO entity) { |  | ||||||
|         // 加载存储引擎 |         // 加载存储引擎 | ||||||
|         if (DisEnableStatusEnum.ENABLE.equals(entity.getStatus())) { |         if (DisEnableStatusEnum.ENABLE.equals(newStatus)) { | ||||||
|             this.load(entity); |             BeanUtil.copyProperties(req, oldStorage); | ||||||
|  |             this.load(oldStorage); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     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(storage -> { |         storageList.forEach(storage -> { | ||||||
|             CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许删除", storage.getName()); |             CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许删除", storage.getName()); | ||||||
| @@ -204,7 +198,7 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO | |||||||
|                 config.setSecretKey(storage.getSecretKey()); |                 config.setSecretKey(storage.getSecretKey()); | ||||||
|                 config.setEndPoint(storage.getEndpoint()); |                 config.setEndPoint(storage.getEndpoint()); | ||||||
|                 config.setBucketName(storage.getBucketName()); |                 config.setBucketName(storage.getBucketName()); | ||||||
|                 config.setDomain(storage.getDomain()); |                 config.setDomain(StrUtil.emptyIfNull(storage.getDomain())); | ||||||
|                 fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections |                 fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections | ||||||
|                     .singletonList(config), null)); |                     .singletonList(config), null)); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -67,9 +67,10 @@ public class CommonController { | |||||||
|  |  | ||||||
|     @Operation(summary = "上传文件", description = "上传文件") |     @Operation(summary = "上传文件", description = "上传文件") | ||||||
|     @PostMapping("/file") |     @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, "文件不能为空"); |         ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); | ||||||
|         FileInfo fileInfo = fileService.upload(file, path); |         FileInfo fileInfo = fileService.upload(file, parentPath); | ||||||
|         return FileUploadResp.builder() |         return FileUploadResp.builder() | ||||||
|             .id(fileInfo.getId()) |             .id(fileInfo.getId()) | ||||||
|             .url(fileInfo.getUrl()) |             .url(fileInfo.getUrl()) | ||||||
|   | |||||||
| @@ -19,19 +19,27 @@ package top.continew.admin.controller.system; | |||||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | import cn.dev33.satoken.annotation.SaCheckPermission; | ||||||
| import io.swagger.v3.oas.annotations.Operation; | import io.swagger.v3.oas.annotations.Operation; | ||||||
| import io.swagger.v3.oas.annotations.tags.Tag; | import io.swagger.v3.oas.annotations.tags.Tag; | ||||||
|  | import jakarta.validation.constraints.NotNull; | ||||||
| import lombok.RequiredArgsConstructor; | import lombok.RequiredArgsConstructor; | ||||||
|  | import org.dromara.x.file.storage.core.FileInfo; | ||||||
| import org.springframework.web.bind.annotation.*; | import org.springframework.web.bind.annotation.*; | ||||||
|  | import org.springframework.web.multipart.MultipartFile; | ||||||
| import top.continew.admin.common.controller.BaseController; | import top.continew.admin.common.controller.BaseController; | ||||||
| import top.continew.admin.system.model.query.FileQuery; | import top.continew.admin.system.model.query.FileQuery; | ||||||
| import top.continew.admin.system.model.req.FileReq; | 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.FileResp; | ||||||
| import top.continew.admin.system.model.resp.file.FileStatisticsResp; | 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.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.annotation.CrudRequestMapping; | ||||||
| import top.continew.starter.extension.crud.enums.Api; | import top.continew.starter.extension.crud.enums.Api; | ||||||
| import top.continew.starter.extension.crud.model.resp.IdResp; | import top.continew.starter.extension.crud.model.resp.IdResp; | ||||||
| import top.continew.starter.log.annotation.Log; | import top.continew.starter.log.annotation.Log; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 文件管理 API |  * 文件管理 API | ||||||
|  * |  * | ||||||
| @@ -44,6 +52,46 @@ import top.continew.starter.log.annotation.Log; | |||||||
| @CrudRequestMapping(value = "/system/file", api = {Api.PAGE, Api.UPDATE, Api.DELETE}) | @CrudRequestMapping(value = "/system/file", api = {Api.PAGE, Api.UPDATE, Api.DELETE}) | ||||||
| public class FileController extends BaseController<FileService, FileResp, FileResp, FileQuery, FileReq> { | 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) |     @Log(ignore = true) | ||||||
|     @Operation(summary = "查询文件资源统计", description = "查询文件资源统计") |     @Operation(summary = "查询文件资源统计", description = "查询文件资源统计") | ||||||
|     @SaCheckPermission("system:file:list") |     @SaCheckPermission("system:file:list") | ||||||
| @@ -59,10 +107,4 @@ public class FileController extends BaseController<FileService, FileResp, FileRe | |||||||
|     public FileResp checkFile(String fileHash) { |     public FileResp checkFile(String fileHash) { | ||||||
|         return baseService.check(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()), | (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()), | (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()), | (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()), | (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()), | (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', |     `id`                 bigint(20)    NOT NULL AUTO_INCREMENT     COMMENT 'ID', | ||||||
|     `name`               varchar(255)  NOT NULL                    COMMENT '名称', |     `name`               varchar(255)  NOT NULL                    COMMENT '名称', | ||||||
|     `original_name`      varchar(255)  NOT NULL                    COMMENT '原始名称', |     `original_name`      varchar(255)  NOT NULL                    COMMENT '原始名称', | ||||||
|     `size`               bigint(20)    NOT NULL                    COMMENT '大小(字节)', |     `size`               bigint(20)    DEFAULT NULL                COMMENT '大小(字节)', | ||||||
|     `path`               varchar(512)  NOT NULL                    COMMENT '存储路径', |     `parent_path`        varchar(512)  NOT NULL DEFAULT '/'        COMMENT '上级目录', | ||||||
|  |     `path`               varchar(512)  NOT NULL                    COMMENT '路径', | ||||||
|     `extension`          varchar(32)   DEFAULT NULL                COMMENT '扩展名', |     `extension`          varchar(32)   DEFAULT NULL                COMMENT '扩展名', | ||||||
|     `content_type`       varchar(255)  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:音频)', |     `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()), | (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()), | (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()), | (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()), | (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()), | (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, |     "id"                 int8         NOT NULL, | ||||||
|     "name"               varchar(255) NOT NULL, |     "name"               varchar(255) NOT NULL, | ||||||
|     "original_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, |     "path"               varchar(512) NOT NULL, | ||||||
|     "extension"          varchar(100) DEFAULT NULL, |     "extension"          varchar(100) DEFAULT NULL, | ||||||
|     "content_type"       varchar(255) 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"."name"               IS '名称'; | ||||||
| COMMENT ON COLUMN "sys_file"."original_name"      IS '原始名称'; | COMMENT ON COLUMN "sys_file"."original_name"      IS '原始名称'; | ||||||
| COMMENT ON COLUMN "sys_file"."size"               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"."extension"          IS '扩展名'; | ||||||
| COMMENT ON COLUMN "sys_file"."content_type"       IS '内容类型'; | COMMENT ON COLUMN "sys_file"."content_type"       IS '内容类型'; | ||||||
| COMMENT ON COLUMN "sys_file"."type"               IS '类型(0: 目录;1:其他;2:图片;3:文档;4:视频;5:音频)'; | COMMENT ON COLUMN "sys_file"."type"               IS '类型(0: 目录;1:其他;2:图片;3:文档;4:视频;5:音频)'; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user