新增:根据文件夹路径查询所有文件夹和文件
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user