refactor(system/file): 重构文件管理相关代码,完善文件夹场景

This commit is contained in:
2025-05-15 23:25:35 +08:00
parent bc057da265
commit 37027c774b
15 changed files with 222 additions and 157 deletions

View File

@@ -16,9 +16,11 @@
package top.continew.admin.system.config.file; package top.continew.admin.system.config.file;
import cn.hutool.core.collection.CollUtil;
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;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileInfo;
@@ -32,7 +34,9 @@ import top.continew.admin.system.model.entity.StorageDO;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.URLUtils; import top.continew.starter.core.util.URLUtils;
import java.util.Optional; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* 文件记录实现类 * 文件记录实现类
@@ -58,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.appendIfMissing(storage.getDomain(), StringConstants.SLASH); String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint());
String url = URLUtil.normalize(prefix + fileInfo.getUrl()); String url = URLUtil.completeUrl(prefix, fileInfo.getUrl());
fileInfo.setUrl(url); fileInfo.setUrl(url);
if (StrUtil.isNotBlank(fileInfo.getThUrl())) { if (StrUtil.isNotBlank(fileInfo.getThUrl())) {
fileInfo.setThUrl(URLUtil.normalize(prefix + fileInfo.getThUrl())); fileInfo.setThUrl(URLUtil.completeUrl(prefix, fileInfo.getThUrl()));
} }
} }
return true; return true;
@@ -81,7 +85,10 @@ public class FileRecorderImpl implements FileRecorder {
@Override @Override
public boolean delete(String url) { public boolean delete(String url) {
FileDO file = this.getFileByUrl(url); FileDO file = this.getFileByUrl(url);
return fileMapper.lambdaUpdate().eq(FileDO::getUrl, file.getUrl()).remove(); if (null == file) {
return false;
}
return fileMapper.lambdaUpdate().eq(FileDO::getId, file.getId()).remove();
} }
@Override @Override
@@ -106,10 +113,32 @@ public class FileRecorderImpl implements FileRecorder {
* @return 文件信息 * @return 文件信息
*/ */
private FileDO getFileByUrl(String url) { private FileDO getFileByUrl(String url) {
Optional<FileDO> fileOptional = fileMapper.lambdaQuery().eq(FileDO::getUrl, url).oneOpt(); LambdaQueryChainWrapper<FileDO> queryWrapper = fileMapper.lambdaQuery()
return fileOptional.orElseGet(() -> fileMapper.lambdaQuery() .eq(FileDO::getName, StrUtil.subAfter(url, StringConstants.SLASH, true));
.likeLeft(FileDO::getUrl, StrUtil.subAfter(url, StringConstants.SLASH, true)) // 非 HTTP URL 场景
.oneOpt() if (!URLUtils.isHttpUrl(url)) {
.orElse(null)); return queryWrapper.eq(FileDO::getPath, StrUtil.prependIfMissing(StrUtil
.subBefore(url, StringConstants.SLASH, true), StringConstants.SLASH)).one();
}
// HTTP URL 场景
List<FileDO> list = queryWrapper.list();
if (CollUtil.isEmpty(list)) {
return null;
}
if (list.size() == 1) {
return list.get(0);
}
// 结合存储配置进行匹配
List<StorageDO> storageList = storageMapper.selectByIds(list.stream().map(FileDO::getStorageId).toList());
Map<Long, StorageDO> storageMap = storageList.stream()
.collect(Collectors.toMap(StorageDO::getId, storage -> storage));
return list.stream().filter(file -> {
// http://localhost:8000/file/user/avatar/6825e687db4174e7a297a5f8.png => http://localhost:8000/file/user/avatar
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));
}).findFirst().orElse(null);
} }
} }

View File

@@ -16,7 +16,6 @@
package top.continew.admin.system.config.file; package top.continew.admin.system.config.file;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -25,8 +24,6 @@ import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.admin.system.model.entity.StorageDO; import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.query.StorageQuery;
import top.continew.admin.system.model.resp.StorageResp;
import top.continew.admin.system.service.StorageService; import top.continew.admin.system.service.StorageService;
import java.util.List; import java.util.List;
@@ -46,12 +43,10 @@ public class FileStorageConfigLoader implements ApplicationRunner {
@Override @Override
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) {
StorageQuery query = new StorageQuery(); List<StorageDO> list = storageService.lambdaQuery().eq(StorageDO::getStatus, DisEnableStatusEnum.ENABLE).list();
query.setStatus(DisEnableStatusEnum.ENABLE); if (CollUtil.isEmpty(list)) {
List<StorageResp> storageList = storageService.list(query, null);
if (CollUtil.isEmpty(storageList)) {
return; return;
} }
storageList.forEach(storage -> storageService.load(BeanUtil.copyProperties(storage, StorageDO.class))); list.forEach(storageService::load);
} }
} }

