package com.trafficaudit.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.io.IOException; import java.net.URLEncoder; /** * 数据导入页「下载模板」:/api/templates/** -> docs 目录下的模板文件。 * 用 Controller 而非静态资源映射,便于设置中文文件名的下载头并防路径穿越。 */ @RestController @RequestMapping("/api/templates") public class TemplateDownloadController { @Value("${docs.template-root:docs}") private String templateRoot; @GetMapping("/{*path}") public ResponseEntity download(@PathVariable String path) throws IOException { File file = resolveTemplateFile(path); if (file == null) { return ResponseEntity.notFound().build(); } String encoded = URLEncoder.encode(file.getName(), "UTF-8").replace("+", "%20"); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) .contentType(MediaType.APPLICATION_OCTET_STREAM) .contentLength(file.length()) .body(new FileSystemResource(file)); } /** 模板定位:配置目录 → user.dir → user.dir/.. 逐级回退(与报表模块一致),并防路径穿越 */ private File resolveTemplateFile(String rel) throws IOException { String r = rel; while (r.startsWith("./")) r = r.substring(2); if (r.contains("..")) { return null; } String[] roots = { templateRoot, System.getProperty("user.dir") + "/" + templateRoot, System.getProperty("user.dir") + "/../" + templateRoot }; for (String root : roots) { if (root == null || root.trim().isEmpty()) continue; File f = new File(root, r); if (f.exists() && f.isFile()) { return f.getCanonicalFile(); } } return null; } }