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