View File

@@ -43,14 +43,15 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
@Override @Override
public void validate(StorageReq req) { public void validate(StorageReq req) {
ValidationUtils.validate(req, ValidationGroup.Storage.Local.class); ValidationUtils.validate(req, ValidationGroup.Storage.Local.class);
ValidationUtils.throwIf(!URLUtils.isHttpUrl(req.getDomain()), "访问路径格式不正确"); ValidationUtils.throwIf(StrUtil.isNotBlank(req.getDomain()) && !URLUtils.isHttpUrl(req
.getDomain()), "访问路径格式不正确");
} }
@Override @Override
public void pretreatment(StorageReq req) { public void pretreatment(StorageReq req) {
super.pretreatment(req); super.pretreatment(req);
req.setBucketName(StrUtil.appendIfMissing(req.getBucketName() // 本地存储路径需要以 “/” 结尾
.replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH)); req.setBucketName(StrUtil.appendIfMissing(req.getBucketName(), StringConstants.SLASH));
} }
}, },
@@ -61,7 +62,8 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
@Override @Override
public void validate(StorageReq req) { public void validate(StorageReq req) {
ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class); ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class);
ValidationUtils.throwIf(!URLUtils.isHttpUrl(req.getDomain()), "域名格式不正确"); ValidationUtils.throwIf(StrUtil.isNotBlank(req.getDomain()) && !URLUtils.isHttpUrl(req
.getDomain()), "域名格式不正确");
} }
}; };
@@ -81,6 +83,9 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
* @param req 请求参数 * @param req 请求参数
*/ */
public void pretreatment(StorageReq req) { public void pretreatment(StorageReq req) {
req.setDomain(StrUtil.removeSuffix(req.getDomain(), StringConstants.SLASH)); // 域名需要以 “/” 结尾
if (StrUtil.isNotBlank(req.getDomain())) {
req.setDomain(StrUtil.appendIfMissing(req.getDomain(), StringConstants.SLASH));
}
} }
} }

View File

