commit
32ed3114d4
9 changed files with 1620 additions and 0 deletions
@ -0,0 +1,132 @@ |
|||
# =========================================== |
|||
# 编译和构建输出 |
|||
# =========================================== |
|||
target/ |
|||
build/ |
|||
out/ |
|||
bin/ |
|||
*.class |
|||
*.jar |
|||
*.war |
|||
*.ear |
|||
|
|||
# =========================================== |
|||
# 依赖和缓存 |
|||
# =========================================== |
|||
.m2/repository/ |
|||
.gradle/ |
|||
node_modules/ |
|||
|
|||
# =========================================== |
|||
# 日志文件 |
|||
# =========================================== |
|||
*.log |
|||
logs/ |
|||
log/ |
|||
|
|||
# =========================================== |
|||
# 配置文件(包含敏感信息) |
|||
# =========================================== |
|||
# 主要配置文件 |
|||
application.properties |
|||
application.yml |
|||
application-*.properties |
|||
application-*.yml |
|||
|
|||
# 例外:可以提交示例配置文件 |
|||
!application.properties.example |
|||
!application.yml.example |
|||
|
|||
# =========================================== |
|||
# IDE 配置文件 |
|||
# =========================================== |
|||
# IntelliJ IDEA |
|||
.idea/ |
|||
*.iws |
|||
*.iml |
|||
*.ipr |
|||
*.eml |
|||
.idea_modules/ |
|||
|
|||
# Eclipse |
|||
.settings/ |
|||
.project |
|||
.classpath |
|||
.metadata/ |
|||
|
|||
# NetBeans |
|||
nbproject/ |
|||
nbactions.xml |
|||
|
|||
# VS Code |
|||
.vscode/ |
|||
!.vscode/settings.json.example |
|||
!.vscode/launch.json.example |
|||
|
|||
# =========================================== |
|||
# 操作系统文件 |
|||
# =========================================== |
|||
# Windows |
|||
Thumbs.db |
|||
ehthumbs.db |
|||
Desktop.ini |
|||
|
|||
# Mac OS X |
|||
.DS_Store |
|||
|
|||
# Linux |
|||
*~ |
|||
|
|||
# =========================================== |
|||
# 临时文件 |
|||
# =========================================== |
|||
*.tmp |
|||
*.temp |
|||
*~ |
|||
~* |
|||
*.swp |
|||
*.swo |
|||
*.# |
|||
|
|||
# =========================================== |
|||
# 测试和报告 |
|||
# =========================================== |
|||
target/surefire-reports/ |
|||
target/failsafe-reports/ |
|||
target/site/ |
|||
*.coverage |
|||
*.coverage.xml |
|||
|
|||
# =========================================== |
|||
# 上传和下载目录 |
|||
# =========================================== |
|||
uploads/ |
|||
downloads/ |
|||
temp/ |
|||
|
|||
# =========================================== |
|||
# Tomcat 相关 |
|||
# =========================================== |
|||
apache-tomcat-*/ |
|||
tomcat/ |
|||
|
|||
# =========================================== |
|||
# 数据库文件 |
|||
# =========================================== |
|||
*.db |
|||
*.sql |
|||
*.h2.db |
|||
|
|||
# =========================================== |
|||
# 文档生成 |
|||
# =========================================== |
|||
apidocs/ |
|||
docs/_build/ |
|||
|
|||
# =========================================== |
|||
# 其他 |
|||
# =========================================== |
|||
*.bak |
|||
*.backup |
|||
*.orig |
|||
*.rej |
|||
@ -0,0 +1,106 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 |
|||
http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>2.7.18</version> |
|||
</parent> |
|||
|
|||
<groupId>com.example</groupId> |
|||
<artifactId>file-search-service</artifactId> |
|||
<version>1.0.0</version> |
|||
<packaging>war</packaging> |
|||
<name>File Search Service</name> |
|||
<description>Windows Server文件搜索服务</description> |
|||
|
|||
<properties> |
|||
<java.version>1.8</java.version> |
|||
<maven.compiler.source>1.8</maven.compiler.source> |
|||
<maven.compiler.target>1.8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> |
|||
</properties> |
|||
|
|||
<dependencies> |
|||
<!-- Spring Boot Web Starter (排除内嵌Tomcat) --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-tomcat</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
|
|||
<!-- 参数验证依赖 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Servlet API (Tomcat 8使用Servlet 3.1) --> |
|||
<dependency> |
|||
<groupId>javax.servlet</groupId> |
|||
<artifactId>javax.servlet-api</artifactId> |
|||
<version>3.1.0</version> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
|
|||
<!-- JSON处理 --> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-databind</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 工具类 --> |
|||
<dependency> |
|||
<groupId>org.apache.commons</groupId> |
|||
<artifactId>commons-lang3</artifactId> |
|||
<version>3.12.0</version> |
|||
</dependency> |
|||
|
|||
<!-- 测试依赖 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<finalName>file-search-service</finalName> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
<configuration> |
|||
<excludes> |
|||
<exclude> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-tomcat</artifactId> |
|||
</exclude> |
|||
</excludes> |
|||
</configuration> |
|||
</plugin> |
|||
|
|||
<!-- 指定JDK版本 --> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>3.8.1</version> |
|||
<configuration> |
|||
<source>1.8</source> |
|||
<target>1.8</target> |
|||
<encoding>UTF-8</encoding> |
|||
</configuration> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
|||
@ -0,0 +1,19 @@ |
|||
package com.example.fileservice; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
import org.springframework.boot.builder.SpringApplicationBuilder; |
|||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; |
|||
|
|||
@SpringBootApplication |
|||
public class FileServiceApplication extends SpringBootServletInitializer { |
|||
|
|||
@Override |
|||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { |
|||
return application.sources(FileServiceApplication.class); |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(FileServiceApplication.class, args); |
|||
} |
|||
} |
|||
@ -0,0 +1,687 @@ |
|||
package com.example.fileservice.controller; |
|||
|
|||
import com.example.fileservice.dto.ApiResponse; |
|||
import com.example.fileservice.dto.FileInfo; |
|||
import com.example.fileservice.service.FileService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.core.io.FileSystemResource; |
|||
import org.springframework.core.io.Resource; // 正确的Spring Resource
|
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import javax.servlet.http.HttpServletResponse; |
|||
import javax.validation.Valid; |
|||
import javax.validation.constraints.NotBlank; |
|||
import java.io.File; |
|||
import java.io.IOException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/files") |
|||
@CrossOrigin(origins = "*") |
|||
public class FileController { |
|||
|
|||
@Autowired |
|||
private FileService fileService; |
|||
|
|||
/** |
|||
* 查找文件(简单接口) |
|||
* GET /api/files/find?path=C:\Users&fileName=*.txt |
|||
*/ |
|||
@GetMapping("/find") |
|||
public ResponseEntity<ApiResponse<List<String>>> findFiles( |
|||
@RequestParam("path") String searchPath, |
|||
@RequestParam("fileName") String fileName) { |
|||
|
|||
try { |
|||
List<String> foundFiles = fileService.findFiles(searchPath, fileName); |
|||
|
|||
if (foundFiles.isEmpty()) { |
|||
return ResponseEntity.ok(ApiResponse.success("未找到匹配的文件", foundFiles)); |
|||
} |
|||
|
|||
return ResponseEntity.ok(ApiResponse.success( |
|||
String.format("找到 %d 个文件", foundFiles.size()), |
|||
foundFiles |
|||
)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 查找文件(详细接口) |
|||
* GET /api/files/find-detail?path=C:\Users&fileName=*.txt&maxDepth=3 |
|||
*/ |
|||
@GetMapping("/find-detail") |
|||
public ResponseEntity<ApiResponse<List<FileInfo>>> findFilesWithDetail( |
|||
@RequestParam("path") String searchPath, |
|||
@RequestParam("fileName") String fileName, |
|||
@RequestParam(value = "maxDepth", defaultValue = "5") int maxDepth) { |
|||
|
|||
try { |
|||
// 限制最大深度,防止递归过深
|
|||
if (maxDepth < 1 || maxDepth > 20) { |
|||
maxDepth = 5; |
|||
} |
|||
|
|||
// 方法1: 使用 findFilesWithInfo 方法(如果存在)
|
|||
// List<FileInfo> foundFiles = fileService.findFilesWithInfo(searchPath, fileName);
|
|||
|
|||
// 方法2: 使用 findFiles 获取路径,然后转换为 FileInfo
|
|||
List<FileInfo> foundFiles = fileService.findFiles(searchPath, fileName, maxDepth) |
|||
.stream() |
|||
.map(path -> fileService.getFileInfo(path)) |
|||
.collect(Collectors.toList()); |
|||
|
|||
return ResponseEntity.ok(ApiResponse.success( |
|||
String.format("找到 %d 个文件", foundFiles.size()), |
|||
foundFiles |
|||
)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取文件信息 |
|||
* GET /api/files/info?path=C:\Windows\notepad.exe |
|||
*/ |
|||
@GetMapping("/info") |
|||
public ResponseEntity<ApiResponse<FileInfo>> getFileInfo( |
|||
@RequestParam("path") String filePath) { |
|||
|
|||
try { |
|||
FileInfo fileInfo = fileService.getFileInfo(filePath); |
|||
return ResponseEntity.ok(ApiResponse.success(fileInfo)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 列出目录内容 |
|||
* GET /api/files/list?path=C:\Windows |
|||
*/ |
|||
@GetMapping("/list") |
|||
public ResponseEntity<ApiResponse<List<FileInfo>>> listDirectory( |
|||
@RequestParam("path") String dirPath) { |
|||
|
|||
try { |
|||
List<FileInfo> fileList = fileService.listDirectory(dirPath); |
|||
return ResponseEntity.ok(ApiResponse.success( |
|||
String.format("目录包含 %d 个项目", fileList.size()), |
|||
fileList |
|||
)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取目录统计信息 |
|||
* GET /api/files/stats?path=C:\Windows |
|||
*/ |
|||
@GetMapping("/stats") |
|||
public ResponseEntity<ApiResponse<FileService.DirectoryStats>> getDirectoryStats( |
|||
@RequestParam("path") String dirPath) { |
|||
|
|||
try { |
|||
FileService.DirectoryStats stats = fileService.getDirectoryStats(dirPath); |
|||
return ResponseEntity.ok(ApiResponse.success(stats)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 批量查找文件(POST请求) |
|||
* POST /api/files/batch-find |
|||
*/ |
|||
@PostMapping("/batch-find") |
|||
public ResponseEntity<ApiResponse<List<FileInfo>>> batchFindFiles( |
|||
@Valid @RequestBody FileSearchRequest request) { |
|||
|
|||
try { |
|||
// 验证参数
|
|||
if (request.getSearchPath() == null || request.getSearchPath().isEmpty()) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error("搜索路径不能为空")); |
|||
} |
|||
|
|||
if (request.getFileName() == null || request.getFileName().isEmpty()) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error("文件名不能为空")); |
|||
} |
|||
|
|||
// 限制最大深度
|
|||
int maxDepth = request.getMaxDepth(); |
|||
if (maxDepth < 1 || maxDepth > 10) { |
|||
maxDepth = 5; |
|||
} |
|||
|
|||
List<FileInfo> foundFiles = fileService.findFiles( |
|||
request.getSearchPath(), |
|||
request.getFileName(), |
|||
maxDepth |
|||
).stream() |
|||
.map(path -> fileService.getFileInfo(path)) |
|||
.collect(Collectors.toList()); |
|||
|
|||
// 如果指定了最大结果数,则截取结果
|
|||
if (request.getMaxResults() > 0 && foundFiles.size() > request.getMaxResults()) { |
|||
foundFiles = foundFiles.subList(0, request.getMaxResults()); |
|||
} |
|||
|
|||
return ResponseEntity.ok(ApiResponse.success( |
|||
String.format("找到 %d 个文件,显示前 %d 个", |
|||
foundFiles.size(), |
|||
Math.min(foundFiles.size(), request.getMaxResults() > 0 ? request.getMaxResults() : foundFiles.size())), |
|||
foundFiles |
|||
)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error(e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 文件搜索请求类 |
|||
*/ |
|||
public static class FileSearchRequest { |
|||
@NotBlank(message = "搜索路径不能为空") |
|||
private String searchPath; |
|||
|
|||
@NotBlank(message = "文件名不能为空") |
|||
private String fileName; |
|||
|
|||
private int maxDepth = 5; |
|||
private int maxResults = 100; |
|||
private boolean includeHidden = false; |
|||
private long minSize = 0; |
|||
private long maxSize = Long.MAX_VALUE; |
|||
|
|||
// Getter和Setter
|
|||
public String getSearchPath() { |
|||
return searchPath; |
|||
} |
|||
|
|||
public void setSearchPath(String searchPath) { |
|||
this.searchPath = searchPath; |
|||
} |
|||
|
|||
public String getFileName() { |
|||
return fileName; |
|||
} |
|||
|
|||
public void setFileName(String fileName) { |
|||
this.fileName = fileName; |
|||
} |
|||
|
|||
public int getMaxDepth() { |
|||
return maxDepth; |
|||
} |
|||
|
|||
public void setMaxDepth(int maxDepth) { |
|||
this.maxDepth = maxDepth; |
|||
} |
|||
|
|||
public int getMaxResults() { |
|||
return maxResults; |
|||
} |
|||
|
|||
public void setMaxResults(int maxResults) { |
|||
this.maxResults = maxResults; |
|||
} |
|||
|
|||
public boolean isIncludeHidden() { |
|||
return includeHidden; |
|||
} |
|||
|
|||
public void setIncludeHidden(boolean includeHidden) { |
|||
this.includeHidden = includeHidden; |
|||
} |
|||
|
|||
public long getMinSize() { |
|||
return minSize; |
|||
} |
|||
|
|||
public void setMinSize(long minSize) { |
|||
this.minSize = minSize; |
|||
} |
|||
|
|||
public long getMaxSize() { |
|||
return maxSize; |
|||
} |
|||
|
|||
public void setMaxSize(long maxSize) { |
|||
this.maxSize = maxSize; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "FileSearchRequest{" + |
|||
"searchPath='" + searchPath + '\'' + |
|||
", fileName='" + fileName + '\'' + |
|||
", maxDepth=" + maxDepth + |
|||
", maxResults=" + maxResults + |
|||
", includeHidden=" + includeHidden + |
|||
", minSize=" + minSize + |
|||
", maxSize=" + maxSize + |
|||
'}'; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 系统信息接口 |
|||
* GET /api/files/system-info |
|||
*/ |
|||
@GetMapping("/system-info") |
|||
public ResponseEntity<ApiResponse<Map<String, Object>>> getSystemInfo() { |
|||
try { |
|||
Map<String, Object> info = new java.util.HashMap<>(); |
|||
|
|||
// 系统信息
|
|||
info.put("os.name", System.getProperty("os.name")); |
|||
info.put("os.version", System.getProperty("os.version")); |
|||
info.put("os.arch", System.getProperty("os.arch")); |
|||
info.put("java.version", System.getProperty("java.version")); |
|||
info.put("java.home", System.getProperty("java.home")); |
|||
|
|||
// 磁盘信息
|
|||
java.io.File[] roots = java.io.File.listRoots(); |
|||
List<Map<String, Object>> disks = new java.util.ArrayList<>(); |
|||
|
|||
for (java.io.File root : roots) { |
|||
Map<String, Object> diskInfo = new java.util.HashMap<>(); |
|||
diskInfo.put("path", root.getAbsolutePath()); |
|||
diskInfo.put("totalSpace", root.getTotalSpace()); |
|||
diskInfo.put("freeSpace", root.getFreeSpace()); |
|||
diskInfo.put("usableSpace", root.getUsableSpace()); |
|||
diskInfo.put("formattedTotalSpace", formatSize(root.getTotalSpace())); |
|||
diskInfo.put("formattedFreeSpace", formatSize(root.getFreeSpace())); |
|||
diskInfo.put("formattedUsableSpace", formatSize(root.getUsableSpace())); |
|||
|
|||
if (root.getTotalSpace() > 0) { |
|||
double usedPercent = 100.0 * (root.getTotalSpace() - root.getFreeSpace()) / root.getTotalSpace(); |
|||
diskInfo.put("usedPercent", String.format("%.1f%%", usedPercent)); |
|||
} |
|||
|
|||
disks.add(diskInfo); |
|||
} |
|||
|
|||
info.put("disks", disks); |
|||
info.put("serverTime", java.time.LocalDateTime.now().toString()); |
|||
|
|||
return ResponseEntity.ok(ApiResponse.success("系统信息获取成功", info)); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.internalServerError() |
|||
.body(ApiResponse.error("获取系统信息失败: " + e.getMessage())); |
|||
} |
|||
} |
|||
|
|||
private String formatSize(long size) { |
|||
if (size < 1024) return size + " B"; |
|||
if (size < 1024 * 1024) return String.format("%.1f KB", size / 1024.0); |
|||
if (size < 1024 * 1024 * 1024) return String.format("%.1f MB", size / (1024.0 * 1024.0)); |
|||
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); |
|||
} |
|||
|
|||
/** |
|||
* 查找并下载单个文件 |
|||
* GET /api/files/download?path=C:\Windows&fileName=notepad.exe |
|||
* GET /api/files/download?path=C:\Windows&fileName=*.txt (如果只匹配一个) |
|||
*/ |
|||
@GetMapping("/download") |
|||
public ResponseEntity<Resource> downloadFile( |
|||
@RequestParam("path") String searchPath, |
|||
@RequestParam("fileName") String fileName, |
|||
HttpServletResponse response) { |
|||
|
|||
try { |
|||
// 查找匹配的文件
|
|||
List<String> foundFiles = fileService.findFiles(searchPath, fileName); |
|||
|
|||
if (foundFiles.isEmpty()) { |
|||
// 返回404错误
|
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
|
|||
if (foundFiles.size() > 1) { |
|||
// 如果找到多个文件,返回错误信息
|
|||
String errorMessage = String.format("找到 %d 个匹配的文件,请指定更精确的文件名", foundFiles.size()); |
|||
throw new IllegalArgumentException(errorMessage); |
|||
} |
|||
|
|||
// 获取唯一的文件路径
|
|||
String filePath = foundFiles.get(0); |
|||
File file = new File(filePath); |
|||
|
|||
// 验证文件是否存在且可读
|
|||
if (!file.exists()) { |
|||
throw new IllegalArgumentException("文件不存在: " + filePath); |
|||
} |
|||
|
|||
if (!file.canRead()) { |
|||
throw new IllegalArgumentException("文件不可读: " + filePath); |
|||
} |
|||
|
|||
// 如果是目录,不能下载
|
|||
if (file.isDirectory()) { |
|||
throw new IllegalArgumentException("不能下载目录: " + filePath); |
|||
} |
|||
|
|||
// 准备文件资源
|
|||
Resource resource = new FileSystemResource(file); |
|||
|
|||
// 获取文件名
|
|||
String filename = file.getName(); |
|||
|
|||
// 确定文件类型
|
|||
String contentType = determineContentType(file); |
|||
|
|||
// 设置响应头
|
|||
return ResponseEntity.ok() |
|||
.contentType(MediaType.parseMediaType(contentType)) |
|||
.header(HttpHeaders.CONTENT_DISPOSITION, |
|||
String.format("attachment; filename=\"%s\"", filename)) |
|||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length())) |
|||
.body(resource); |
|||
|
|||
} catch (IllegalArgumentException e) { |
|||
// 返回400错误
|
|||
return ResponseEntity.badRequest() |
|||
.body(null); |
|||
} catch (Exception e) { |
|||
// 返回500错误
|
|||
return ResponseEntity.internalServerError() |
|||
.body(null); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据文件扩展名确定Content-Type |
|||
*/ |
|||
private String determineContentType(File file) { |
|||
String fileName = file.getName().toLowerCase(); |
|||
|
|||
if (fileName.endsWith(".txt") || fileName.endsWith(".log")) { |
|||
return "text/plain; charset=UTF-8"; |
|||
} else if (fileName.endsWith(".pdf")) { |
|||
return "application/pdf"; |
|||
} else if (fileName.endsWith(".doc") || fileName.endsWith(".docx")) { |
|||
return "application/msword"; |
|||
} else if (fileName.endsWith(".xls") || fileName.endsWith(".xlsx")) { |
|||
return "application/vnd.ms-excel"; |
|||
} else if (fileName.endsWith(".zip")) { |
|||
return "application/zip"; |
|||
} else if (fileName.endsWith(".rar")) { |
|||
return "application/x-rar-compressed"; |
|||
} else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { |
|||
return "image/jpeg"; |
|||
} else if (fileName.endsWith(".png")) { |
|||
return "image/png"; |
|||
} else if (fileName.endsWith(".gif")) { |
|||
return "image/gif"; |
|||
} else if (fileName.endsWith(".exe")) { |
|||
return "application/x-msdownload"; |
|||
} else if (fileName.endsWith(".xml")) { |
|||
return "application/xml"; |
|||
} else if (fileName.endsWith(".json")) { |
|||
return "application/json"; |
|||
} else if (fileName.endsWith(".html") || fileName.endsWith(".htm")) { |
|||
return "text/html"; |
|||
} else if (fileName.endsWith(".css")) { |
|||
return "text/css"; |
|||
} else if (fileName.endsWith(".js")) { |
|||
return "application/javascript"; |
|||
} else { |
|||
// 默认二进制流
|
|||
return "application/octet-stream"; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 查找并直接下载文件(替代版本,使用Servlet响应) |
|||
* GET /api/files/download2?path=C:\Windows&fileName=notepad.exe |
|||
*/ |
|||
@GetMapping("/download2") |
|||
public void downloadFileDirect( |
|||
@RequestParam("path") String searchPath, |
|||
@RequestParam("fileName") String fileName, |
|||
HttpServletResponse response) { |
|||
|
|||
try { |
|||
// 查找匹配的文件
|
|||
List<String> foundFiles = fileService.findFiles(searchPath, fileName); |
|||
|
|||
if (foundFiles.isEmpty()) { |
|||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "未找到匹配的文件"); |
|||
return; |
|||
} |
|||
|
|||
if (foundFiles.size() > 1) { |
|||
String errorMessage = String.format("找到 %d 个匹配的文件,请指定更精确的文件名", foundFiles.size()); |
|||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, errorMessage); |
|||
return; |
|||
} |
|||
|
|||
// 获取唯一的文件路径
|
|||
String filePath = foundFiles.get(0); |
|||
Path path = Paths.get(filePath); |
|||
|
|||
// 验证文件
|
|||
if (!Files.exists(path)) { |
|||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在: " + filePath); |
|||
return; |
|||
} |
|||
|
|||
if (!Files.isRegularFile(path)) { |
|||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "不能下载目录: " + filePath); |
|||
return; |
|||
} |
|||
|
|||
// 获取文件名
|
|||
String filename = path.getFileName().toString(); |
|||
|
|||
// 设置响应头
|
|||
response.setContentType(determineContentType(path.toFile())); |
|||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, |
|||
"attachment; filename=\"" + filename + "\""); |
|||
response.setContentLengthLong(Files.size(path)); |
|||
|
|||
// 将文件内容复制到响应输出流
|
|||
Files.copy(path, response.getOutputStream()); |
|||
response.flushBuffer(); |
|||
|
|||
} catch (IOException e) { |
|||
try { |
|||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, |
|||
"下载文件时发生错误: " + e.getMessage()); |
|||
} catch (IOException ex) { |
|||
// 忽略
|
|||
} |
|||
} catch (Exception e) { |
|||
try { |
|||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, |
|||
"系统错误: " + e.getMessage()); |
|||
} catch (IOException ex) { |
|||
// 忽略
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 查找并预览文件(仅用于文本文件) |
|||
* GET /api/files/preview?path=C:\Windows&fileName=*.txt |
|||
*/ |
|||
@GetMapping("/preview") |
|||
public ResponseEntity<String> previewTextFile( |
|||
@RequestParam("path") String searchPath, |
|||
@RequestParam("fileName") String fileName, |
|||
@RequestParam(value = "maxSize", defaultValue = "10240") int maxSize) { |
|||
|
|||
try { |
|||
// 限制预览文件大小(默认10KB)
|
|||
if (maxSize > 102400) { // 最大100KB
|
|||
maxSize = 102400; |
|||
} |
|||
|
|||
// 查找匹配的文件
|
|||
List<String> foundFiles = fileService.findFiles(searchPath, fileName); |
|||
|
|||
if (foundFiles.isEmpty()) { |
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
|
|||
if (foundFiles.size() > 1) { |
|||
String errorMessage = String.format("找到 %d 个匹配的文件,请指定更精确的文件名", foundFiles.size()); |
|||
return ResponseEntity.badRequest() |
|||
.body("错误: " + errorMessage); |
|||
} |
|||
|
|||
// 获取唯一的文件路径
|
|||
String filePath = foundFiles.get(0); |
|||
Path path = Paths.get(filePath); |
|||
|
|||
// 验证文件
|
|||
if (!Files.exists(path)) { |
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
|
|||
if (!Files.isRegularFile(path)) { |
|||
return ResponseEntity.badRequest() |
|||
.body("错误: 不能预览目录"); |
|||
} |
|||
|
|||
// 检查文件大小
|
|||
long fileSize = Files.size(path); |
|||
if (fileSize > maxSize) { |
|||
return ResponseEntity.badRequest() |
|||
.body(String.format("文件过大(%d字节),超过预览限制(%d字节)", fileSize, maxSize)); |
|||
} |
|||
|
|||
// 读取文件内容(假设是文本文件)
|
|||
String content = new String(Files.readAllBytes(path)); |
|||
|
|||
return ResponseEntity.ok() |
|||
.contentType(MediaType.TEXT_PLAIN) |
|||
.header(HttpHeaders.CONTENT_DISPOSITION, |
|||
String.format("inline; filename=\"%s\"", path.getFileName())) |
|||
.body(content); |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.internalServerError() |
|||
.body("预览文件时发生错误: " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 批量下载文件(压缩为ZIP) |
|||
* POST /api/files/download-batch |
|||
*/ |
|||
@PostMapping("/download-batch") |
|||
public ResponseEntity<Resource> downloadMultipleFiles( |
|||
@Valid @RequestBody BatchDownloadRequest request) { |
|||
|
|||
try { |
|||
// 查找匹配的文件
|
|||
List<String> foundFiles = fileService.findFiles( |
|||
request.getSearchPath(), |
|||
request.getFileName(), |
|||
request.getMaxDepth() |
|||
); |
|||
|
|||
if (foundFiles.isEmpty()) { |
|||
return ResponseEntity.notFound().build(); |
|||
} |
|||
|
|||
// 限制文件数量
|
|||
if (foundFiles.size() > request.getMaxFiles()) { |
|||
foundFiles = foundFiles.subList(0, request.getMaxFiles()); |
|||
} |
|||
|
|||
// 这里应该创建ZIP文件并返回
|
|||
// 由于实现较复杂,这里返回第一个文件作为示例
|
|||
// 实际项目中应该实现ZIP压缩逻辑
|
|||
|
|||
if (foundFiles.size() == 1) { |
|||
// 单个文件直接下载
|
|||
return downloadFile(request.getSearchPath(), request.getFileName(), null); |
|||
} else { |
|||
// 多个文件暂时返回错误,需要实现ZIP功能
|
|||
return ResponseEntity.badRequest() |
|||
.body(null); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
return ResponseEntity.internalServerError() |
|||
.body(null); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 文件下载请求类 |
|||
*/ |
|||
public static class FileDownloadRequest { |
|||
@NotBlank(message = "搜索路径不能为空") |
|||
private String searchPath; |
|||
|
|||
@NotBlank(message = "文件名不能为空") |
|||
private String fileName; |
|||
|
|||
// Getter和Setter
|
|||
public String getSearchPath() { return searchPath; } |
|||
public void setSearchPath(String searchPath) { this.searchPath = searchPath; } |
|||
|
|||
public String getFileName() { return fileName; } |
|||
public void setFileName(String fileName) { this.fileName = fileName; } |
|||
} |
|||
|
|||
/** |
|||
* 批量下载请求类 |
|||
*/ |
|||
public static class BatchDownloadRequest { |
|||
@NotBlank(message = "搜索路径不能为空") |
|||
private String searchPath; |
|||
|
|||
@NotBlank(message = "文件名不能为空") |
|||
private String fileName; |
|||
|
|||
private int maxDepth = 3; |
|||
private int maxFiles = 10; |
|||
|
|||
// Getter和Setter
|
|||
public String getSearchPath() { return searchPath; } |
|||
public void setSearchPath(String searchPath) { this.searchPath = searchPath; } |
|||
|
|||
public String getFileName() { return fileName; } |
|||
public void setFileName(String fileName) { this.fileName = fileName; } |
|||
|
|||
public int getMaxDepth() { return maxDepth; } |
|||
public void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; } |
|||
|
|||
public int getMaxFiles() { return maxFiles; } |
|||
public void setMaxFiles(int maxFiles) { this.maxFiles = maxFiles; } |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
package com.example.fileservice.controller; |
|||
|
|||
import com.example.fileservice.dto.ApiResponse; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.validation.FieldError; |
|||
import org.springframework.web.bind.MethodArgumentNotValidException; |
|||
import org.springframework.web.bind.annotation.ControllerAdvice; |
|||
import org.springframework.web.bind.annotation.ExceptionHandler; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@ControllerAdvice |
|||
public class GlobalExceptionHandler { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); |
|||
|
|||
/** |
|||
* 处理参数验证异常 |
|||
*/ |
|||
@ExceptionHandler(MethodArgumentNotValidException.class) |
|||
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions( |
|||
MethodArgumentNotValidException ex) { |
|||
|
|||
Map<String, String> errors = new HashMap<>(); |
|||
ex.getBindingResult().getAllErrors().forEach((error) -> { |
|||
String fieldName = ((FieldError) error).getField(); |
|||
String errorMessage = error.getDefaultMessage(); |
|||
errors.put(fieldName, errorMessage); |
|||
}); |
|||
|
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error("参数验证失败", errors)); |
|||
} |
|||
|
|||
/** |
|||
* 处理IllegalArgumentException异常 |
|||
*/ |
|||
@ExceptionHandler(IllegalArgumentException.class) |
|||
public ResponseEntity<ApiResponse<String>> handleIllegalArgument( |
|||
IllegalArgumentException ex, HttpServletRequest request) { |
|||
|
|||
logger.warn("非法参数异常: {} - {}", request.getRequestURI(), ex.getMessage()); |
|||
|
|||
return ResponseEntity.badRequest() |
|||
.body(ApiResponse.error("请求参数错误: " + ex.getMessage())); |
|||
} |
|||
|
|||
/** |
|||
* 处理RuntimeException异常 |
|||
*/ |
|||
@ExceptionHandler(RuntimeException.class) |
|||
public ResponseEntity<ApiResponse<String>> handleRuntimeException( |
|||
RuntimeException ex, HttpServletRequest request) { |
|||
|
|||
logger.error("运行时异常: {} - {}", request.getRequestURI(), ex.getMessage(), ex); |
|||
|
|||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) |
|||
.body(ApiResponse.error("服务器内部错误: " + ex.getMessage())); |
|||
} |
|||
|
|||
/** |
|||
* 处理所有其他异常 |
|||
*/ |
|||
@ExceptionHandler(Exception.class) |
|||
public ResponseEntity<ApiResponse<String>> handleGeneralException( |
|||
Exception ex, HttpServletRequest request) { |
|||
|
|||
logger.error("未处理异常: {} - {}", request.getRequestURI(), ex.getMessage(), ex); |
|||
|
|||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) |
|||
.body(ApiResponse.error("系统繁忙,请稍后重试")); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
package com.example.fileservice.controller; |
|||
|
|||
import com.example.fileservice.dto.ApiResponse; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/health") |
|||
public class HealthController { |
|||
|
|||
/** |
|||
* 健康检查接口 |
|||
* GET /api/health |
|||
*/ |
|||
@GetMapping |
|||
public ApiResponse<Map<String, Object>> healthCheck() { |
|||
Map<String, Object> healthInfo = new HashMap<>(); |
|||
|
|||
healthInfo.put("status", "UP"); |
|||
healthInfo.put("service", "file-search-service"); |
|||
healthInfo.put("version", "1.0.0"); |
|||
healthInfo.put("timestamp", java.time.LocalDateTime.now().toString()); |
|||
|
|||
// 系统信息
|
|||
healthInfo.put("javaVersion", System.getProperty("java.version")); |
|||
healthInfo.put("os", System.getProperty("os.name")); |
|||
healthInfo.put("availableProcessors", Runtime.getRuntime().availableProcessors()); |
|||
healthInfo.put("freeMemory", Runtime.getRuntime().freeMemory()); |
|||
healthInfo.put("totalMemory", Runtime.getRuntime().totalMemory()); |
|||
healthInfo.put("maxMemory", Runtime.getRuntime().maxMemory()); |
|||
|
|||
// 格式化内存信息
|
|||
healthInfo.put("formattedFreeMemory", formatMemory(Runtime.getRuntime().freeMemory())); |
|||
healthInfo.put("formattedTotalMemory", formatMemory(Runtime.getRuntime().totalMemory())); |
|||
healthInfo.put("formattedMaxMemory", formatMemory(Runtime.getRuntime().maxMemory())); |
|||
|
|||
return ApiResponse.success("服务运行正常", healthInfo); |
|||
} |
|||
|
|||
/** |
|||
* 存活检查接口 |
|||
* GET /api/health/alive |
|||
*/ |
|||
@GetMapping("/alive") |
|||
public ApiResponse<String> alive() { |
|||
return ApiResponse.success("服务存活", "OK"); |
|||
} |
|||
|
|||
private String formatMemory(long bytes) { |
|||
if (bytes < 1024) return bytes + " B"; |
|||
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); |
|||
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); |
|||
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
package com.example.fileservice.dto; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
|
|||
@JsonInclude(JsonInclude.Include.NON_NULL) |
|||
public class ApiResponse<T> { |
|||
private boolean success; |
|||
private String message; |
|||
private T data; |
|||
private String timestamp; |
|||
|
|||
// 构造方法
|
|||
public ApiResponse() { |
|||
this.timestamp = java.time.LocalDateTime.now().toString(); |
|||
} |
|||
|
|||
public ApiResponse(boolean success, String message, T data) { |
|||
this.success = success; |
|||
this.message = message; |
|||
this.data = data; |
|||
this.timestamp = java.time.LocalDateTime.now().toString(); |
|||
} |
|||
|
|||
// 静态工厂方法
|
|||
public static <T> ApiResponse<T> success(T data) { |
|||
return new ApiResponse<>(true, "操作成功", data); |
|||
} |
|||
|
|||
public static <T> ApiResponse<T> success(String message, T data) { |
|||
return new ApiResponse<>(true, message, data); |
|||
} |
|||
|
|||
public static <T> ApiResponse<T> error(String message) { |
|||
return new ApiResponse<>(false, message, null); |
|||
} |
|||
|
|||
public static <T> ApiResponse<T> error(String message, T data) { |
|||
return new ApiResponse<>(false, message, data); |
|||
} |
|||
|
|||
// Getter和Setter
|
|||
public boolean isSuccess() { |
|||
return success; |
|||
} |
|||
|
|||
public void setSuccess(boolean success) { |
|||
this.success = success; |
|||
} |
|||
|
|||
public String getMessage() { |
|||
return message; |
|||
} |
|||
|
|||
public void setMessage(String message) { |
|||
this.message = message; |
|||
} |
|||
|
|||
public T getData() { |
|||
return data; |
|||
} |
|||
|
|||
public void setData(T data) { |
|||
this.data = data; |
|||
} |
|||
|
|||
public String getTimestamp() { |
|||
return timestamp; |
|||
} |
|||
|
|||
public void setTimestamp(String timestamp) { |
|||
this.timestamp = timestamp; |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
package com.example.fileservice.dto; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
import java.text.DecimalFormat; |
|||
|
|||
public class FileInfo { |
|||
private String path; |
|||
private String name; |
|||
private long size; |
|||
private String formattedSize; |
|||
|
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
|||
private java.util.Date lastModified; |
|||
|
|||
private boolean isDirectory; |
|||
private boolean isReadable; |
|||
private boolean isWritable; |
|||
private boolean isHidden; |
|||
private String extension; |
|||
|
|||
// 默认构造方法
|
|||
public FileInfo() { |
|||
} |
|||
|
|||
// 从File对象构造
|
|||
public FileInfo(java.io.File file) { |
|||
this.path = file.getAbsolutePath(); |
|||
this.name = file.getName(); |
|||
this.size = file.length(); |
|||
this.formattedSize = formatSize(file.length()); |
|||
this.lastModified = new java.util.Date(file.lastModified()); |
|||
this.isDirectory = file.isDirectory(); |
|||
this.isReadable = file.canRead(); |
|||
this.isWritable = file.canWrite(); |
|||
this.isHidden = file.isHidden(); |
|||
|
|||
// 提取文件扩展名
|
|||
int dotIndex = file.getName().lastIndexOf('.'); |
|||
if (dotIndex > 0 && dotIndex < file.getName().length() - 1) { |
|||
this.extension = file.getName().substring(dotIndex + 1); |
|||
} else { |
|||
this.extension = ""; |
|||
} |
|||
} |
|||
|
|||
// 格式化文件大小
|
|||
private String formatSize(long size) { |
|||
if (size <= 0) return "0 B"; |
|||
final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" }; |
|||
int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); |
|||
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; |
|||
} |
|||
|
|||
// Getter和Setter
|
|||
public String getPath() { |
|||
return path; |
|||
} |
|||
|
|||
public void setPath(String path) { |
|||
this.path = path; |
|||
} |
|||
|
|||
public String getName() { |
|||
return name; |
|||
} |
|||
|
|||
public void setName(String name) { |
|||
this.name = name; |
|||
} |
|||
|
|||
public long getSize() { |
|||
return size; |
|||
} |
|||
|
|||
public void setSize(long size) { |
|||
this.size = size; |
|||
this.formattedSize = formatSize(size); |
|||
} |
|||
|
|||
public String getFormattedSize() { |
|||
return formattedSize; |
|||
} |
|||
|
|||
public java.util.Date getLastModified() { |
|||
return lastModified; |
|||
} |
|||
|
|||
public void setLastModified(java.util.Date lastModified) { |
|||
this.lastModified = lastModified; |
|||
} |
|||
|
|||
public boolean isDirectory() { |
|||
return isDirectory; |
|||
} |
|||
|
|||
public void setDirectory(boolean directory) { |
|||
isDirectory = directory; |
|||
} |
|||
|
|||
public boolean isReadable() { |
|||
return isReadable; |
|||
} |
|||
|
|||
public void setReadable(boolean readable) { |
|||
isReadable = readable; |
|||
} |
|||
|
|||
public boolean isWritable() { |
|||
return isWritable; |
|||
} |
|||
|
|||
public void setWritable(boolean writable) { |
|||
isWritable = writable; |
|||
} |
|||
|
|||
public boolean isHidden() { |
|||
return isHidden; |
|||
} |
|||
|
|||
public void setHidden(boolean hidden) { |
|||
isHidden = hidden; |
|||
} |
|||
|
|||
public String getExtension() { |
|||
return extension; |
|||
} |
|||
|
|||
public void setExtension(String extension) { |
|||
this.extension = extension; |
|||
} |
|||
} |
|||
@ -0,0 +1,335 @@ |
|||
package com.example.fileservice.service; |
|||
|
|||
import com.example.fileservice.dto.FileInfo; |
|||
import org.springframework.stereotype.Service; |
|||
import java.io.IOException; |
|||
import java.nio.file.*; |
|||
import java.nio.file.attribute.BasicFileAttributes; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
@Service |
|||
public class FileService { |
|||
|
|||
// 默认最大搜索深度
|
|||
private static final int DEFAULT_MAX_DEPTH = 5; |
|||
|
|||
/** |
|||
* 根据文件名查找文件(支持通配符) |
|||
*/ |
|||
public List<String> findFiles(String searchPath, String fileName) { |
|||
return findFiles(searchPath, fileName, DEFAULT_MAX_DEPTH); |
|||
} |
|||
|
|||
/** |
|||
* 根据文件名查找文件(指定最大深度) |
|||
*/ |
|||
public List<String> findFiles(String searchPath, String fileName, int maxDepth) { |
|||
List<String> foundFiles = new ArrayList<>(); |
|||
|
|||
try { |
|||
Path startPath = Paths.get(searchPath); |
|||
|
|||
// 验证路径
|
|||
validatePath(startPath); |
|||
|
|||
// 创建文件查找器
|
|||
FileFinder finder = new FileFinder(fileName, foundFiles, maxDepth); |
|||
Files.walkFileTree(startPath, finder); |
|||
|
|||
} catch (IOException e) { |
|||
throw new RuntimeException("搜索文件时发生IO错误: " + e.getMessage(), e); |
|||
} catch (InvalidPathException e) { |
|||
throw new IllegalArgumentException("无效的路径格式: " + searchPath, e); |
|||
} |
|||
|
|||
return foundFiles; |
|||
} |
|||
|
|||
/** |
|||
* 查找文件并返回详细信息 |
|||
*/ |
|||
public List<FileInfo> findFilesWithInfo(String searchPath, String fileName) { |
|||
List<FileInfo> foundFiles = new ArrayList<>(); |
|||
|
|||
try { |
|||
Path startPath = Paths.get(searchPath); |
|||
validatePath(startPath); |
|||
|
|||
// 创建文件查找器(返回详细信息)
|
|||
FileInfoFinder finder = new FileInfoFinder(fileName, foundFiles, DEFAULT_MAX_DEPTH); |
|||
Files.walkFileTree(startPath, finder); |
|||
|
|||
} catch (Exception e) { |
|||
throw new RuntimeException("搜索文件时发生错误: " + e.getMessage(), e); |
|||
} |
|||
|
|||
return foundFiles; |
|||
} |
|||
|
|||
/** |
|||
* 获取文件详细信息 |
|||
*/ |
|||
public FileInfo getFileInfo(String filePath) { |
|||
java.io.File file = new java.io.File(filePath); |
|||
|
|||
if (!file.exists()) { |
|||
throw new IllegalArgumentException("文件或目录不存在: " + filePath); |
|||
} |
|||
|
|||
return new FileInfo(file); |
|||
} |
|||
|
|||
/** |
|||
* 获取目录内容列表 |
|||
*/ |
|||
public List<FileInfo> listDirectory(String dirPath) { |
|||
return listDirectory(dirPath, false); |
|||
} |
|||
|
|||
/** |
|||
* 获取目录内容列表(可选是否包含子目录) |
|||
*/ |
|||
public List<FileInfo> listDirectory(String dirPath, boolean includeSubdirectories) { |
|||
java.io.File directory = new java.io.File(dirPath); |
|||
|
|||
if (!directory.exists()) { |
|||
throw new IllegalArgumentException("目录不存在: " + dirPath); |
|||
} |
|||
|
|||
if (!directory.isDirectory()) { |
|||
throw new IllegalArgumentException("路径不是目录: " + dirPath); |
|||
} |
|||
|
|||
List<FileInfo> fileList = new ArrayList<>(); |
|||
java.io.File[] files = directory.listFiles(); |
|||
|
|||
if (files != null) { |
|||
for (java.io.File file : files) { |
|||
fileList.add(new FileInfo(file)); |
|||
} |
|||
} |
|||
|
|||
return fileList; |
|||
} |
|||
|
|||
/** |
|||
* 统计目录信息 |
|||
*/ |
|||
public DirectoryStats getDirectoryStats(String dirPath) { |
|||
java.io.File directory = new java.io.File(dirPath); |
|||
|
|||
if (!directory.exists() || !directory.isDirectory()) { |
|||
throw new IllegalArgumentException("目录不存在或不是有效目录: " + dirPath); |
|||
} |
|||
|
|||
DirectoryStats stats = new DirectoryStats(); |
|||
stats.setPath(dirPath); |
|||
|
|||
try { |
|||
AtomicInteger fileCount = new AtomicInteger(0); |
|||
AtomicInteger dirCount = new AtomicInteger(0); |
|||
AtomicInteger totalSize = new AtomicInteger(0); |
|||
|
|||
Files.walkFileTree(Paths.get(dirPath), new SimpleFileVisitor<Path>() { |
|||
@Override |
|||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { |
|||
fileCount.incrementAndGet(); |
|||
totalSize.addAndGet((int) attrs.size()); |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { |
|||
if (!dir.toString().equals(dirPath)) { |
|||
dirCount.incrementAndGet(); |
|||
} |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult visitFileFailed(Path file, IOException exc) { |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
}); |
|||
|
|||
stats.setFileCount(fileCount.get()); |
|||
stats.setDirectoryCount(dirCount.get()); |
|||
stats.setTotalSize(totalSize.get()); |
|||
stats.setFormattedTotalSize(formatSize(totalSize.get())); |
|||
|
|||
} catch (IOException e) { |
|||
throw new RuntimeException("统计目录信息失败: " + e.getMessage(), e); |
|||
} |
|||
|
|||
return stats; |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 格式化文件大小 |
|||
*/ |
|||
private String formatSize(long size) { |
|||
if (size < 1024) return size + " B"; |
|||
if (size < 1024 * 1024) return String.format("%.1f KB", size / 1024.0); |
|||
if (size < 1024 * 1024 * 1024) return String.format("%.1f MB", size / (1024.0 * 1024.0)); |
|||
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); |
|||
} |
|||
|
|||
/** |
|||
* 文件查找器(返回路径) |
|||
*/ |
|||
private static class FileFinder extends SimpleFileVisitor<Path> { |
|||
private final PathMatcher matcher; |
|||
private final List<String> foundFiles; |
|||
private final int maxDepth; |
|||
private int currentDepth = 0; |
|||
|
|||
FileFinder(String pattern, List<String> foundFiles, int maxDepth) { |
|||
this.matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); |
|||
this.foundFiles = foundFiles; |
|||
this.maxDepth = maxDepth; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { |
|||
currentDepth++; |
|||
if (currentDepth > maxDepth) { |
|||
currentDepth--; |
|||
return FileVisitResult.SKIP_SUBTREE; |
|||
} |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) { |
|||
currentDepth--; |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { |
|||
Path name = file.getFileName(); |
|||
if (name != null && matcher.matches(name)) { |
|||
foundFiles.add(file.toAbsolutePath().toString()); |
|||
} |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult visitFileFailed(Path file, IOException exc) { |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 文件查找器(返回详细信息) |
|||
*/ |
|||
private static class FileInfoFinder extends SimpleFileVisitor<Path> { |
|||
private final PathMatcher matcher; |
|||
private final List<FileInfo> foundFiles; |
|||
private final int maxDepth; |
|||
private int currentDepth = 0; |
|||
|
|||
FileInfoFinder(String pattern, List<FileInfo> foundFiles, int maxDepth) { |
|||
this.matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); |
|||
this.foundFiles = foundFiles; |
|||
this.maxDepth = maxDepth; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { |
|||
currentDepth++; |
|||
if (currentDepth > maxDepth) { |
|||
currentDepth--; |
|||
return FileVisitResult.SKIP_SUBTREE; |
|||
} |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) { |
|||
currentDepth--; |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { |
|||
Path name = file.getFileName(); |
|||
if (name != null && matcher.matches(name)) { |
|||
foundFiles.add(new FileInfo(file.toFile())); |
|||
} |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
|
|||
@Override |
|||
public FileVisitResult visitFileFailed(Path file, IOException exc) { |
|||
return FileVisitResult.CONTINUE; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 目录统计信息类 |
|||
*/ |
|||
public static class DirectoryStats { |
|||
private String path; |
|||
private int fileCount; |
|||
private int directoryCount; |
|||
private long totalSize; |
|||
private String formattedTotalSize; |
|||
|
|||
// Getter和Setter
|
|||
public String getPath() { return path; } |
|||
public void setPath(String path) { this.path = path; } |
|||
|
|||
public int getFileCount() { return fileCount; } |
|||
public void setFileCount(int fileCount) { this.fileCount = fileCount; } |
|||
|
|||
public int getDirectoryCount() { return directoryCount; } |
|||
public void setDirectoryCount(int directoryCount) { this.directoryCount = directoryCount; } |
|||
|
|||
public long getTotalSize() { return totalSize; } |
|||
public void setTotalSize(long totalSize) { this.totalSize = totalSize; } |
|||
|
|||
public String getFormattedTotalSize() { return formattedTotalSize; } |
|||
public void setFormattedTotalSize(String formattedTotalSize) { this.formattedTotalSize = formattedTotalSize; } |
|||
} |
|||
|
|||
/** |
|||
* 查找单个文件(严格匹配,不支持通配符) |
|||
*/ |
|||
public String findSingleFile(String searchPath, String exactFileName) { |
|||
Path startPath = Paths.get(searchPath); |
|||
|
|||
// 验证路径
|
|||
validatePath(startPath); |
|||
|
|||
// 直接构建完整路径
|
|||
Path filePath = startPath.resolve(exactFileName); |
|||
|
|||
if (Files.exists(filePath) && Files.isRegularFile(filePath)) { |
|||
return filePath.toAbsolutePath().toString(); |
|||
} else { |
|||
throw new IllegalArgumentException("文件不存在: " + filePath); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证路径 |
|||
*/ |
|||
private void validatePath(Path path) { |
|||
if (!Files.exists(path)) { |
|||
throw new IllegalArgumentException("路径不存在: " + path.toAbsolutePath()); |
|||
} |
|||
|
|||
if (!Files.isDirectory(path)) { |
|||
throw new IllegalArgumentException("路径不是目录: " + path.toAbsolutePath()); |
|||
} |
|||
|
|||
if (!Files.isReadable(path)) { |
|||
throw new IllegalArgumentException("路径不可读: " + path.toAbsolutePath()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue