Browse Source

首次提交

master
liwenxuan 4 weeks ago
commit
32ed3114d4
  1. 132
      .gitignore
  2. 106
      pom.xml
  3. 19
      src/main/java/com/example/fileservice/FileServiceApplication.java
  4. 687
      src/main/java/com/example/fileservice/controller/FileController.java
  5. 78
      src/main/java/com/example/fileservice/controller/GlobalExceptionHandler.java
  6. 59
      src/main/java/com/example/fileservice/controller/HealthController.java
  7. 73
      src/main/java/com/example/fileservice/dto/ApiResponse.java
  8. 131
      src/main/java/com/example/fileservice/dto/FileInfo.java
  9. 335
      src/main/java/com/example/fileservice/service/FileService.java

132
.gitignore

@ -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

106
pom.xml

@ -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>

19
src/main/java/com/example/fileservice/FileServiceApplication.java

@ -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);
}
}

687
src/main/java/com/example/fileservice/controller/FileController.java

@ -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; }
}
}

78
src/main/java/com/example/fileservice/controller/GlobalExceptionHandler.java

@ -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("系统繁忙,请稍后重试"));
}
}

59
src/main/java/com/example/fileservice/controller/HealthController.java

@ -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));
}
}

73
src/main/java/com/example/fileservice/dto/ApiResponse.java

@ -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;
}
}

131
src/main/java/com/example/fileservice/dto/FileInfo.java

@ -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;
}
}

335
src/main/java/com/example/fileservice/service/FileService.java

@ -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…
Cancel
Save