@@ -17,8 +17,6 @@
package top.continew.admin.system.model.entity; package top.continew.admin.system.model.entity;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@@ -28,7 +26,6 @@ import org.dromara.x.file.storage.core.FileInfo;
import top.continew.admin.common.model.entity.BaseDO; import top.continew.admin.common.model.entity.BaseDO;
import top.continew.admin.system.enums.FileTypeEnum; import top.continew.admin.system.enums.FileTypeEnum;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.StrUtils;
import java.io.Serial; import java.io.Serial;
import java.util.Map; import java.util.Map;
@@ -52,25 +49,20 @@ public class FileDO extends BaseDO {
*/ */
private String name; private String name;
/**
* 原始名称
*/
private String originalName;
/** /**
* 大小(字节) * 大小(字节)
*/ */
private Long size; private Long size;
/** /**
* URL * 存储路径
*/ */
private String url; private String path;
/**
* 上级目录
*/
private String parentPath;
/**
* 绝对路径
*/
private String absPath;
/** /**
* 扩展名 * 扩展名
@@ -88,7 +80,7 @@ public class FileDO extends BaseDO {
private FileTypeEnum type; private FileTypeEnum type;
/** /**
* SHA256值 * SHA256
*/ */
private String sha256; private String sha256;
@@ -97,16 +89,16 @@ public class FileDO extends BaseDO {
*/ */
private String metadata; private String metadata;
/**
* 缩略图名称
*/
private String thumbnailName;
/** /**
* 缩略图大小(字节) * 缩略图大小(字节)
*/ */
private Long thumbnailSize; private Long thumbnailSize;
/**
* 缩略图 URL
*/
private String thumbnailUrl;
/** /**
* 缩略图元数据 * 缩略图元数据
*/ */
@@ -123,21 +115,21 @@ public class FileDO extends BaseDO {
* @param fileInfo {@link FileInfo} 文件信息 * @param fileInfo {@link FileInfo} 文件信息
*/ */
public FileDO(FileInfo fileInfo) { public FileDO(FileInfo fileInfo) {
this.name = FileNameUtil.getPrefix(EscapeUtil.unescape(fileInfo.getOriginalFilename())); this.name = fileInfo.getFilename();
this.originalName = fileInfo.getOriginalFilename();
this.size = fileInfo.getSize(); this.size = fileInfo.getSize();
this.url = fileInfo.getUrl(); // 如果为空,则为 /;如果不为空,则调整格式为:/xxx
this.absPath = StrUtil.isEmpty(fileInfo.getPath()) this.path = StrUtil.isEmpty(fileInfo.getPath())
? StringConstants.SLASH ? StringConstants.SLASH
: StrUtil.prependIfMissing(fileInfo.getPath(), StringConstants.SLASH); : StrUtil.removeSuffix(StrUtil.prependIfMissing(fileInfo
String[] pathAttr = this.absPath.split(StringConstants.SLASH); .getPath(), StringConstants.SLASH), StringConstants.SLASH);
this.parentPath = pathAttr.length > 1 ? pathAttr[pathAttr.length - 1] : 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);
this.sha256 = fileInfo.getHashInfo().getSha256(); this.sha256 = fileInfo.getHashInfo().getSha256();
this.metadata = JSONUtil.toJsonStr(fileInfo.getMetadata()); this.metadata = JSONUtil.toJsonStr(fileInfo.getMetadata());
this.thumbnailName = fileInfo.getThFilename();
this.thumbnailSize = fileInfo.getThSize(); this.thumbnailSize = fileInfo.getThSize();
this.thumbnailUrl = fileInfo.getThUrl();
this.thumbnailMetadata = JSONUtil.toJsonStr(fileInfo.getThMetadata()); this.thumbnailMetadata = JSONUtil.toJsonStr(fileInfo.getThMetadata());
this.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime())); this.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime()));
} }
@@ -151,27 +143,24 @@ public class FileDO extends BaseDO {
public FileInfo toFileInfo(StorageDO storage) { public FileInfo toFileInfo(StorageDO storage) {
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
fileInfo.setPlatform(storage.getCode()); fileInfo.setPlatform(storage.getCode());
fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH) fileInfo.setFilename(this.name);
? StrUtil.subAfter(this.url, StringConstants.SLASH, true) fileInfo.setOriginalFilename(this.originalName);
: this.url); // 暂不使用,所以保持空
fileInfo.setOriginalFilename(StrUtils
.blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex));
fileInfo.setBasePath(StringConstants.EMPTY); fileInfo.setBasePath(StringConstants.EMPTY);
fileInfo.setSize(this.size); fileInfo.setSize(this.size);
fileInfo.setUrl(this.url); fileInfo.setPath(StringConstants.SLASH.equals(this.path)
fileInfo.setPath(StringConstants.SLASH.equals(this.absPath)
? StringConstants.EMPTY ? StringConstants.EMPTY
: StrUtil.removePrefix(this.absPath, StringConstants.SLASH)); : StrUtil.appendIfMissing(StrUtil.removePrefix(this.path, StringConstants.SLASH), StringConstants.SLASH));
fileInfo.setExt(this.extension); fileInfo.setExt(this.extension);
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.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH) fileInfo.setThFilename(this.thumbnailName);
? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true)
: this.thumbnailUrl);
fileInfo.setThSize(this.thumbnailSize); fileInfo.setThSize(this.thumbnailSize);
fileInfo.setThUrl(this.thumbnailUrl); fileInfo.setThUrl(fileInfo.getPath() + fileInfo.getThFilename());
if (StrUtil.isNotBlank(this.thumbnailMetadata)) { if (StrUtil.isNotBlank(this.thumbnailMetadata)) {
fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class)); fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class));
} }

View File

