From 32ed3114d4753cf837fcf1c95e6ad177780565c4 Mon Sep 17 00:00:00 2001 From: liwenxuan <1298531568@qq.com> Date: Wed, 24 Dec 2025 13:07:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 132 ++++ pom.xml | 106 +++ .../fileservice/FileServiceApplication.java | 19 + .../controller/FileController.java | 687 ++++++++++++++++++ .../controller/GlobalExceptionHandler.java | 78 ++ .../controller/HealthController.java | 59 ++ .../example/fileservice/dto/ApiResponse.java | 73 ++ .../com/example/fileservice/dto/FileInfo.java | 131 ++++ .../fileservice/service/FileService.java | 335 +++++++++ 9 files changed, 1620 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/example/fileservice/FileServiceApplication.java create mode 100644 src/main/java/com/example/fileservice/controller/FileController.java create mode 100644 src/main/java/com/example/fileservice/controller/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/fileservice/controller/HealthController.java create mode 100644 src/main/java/com/example/fileservice/dto/ApiResponse.java create mode 100644 src/main/java/com/example/fileservice/dto/FileInfo.java create mode 100644 src/main/java/com/example/fileservice/service/FileService.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cb0c93 --- /dev/null +++ b/.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 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..29ffa5c --- /dev/null +++ b/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.example + file-search-service + 1.0.0 + war + File Search Service + Windows Server文件搜索服务 + + + 1.8 + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + file-search-service + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/FileServiceApplication.java b/src/main/java/com/example/fileservice/FileServiceApplication.java new file mode 100644 index 0000000..f1e799b --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/controller/FileController.java b/src/main/java/com/example/fileservice/controller/FileController.java new file mode 100644 index 0000000..380d440 --- /dev/null +++ b/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>> findFiles( + @RequestParam("path") String searchPath, + @RequestParam("fileName") String fileName) { + + try { + List 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>> 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 foundFiles = fileService.findFilesWithInfo(searchPath, fileName); + + // 方法2: 使用 findFiles 获取路径,然后转换为 FileInfo + List 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> 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>> listDirectory( + @RequestParam("path") String dirPath) { + + try { + List 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> 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>> 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 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>> getSystemInfo() { + try { + Map 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> disks = new java.util.ArrayList<>(); + + for (java.io.File root : roots) { + Map 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 downloadFile( + @RequestParam("path") String searchPath, + @RequestParam("fileName") String fileName, + HttpServletResponse response) { + + try { + // 查找匹配的文件 + List 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 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 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 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 downloadMultipleFiles( + @Valid @RequestBody BatchDownloadRequest request) { + + try { + // 查找匹配的文件 + List 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; } + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/controller/GlobalExceptionHandler.java b/src/main/java/com/example/fileservice/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..ec74b02 --- /dev/null +++ b/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>> handleValidationExceptions( + MethodArgumentNotValidException ex) { + + Map 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> 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> 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> handleGeneralException( + Exception ex, HttpServletRequest request) { + + logger.error("未处理异常: {} - {}", request.getRequestURI(), ex.getMessage(), ex); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("系统繁忙,请稍后重试")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/controller/HealthController.java b/src/main/java/com/example/fileservice/controller/HealthController.java new file mode 100644 index 0000000..50f628f --- /dev/null +++ b/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> healthCheck() { + Map 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 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)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/dto/ApiResponse.java b/src/main/java/com/example/fileservice/dto/ApiResponse.java new file mode 100644 index 0000000..02a5cd7 --- /dev/null +++ b/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 { + 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 ApiResponse success(T data) { + return new ApiResponse<>(true, "操作成功", data); + } + + public static ApiResponse success(String message, T data) { + return new ApiResponse<>(true, message, data); + } + + public static ApiResponse error(String message) { + return new ApiResponse<>(false, message, null); + } + + public static ApiResponse 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/dto/FileInfo.java b/src/main/java/com/example/fileservice/dto/FileInfo.java new file mode 100644 index 0000000..ca55fd3 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/fileservice/service/FileService.java b/src/main/java/com/example/fileservice/service/FileService.java new file mode 100644 index 0000000..4c4755e --- /dev/null +++ b/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 findFiles(String searchPath, String fileName) { + return findFiles(searchPath, fileName, DEFAULT_MAX_DEPTH); + } + + /** + * 根据文件名查找文件(指定最大深度) + */ + public List findFiles(String searchPath, String fileName, int maxDepth) { + List 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 findFilesWithInfo(String searchPath, String fileName) { + List 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 listDirectory(String dirPath) { + return listDirectory(dirPath, false); + } + + /** + * 获取目录内容列表(可选是否包含子目录) + */ + public List 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 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() { + @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 { + private final PathMatcher matcher; + private final List foundFiles; + private final int maxDepth; + private int currentDepth = 0; + + FileFinder(String pattern, List 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 { + private final PathMatcher matcher; + private final List foundFiles; + private final int maxDepth; + private int currentDepth = 0; + + FileInfoFinder(String pattern, List 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()); + } + } +} \ No newline at end of file