新增:根据文件夹路径查询所有文件夹和文件

This commit is contained in:
2026-04-15 15:40:22 +08:00
parent 0ebf76c03c
commit 958057ad8f
4 changed files with 276 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
package com.sdm.common.entity.resp.data;
import lombok.Data;
@Data
public class LocalFileNodeVO {
/** 名称 */
private String name;
/** 完整路径 */
private String fullPath;
/** 是否文件夹 1是文件夹 0 文件 */
private String directory;
/** 文件大小(字节,文件夹为 0 */
private long size;
/** 最后修改时间 */
private String lastModifiedTime;
/** 文件的md5 */
private String fileNameHash;
/** 文件的类型 */
private String contentType;
}

View File

@@ -1,5 +1,6 @@
package com.sdm.common.utils;
import com.sdm.common.entity.resp.data.LocalFileNodeVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.http.HttpStatus;
@@ -16,9 +17,14 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -26,6 +32,19 @@ import java.util.zip.ZipOutputStream;
@Slf4j
@Component
public class FilesUtil {
/**
* 时间格式化器(抽取为常量,线程安全)
*/
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 哈希算法常量
*/
private static final String SHA256 = "SHA-256";
/**
* 在MappedByteBuffer释放后再对它进行读操作的话就会引发jvm crash在并发情况下很容易发生
* 正在释放时另一个线程正开始读取于是crash就发生了。所以为了系统稳定性释放前一般需要检 查是否还有线程在读或写
@@ -868,7 +887,203 @@ public class FilesUtil {
// ==================== 优化核心:文件列表扫描 ====================
/**
* 安全扫描文件夹下的一级文件/目录
* 增强:路径安全校验、空目录保护、流自动关闭、完整异常处理
* @param folderPath 文件夹路径
* @return 文件节点VO列表
*/
public static List<LocalFileNodeVO> listFiles(String folderPath) {
// 1. 空值校验
if (folderPath == null || folderPath.isBlank()) {
log.error("listFiles 参数异常:文件夹路径不能为空");
throw new IllegalArgumentException("文件夹路径不能为空");
}
// 2. 标准化路径(防路径穿越、相对路径混乱)
Path rootPath;
try {
rootPath = Paths.get(folderPath).normalize().toAbsolutePath();
} catch (InvalidPathException | SecurityException e) {
log.error("解析路径失败:{}", folderPath, e);
throw new IllegalArgumentException("无效的文件夹路径:" + folderPath, e);
}
// 3. 路径存在检查
if (!Files.exists(rootPath)) {
log.warn("文件夹不存在:{}", rootPath);
throw new RuntimeException("路径不存在:" + folderPath);
}
// 4. 是否是目录
if (!Files.isDirectory(rootPath)) {
log.error("指定路径不是文件夹:{}", rootPath);
throw new RuntimeException("指定路径不是文件夹:" + folderPath);
}
// 5. 可读权限
if (!Files.isReadable(rootPath)) {
log.error("无文件夹读取权限:{}", rootPath);
throw new RuntimeException("无文件夹读取权限:" + folderPath);
}
// 6. 安全遍历目录(必须 try-with-resources 关闭流)
try (Stream<Path> pathStream = Files.list(rootPath)) {
return pathStream
.map(FilesUtil::convertToFileNode) // 静态方法必须用 类名::
.sorted(fileNodeComparator())
.collect(Collectors.toList());
} catch (SecurityException se) {
log.error("遍历文件夹被安全策略阻止:{}", rootPath, se);
throw new RuntimeException("文件夹访问被拒绝:" + folderPath, se);
} catch (IOException e) {
log.error("读取目录发生IO异常{}", rootPath, e);
throw new RuntimeException("读取目录失败:" + folderPath, e);
}
}
/**
* 路径 -> LocalFileNodeVO 转换(增强健壮性、空保护、异常隔离)
* 单个文件异常不会导致整个列表失败
*/
private static LocalFileNodeVO convertToFileNode(Path path) {
LocalFileNodeVO vo = new LocalFileNodeVO();
try {
// 基础名称与路径
String fileName = path.getFileName() == null ? "未知名称" : path.getFileName().toString();
vo.setName(fileName);
vo.setFullPath(path.toAbsolutePath().toString());
// 目录/文件标记
boolean isDirectory = Files.isDirectory(path);
vo.setDirectory(isDirectory ? "1" : "0");
if (isDirectory) {
vo.setSize(0L);
vo.setContentType("directory");
} else {
// 文件大小(异常保护)
vo.setSize(Files.size(path));
// 内容类型
vo.setContentType(resolveContentTypeSafely(path));
}
// 文件名哈希(空值安全)
vo.setFileNameHash(calcFileNameHash(vo.getName()));
// 最后修改时间(异常保护)
vo.setLastModifiedTime(getLastModifiedTimeSafe(path));
} catch (Exception e) {
// 单个文件读取失败,只打警告,不中断整个列表
log.warn("读取文件信息失败,已跳过:{}", path, e);
// 设置默认值保证VO结构完整
vo.setSize(0L);
vo.setContentType("unknown");
vo.setLastModifiedTime("-");
}
return vo;
}
/**
* 安全获取最后修改时间(容错)
*/
private static String getLastModifiedTimeSafe(Path path) {
try {
FileTime fileTime = Files.getLastModifiedTime(path);
LocalDateTime time = LocalDateTime.ofInstant(fileTime.toInstant(), ZoneId.systemDefault());
return time.format(DATE_TIME_FORMATTER);
} catch (Exception e) {
log.debug("获取文件修改时间失败:{}", path, e);
return "-";
}
}
/**
* 文件排序规则(文件夹优先 > 修改时间倒序 > 名称正序)
*/
private static Comparator<LocalFileNodeVO> fileNodeComparator() {
return Comparator
.comparing(LocalFileNodeVO::getDirectory, Comparator.reverseOrder())
.thenComparing(
LocalFileNodeVO::getLastModifiedTime,
Comparator.nullsLast(Comparator.reverseOrder())
)
.thenComparing(
LocalFileNodeVO::getName,
Comparator.nullsFirst(String.CASE_INSENSITIVE_ORDER)
);
}
/**
* 安全获取文件ContentType不抛异常
*/
private static String resolveContentTypeSafely(Path path) {
try {
String type = Files.probeContentType(path);
if (type != null) {
return type;
}
} catch (Exception ignored) {
// 不打印异常,避免日志泛滥
}
return fallbackContentType(path.getFileName() == null ? "" : path.getFileName().toString());
}
/**
* 根据后缀名兜底ContentType空值安全
*/
private static String fallbackContentType(String fileName) {
if (fileName == null || fileName.isBlank()) {
return "application/octet-stream";
}
String lower = fileName.toLowerCase();
if (lower.endsWith(".txt")) return "text/plain";
if (lower.endsWith(".json")) return "application/json";
if (lower.endsWith(".xml")) return "application/xml";
if (lower.endsWith(".csv")) return "text/csv";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".gif")) return "image/gif";
if (lower.endsWith(".pdf")) return "application/pdf";
if (lower.endsWith(".zip")) return "application/zip";
if (lower.endsWith(".jar")) return "application/java-archive";
// 未知类型返回标准二进制流,避免前端解析异常
return "application/octet-stream";
}
/**
* 计算文件名SHA-256哈希线程安全、空值安全
*/
private static String calcFileNameHash(String fileName) {
if (fileName == null || fileName.isBlank()) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance(SHA256);
byte[] hashBytes = digest.digest(fileName.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hashBytes);
} catch (Exception e) {
log.warn("计算文件名Hash失败{}", fileName, e);
return "";
}
}
/**
* 字节数组转16进制健壮性增强
*/
private static String bytesToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -26,7 +26,9 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
@@ -47,6 +49,12 @@ public class DataFileController implements IDataFeignClient {
@Autowired
private IDataFileService IDataFileService;
/**
* 本地可访问的基础路径
*/
@Value("${local.file.basePath:/home/simulation}")
private String LOCAL_BASE_STORAGE_PATH ;
/**
* 创建文件夹
*
@@ -783,4 +791,14 @@ public class DataFileController implements IDataFeignClient {
return IDataFileService.getFileMinioObjectkeysByDirIds(dirIds);
}
@PostMapping("/getLocalFiles")
public SdmResponse<List<LocalFileNodeVO>> scanDir(@RequestBody LocalFileListReq req) {
String folderPath = req.getFolderPath();
if(StringUtils.isBlank(folderPath)||!folderPath.startsWith(LOCAL_BASE_STORAGE_PATH)) {
throw new RuntimeException("目录路径非法");
}
List<LocalFileNodeVO> localFiles = FilesUtil.listFiles(folderPath);
return SdmResponse.success(localFiles);
}
}

View File

@@ -0,0 +1,14 @@
package com.sdm.data.model.req;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LocalFileListReq {
/**
* 目录路径
*/
@NotBlank
private String folderPath;
}