@@ -41,16 +41,15 @@ public class FileQuery implements Serializable {
/** /**
* 名称 * 名称
*/ */
@Schema(description = "名称", example = "图片") @Schema(description = "名称", example = "example")
@Query(type = QueryType.LIKE) @Query(type = QueryType.LIKE)
private String name; private String originalName;
/** /**
* 绝对路径 * 存储路径
*/ */
@Schema(description = "绝对路径", example = "/2025") @Schema(description = "存储路径", example = "/")
@Query(type = QueryType.EQ) private String path;
private String absPath;
/** /**
* 类型 * 类型

View File

@@ -40,14 +40,14 @@ public class FileReq implements Serializable {
/** /**
* 名称 * 名称
*/ */
@Schema(description = "名称", example = "test123") @Schema(description = "名称", example = "example")
@NotBlank(message = "文件名称不能为空") @NotBlank(message = "文件名称不能为空")
@Length(max = 255, message = "文件名称长度不能超过 {max} 个字符") @Length(max = 255, message = "文件名称长度不能超过 {max} 个字符")
private String name; private String originalName;
/** /**
* 上级目录 * 存储路径
*/ */
@Schema(description = "上级目录", example = "25") @Schema(description = "存储路径", example = "/")
private String parentPath; private String path;
} }

View File

@@ -41,9 +41,15 @@ public class FileResp extends BaseDetailResp {
/** /**
* 名称 * 名称
*/ */
@Schema(description = "名称", example = "example") @Schema(description = "名称", example = "6824afe8408da079832dcfb6.jpg")
private String name; private String name;
/**
* 原始名称
*/
@Schema(description = "原始名称", example = "example.jpg")
private String originalName;
/** /**
* 大小(字节) * 大小(字节)
*/ */
@@ -53,20 +59,14 @@ public class FileResp extends BaseDetailResp {
/** /**
* URL * URL
*/ */
@Schema(description = "URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example/example.jpg") @Schema(description = "URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/2025/2/25/6824afe8408da079832dcfb6.jpg")
private String url; private String url;
/** /**
* 上级目录 * 存储路径
*/ */
@Schema(description = "上级目录", example = "25") @Schema(description = "上级目录", example = "/2025/2/25")
private String parentPath; private String path;
/**
* 绝对路径
*/
@Schema(description = "绝对路径", example = "/2025/2/25")
private String absPath;
/** /**
* 扩展名 * 扩展名
@@ -89,7 +89,7 @@ public class FileResp extends BaseDetailResp {
/** /**
* SHA256 值 * SHA256 值
*/ */
@Schema(description = "SHA256值", example = "722f185c48bed892d6fa12e2b8bf1e5f8200d4a70f522fb62112b6caf13cb74e") @Schema(description = "SHA256 ", example = "722f185c48bed892d6fa12e2b8bf1e5f8200d4a70f522fb62112b6caf13cb74e")
private String sha256; private String sha256;
/** /**
@@ -98,24 +98,30 @@ public class FileResp extends BaseDetailResp {
@Schema(description = "元数据", example = "{width:1024,height:1024}") @Schema(description = "元数据", example = "{width:1024,height:1024}")
private String metadata; private String metadata;
/**
* 缩略图名称
*/
@Schema(description = "缩略图名称", example = "example.jpg.min.jpg")
private String thumbnailName;
/** /**
* 缩略图大小(字节) * 缩略图大小(字节)
*/ */
@Schema(description = "缩略图大小(字节)", example = "1024") @Schema(description = "缩略图大小(字节)", example = "1024")
private Long thumbnailSize; private Long thumbnailSize;
/**
* 缩略图 URL
*/
@Schema(description = "缩略图 URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example/example.jpg.min.jpg")
private String thumbnailUrl;
/** /**
* 缩略图元数据 * 缩略图元数据
*/ */
@Schema(description = "缩略图文件元数据", example = "{width:100,height:100}") @Schema(description = "缩略图文件元数据", example = "{width:100,height:100}")
private String thumbnailMetadata; private String thumbnailMetadata;
/**
* 缩略图 URL
*/
@Schema(description = "缩略图 URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/2025/2/25/example.jpg.min.jpg")
private String thumbnailUrl;
/** /**
* 存储 ID * 存储 ID
*/ */

View File

@@ -25,7 +25,6 @@ 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.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.data.mp.service.IService; import top.continew.starter.data.mp.service.IService;
import top.continew.starter.extension.crud.model.resp.IdResp;
import top.continew.starter.extension.crud.service.BaseService; import top.continew.starter.extension.crud.service.BaseService;
import java.io.IOException; import java.io.IOException;
@@ -103,7 +102,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
* @param req 请求参数 * @param req 请求参数
* @return ID * @return ID
*/ */
IdResp<Long> createDir(FileReq req); Long createDir(FileReq req);
/** /**
* 获取默认文件路径 * 获取默认文件路径

View File

@@ -18,6 +18,7 @@ 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;
@@ -43,12 +44,11 @@ import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService; 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.util.URLUtils;
import top.continew.starter.core.validation.CheckUtils; import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.extension.crud.model.resp.IdResp;
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;
@@ -75,8 +75,19 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) { for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) {
StorageDO storage = storageService.getById(entry.getKey()); StorageDO storage = storageService.getById(entry.getKey());
for (FileDO file : entry.getValue()) { for (FileDO file : entry.getValue()) {
FileInfo fileInfo = file.toFileInfo(storage); if (!FileTypeEnum.DIR.equals(file.getType())) {
fileStorageService.delete(fileInfo); FileInfo fileInfo = file.toFileInfo(storage);
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())
.exists();
CheckUtils.throwIf(exists, "文件夹 [{}] 不为空,请先删除文件夹下的内容", file.getName());
}
} }
} }
} }
@@ -117,6 +128,8 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
log.info("上传结束"); log.info("上传结束");
} }
}); });
// 创建父级目录
this.createDir(path, storage);
// 上传 // 上传
return uploadPretreatment.upload(); return uploadPretreatment.upload();
} }
@@ -152,40 +165,30 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
} }
@Override @Override
public IdResp<Long> createDir(FileReq req) { public Long createDir(FileReq req) {
StorageDO storage = storageService.getDefaultStorage(); StorageDO storage = storageService.getDefaultStorage();
FileDO fileDo = new FileDO(); FileDO file = new FileDO();
fileDo.setName(req.getName()); file.setName(req.getOriginalName());
fileDo.setSize(0L); file.setOriginalName(req.getOriginalName());
fileDo.setUrl(storage.getDomain() + req.getParentPath() + req.getName()); file.setSize(0L);
String absPath = req.getParentPath(); file.setPath(req.getPath());
String tempAbsPath = absPath.length() > 1 ? StrUtil.removeSuffix(absPath, StringConstants.SLASH) : absPath; file.setType(FileTypeEnum.DIR);
String[] pathArr = tempAbsPath.split(StringConstants.SLASH); file.setStorageId(storage.getId());
if (pathArr.length > 1) { baseMapper.insert(file);
fileDo.setParentPath(pathArr[pathArr.length - 1]); return file.getId();
} else {
fileDo.setParentPath(StringConstants.SLASH);
}
fileDo.setAbsPath(tempAbsPath);
fileDo.setExtension("dir");
fileDo.setContentType("");
fileDo.setType(FileTypeEnum.DIR);
fileDo.setSha256("");
fileDo.setStorageId(storage.getId());
baseMapper.insert(fileDo);
return new IdResp<>(fileDo.getId());
} }
@Override @Override
protected void fill(Object obj) { protected void fill(Object obj) {
super.fill(obj); super.fill(obj);
if (obj instanceof FileResp fileResp && !URLUtils.isHttpUrl(fileResp.getUrl())) { if (obj instanceof FileResp fileResp) {
StorageDO storage = storageService.getById(fileResp.getStorageId()); StorageDO storage = storageService.getById(fileResp.getStorageId());
String prefix = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH); String prefix = StrUtil.blankToDefault(storage.getDomain(), storage.getEndpoint());
String url = URLUtil.normalize(prefix + fileResp.getUrl()); String path = fileResp.getPath();
String url = URLUtil.normalize(prefix + path + StringConstants.SLASH + fileResp.getName(), false, true);
fileResp.setUrl(url); fileResp.setUrl(url);
String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailUrl(), url, thUrl -> URLUtil String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailName(), url, thName -> URLUtil
.normalize(prefix + thUrl)); .normalize(prefix + path + 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()));
} }
@@ -214,4 +217,49 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
} }
return StrUtil.appendIfMissing(StrUtil.removePrefix(path, StringConstants.SLASH), StringConstants.SLASH); return StrUtil.appendIfMissing(StrUtil.removePrefix(path, StringConstants.SLASH), StringConstants.SLASH);
} }
/**
* 创建文件夹(支持多级)
*
* <p>
* user/avatar/ => userpath/、avatarpath/user
* </p>
*
* @param dirPath 路径
* @param storage 存储配置
*/
private void createDir(String dirPath, StorageDO storage) {
if (StrUtil.isBlank(dirPath) || StringConstants.SLASH.equals(dirPath)) {
return;
}
// user/avatar/ => user/、avatarpath/user
String[] paths = StrUtil.split(dirPath, StringConstants.SLASH, false, true).toArray(String[]::new);
Map<String, String> pathMap = MapUtil.newHashMap(paths.length, true);
for (int i = 0; i < paths.length; i++) {
String key = paths[i];
String path = (i == 0)
? StringConstants.SLASH
: StringConstants.SLASH + String.join(StringConstants.SLASH, Arrays.copyOfRange(paths, 0, i));
pathMap.put(key, path);
}
// 创建文件夹
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);
}
}
}
} }

View File

@@ -179,7 +179,7 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
return this.getDefaultStorage(); return this.getDefaultStorage();
} }
StorageDO storage = baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one(); StorageDO storage = baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one();
CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", code); CheckUtils.throwIfNotExists(storage, "存储", "code", code);
return storage; return storage;
} }
@@ -193,8 +193,9 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
config.setStoragePath(storage.getBucketName()); config.setStoragePath(storage.getBucketName());
fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
.singletonList(config))); .singletonList(config)));
SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage // 注册资源映射
.getBucketName())); SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(StrUtil.removeSuffix(storage
.getDomain(), StringConstants.SLASH)).getPath(), storage.getBucketName()));
} }
case OSS -> { case OSS -> {
FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config(); FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();

View File

@@ -61,9 +61,8 @@ public class FileController extends BaseController<FileService, FileResp, FileRe
} }
@Operation(summary = "创建文件夹", description = "创建文件夹") @Operation(summary = "创建文件夹", description = "创建文件夹")
@ResponseBody @PostMapping("/dir")
@PostMapping("/createDir")
public IdResp<Long> createDir(@RequestBody FileReq req) { public IdResp<Long> createDir(@RequestBody FileReq req) {
return baseService.createDir(req); return new IdResp<>(baseService.createDir(req));
} }
} }

View File

@@ -273,8 +273,8 @@ INSERT INTO `sys_role_dept` (`role_id`, `dept_id`) VALUES (547888897925840927, 5
INSERT INTO `sys_storage` INSERT INTO `sys_storage`
(`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`) (`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`)
VALUES VALUES
(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', b'1', 1, 1, 1, NOW()), (1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', '本地存储', b'1', 1, 1, 1, NOW()),
(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file', '本地存储', b'0', 2, 2, 1, NOW()); (2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', '本地存储', b'0', 2, 2, 1, NOW());
-- 初始化客户端数据 -- 初始化客户端数据
INSERT INTO `sys_client` INSERT INTO `sys_client`

View File

@@ -260,8 +260,8 @@ CREATE TABLE IF NOT EXISTS `sys_storage` (
`access_key` varchar(255) DEFAULT NULL COMMENT 'Access Key', `access_key` varchar(255) DEFAULT NULL COMMENT 'Access Key',
`secret_key` varchar(255) DEFAULT NULL COMMENT 'Secret Key', `secret_key` varchar(255) DEFAULT NULL COMMENT 'Secret Key',
`endpoint` varchar(255) DEFAULT NULL COMMENT 'Endpoint', `endpoint` varchar(255) DEFAULT NULL COMMENT 'Endpoint',
`bucket_name` varchar(255) DEFAULT NULL COMMENT 'Bucket', `bucket_name` varchar(255) NOT NULL COMMENT 'Bucket',
`domain` varchar(255) NOT NULL DEFAULT '' COMMENT '域名', `domain` varchar(255) DEFAULT NULL COMMENT '域名',
`description` varchar(200) DEFAULT NULL COMMENT '描述', `description` varchar(200) DEFAULT NULL COMMENT '描述',
`is_default` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为默认存储', `is_default` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为默认存储',
`sort` int NOT NULL DEFAULT 999 COMMENT '排序', `sort` int NOT NULL DEFAULT 999 COMMENT '排序',
@@ -279,17 +279,16 @@ CREATE TABLE IF NOT EXISTS `sys_storage` (
CREATE TABLE IF NOT EXISTS `sys_file` ( 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 '原始名称',
`size` bigint(20) NOT NULL COMMENT '大小(字节)', `size` bigint(20) NOT NULL COMMENT '大小(字节)',
`url` varchar(512) NOT NULL COMMENT 'URL', `path` varchar(512) NOT NULL COMMENT '存储路径',
`parent_path` varchar(512) DEFAULT '/' COMMENT '上级目录', `extension` varchar(32) DEFAULT NULL COMMENT '扩展名',
`abs_path` varchar(1024) NOT NULL COMMENT '绝对路径', `content_type` varchar(255) DEFAULT NULL COMMENT '内容类型',
`extension` varchar(100) DEFAULT NULL COMMENT '扩展名',
`content_type` varchar(255) NOT 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音频',
`sha256` varchar(256) NOT NULL COMMENT 'SHA256值', `sha256` varchar(256) DEFAULT NULL COMMENT 'SHA256值',
`metadata` text DEFAULT NULL COMMENT '元数据', `metadata` text DEFAULT NULL COMMENT '元数据',
`thumbnail_name` varchar(255) DEFAULT NULL COMMENT '缩略图名称',
`thumbnail_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小(字节)', `thumbnail_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小(字节)',
`thumbnail_url` varchar(512) DEFAULT NULL COMMENT '缩略图URL',
`thumbnail_metadata` text DEFAULT NULL COMMENT '缩略图元数据', `thumbnail_metadata` text DEFAULT NULL COMMENT '缩略图元数据',
`storage_id` bigint(20) NOT NULL COMMENT '存储ID', `storage_id` bigint(20) NOT NULL COMMENT '存储ID',
`create_user` bigint(20) NOT NULL COMMENT '创建人', `create_user` bigint(20) NOT NULL COMMENT '创建人',
@@ -297,9 +296,8 @@ CREATE TABLE IF NOT EXISTS `sys_file` (
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人', `update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间', `update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `idx_url`(`url`),
INDEX `idx_sha256`(`sha256`),
INDEX `idx_type`(`type`), INDEX `idx_type`(`type`),
INDEX `idx_sha256`(`sha256`),
INDEX `idx_storage_id`(`storage_id`), INDEX `idx_storage_id`(`storage_id`),
INDEX `idx_create_user`(`create_user`) INDEX `idx_create_user`(`create_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表';

View File

@@ -273,8 +273,8 @@ INSERT INTO "sys_role_dept" ("role_id", "dept_id") VALUES (547888897925840927, 5
INSERT INTO "sys_storage" INSERT INTO "sys_storage"
("id", "name", "code", "type", "access_key", "secret_key", "endpoint", "bucket_name", "domain", "description", "is_default", "sort", "status", "create_user", "create_time") ("id", "name", "code", "type", "access_key", "secret_key", "endpoint", "bucket_name", "domain", "description", "is_default", "sort", "status", "create_user", "create_time")
VALUES VALUES
(1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', true, 1, 1, 1, NOW()), (1, '开发环境', 'local_dev', 1, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file/', '本地存储', true, 1, 1, 1, NOW()),
(2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file', '本地存储', false, 2, 2, 1, NOW()); (2, '生产环境', 'local_prod', 1, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file/', '本地存储', false, 2, 2, 1, NOW());
-- 初始化客户端数据 -- 初始化客户端数据
INSERT INTO "sys_client" INSERT INTO "sys_client"

View File

@@ -428,8 +428,8 @@ CREATE TABLE IF NOT EXISTS "sys_storage" (
"access_key" varchar(255) DEFAULT NULL, "access_key" varchar(255) DEFAULT NULL,
"secret_key" varchar(255) DEFAULT NULL, "secret_key" varchar(255) DEFAULT NULL,
"endpoint" varchar(255) DEFAULT NULL, "endpoint" varchar(255) DEFAULT NULL,
"bucket_name" varchar(255) DEFAULT NULL, "bucket_name" varchar(255) NOT NULL,
"domain" varchar(255) NOT NULL DEFAULT '', "domain" varchar(255) DEFAULT NULL,
"description" varchar(200) DEFAULT NULL, "description" varchar(200) DEFAULT NULL,
"is_default" bool NOT NULL DEFAULT false, "is_default" bool NOT NULL DEFAULT false,
"sort" int4 NOT NULL DEFAULT 999, "sort" int4 NOT NULL DEFAULT 999,
@@ -465,17 +465,16 @@ COMMENT ON TABLE "sys_storage" IS '存储表';
CREATE TABLE IF NOT EXISTS "sys_file" ( 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,
"size" int8 NOT NULL, "size" int8 NOT NULL,
"url" varchar(512) NOT NULL, "path" varchar(512) NOT NULL,
"parent_path" varchar(512) NOT NULL DEFAULT '/',
"abs_path" varchar(512) NOT NULL,
"extension" varchar(100) DEFAULT NULL, "extension" varchar(100) DEFAULT NULL,
"content_type" varchar(255) NOT NULL, "content_type" varchar(255) DEFAULT NULL,
"type" int2 NOT NULL DEFAULT 1, "type" int2 NOT NULL DEFAULT 1,
"sha256" varchar(256) NOT NULL, "sha256" varchar(256) NOT NULL,
"metadata" text DEFAULT NULL, "metadata" text DEFAULT NULL,
"thumbnail_name" varchar(255) DEFAULT NULL,
"thumbnail_size" int8 DEFAULT NULL, "thumbnail_size" int8 DEFAULT NULL,
"thumbnail_url" varchar(512) DEFAULT NULL,
"thumbnail_metadata" text DEFAULT NULL, "thumbnail_metadata" text DEFAULT NULL,
"storage_id" int8 NOT NULL, "storage_id" int8 NOT NULL,
"create_user" int8 NOT NULL, "create_user" int8 NOT NULL,
@@ -484,24 +483,22 @@ CREATE TABLE IF NOT EXISTS "sys_file" (
"update_time" timestamp DEFAULT NULL, "update_time" timestamp DEFAULT NULL,
PRIMARY KEY ("id") PRIMARY KEY ("id")
); );
CREATE INDEX "idx_file_url" ON "sys_file" ("url");
CREATE INDEX "idx_file_type" ON "sys_file" ("type"); CREATE INDEX "idx_file_type" ON "sys_file" ("type");
CREATE INDEX "idx_file_sha256" ON "sys_file" ("sha256"); CREATE INDEX "idx_file_sha256" ON "sys_file" ("sha256");
CREATE INDEX "idx_file_storage_id" ON "sys_file" ("storage_id"); CREATE INDEX "idx_file_storage_id" ON "sys_file" ("storage_id");
CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user"); CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user");
COMMENT ON COLUMN "sys_file"."id" IS 'ID'; 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"."size" IS '大小(字节)'; COMMENT ON COLUMN "sys_file"."size" IS '大小(字节)';
COMMENT ON COLUMN "sys_file"."url" IS 'URL'; COMMENT ON COLUMN "sys_file"."path" IS '存储路径';
COMMENT ON COLUMN "sys_file"."parent_path" IS '上级目录';
COMMENT ON COLUMN "sys_file"."abs_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音频';
COMMENT ON COLUMN "sys_file"."sha256" IS 'SHA256值'; COMMENT ON COLUMN "sys_file"."sha256" IS 'SHA256值';
COMMENT ON COLUMN "sys_file"."metadata" IS '元数据'; COMMENT ON COLUMN "sys_file"."metadata" IS '元数据';
COMMENT ON COLUMN "sys_file"."thumbnail_name" IS '缩略图名称';
COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)'; COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)';
COMMENT ON COLUMN "sys_file"."thumbnail_url" IS '缩略图URL';
COMMENT ON COLUMN "sys_file"."thumbnail_metadata" IS '缩略图元数据'; COMMENT ON COLUMN "sys_file"."thumbnail_metadata" IS '缩略图元数据';
COMMENT ON COLUMN "sys_file"."storage_id" IS '存储ID'; COMMENT ON COLUMN "sys_file"."storage_id" IS '存储ID';
COMMENT ON COLUMN "sys_file"."create_user" IS '创建人'; COMMENT ON COLUMN "sys_file"."create_user" IS '创建人';