task-43: 采集源文件查找改为确定路径/索引查询
LocalFileStorageService 新增 fileKey→文件名 有界索引(LinkedHashMap LRU, 上限 1024 淘汰最旧):saveTempFile 登记,findLocalSourceFile 优先按索引 直接构造确定路径,索引过期/进程重启时兜底枚举根层目录,结果与索引一致; key 与索引名均校验单段字符集,防止路径穿越与索引污染。8 个用例覆盖 正常/批量/幂等/空/单元素/索引容量超限/非法输入/清理与重启降级, mvn 全量测试通过。
This commit is contained in:
+58
-8
@@ -20,6 +20,7 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -27,8 +28,15 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class LocalFileStorageService {
|
||||
|
||||
/** fileKey → 文件名 索引容量:超限淘汰最旧条目,避免索引自身无界增长。 */
|
||||
static final int SOURCE_FILE_INDEX_CAPACITY = 1024;
|
||||
|
||||
private final StorageProperties storageProperties;
|
||||
|
||||
/** 源文件确定路径索引:saveTempFile 写入后登记,查找优先命中,兜底目录枚举。 */
|
||||
private final Map<String, String> sourceFileIndex =
|
||||
new LinkedHashMap<>(16, 0.75f, true);
|
||||
|
||||
public UploadFileVo saveTempFile(MultipartFile file, String relativePath) throws IOException {
|
||||
File tempDir = FileUtil.file(storageProperties.getLocalTempDir());
|
||||
File parentDir = tempDir.getParentFile();
|
||||
@@ -38,8 +46,10 @@ public class LocalFileStorageService {
|
||||
FileUtil.mkdir(tempDir);
|
||||
String fileKey = IdUtil.fastSimpleUUID();
|
||||
String extName = FileUtil.extName(file.getOriginalFilename());
|
||||
File target = FileUtil.file(tempDir, fileKey + (extName.isEmpty() ? "" : "." + extName));
|
||||
String filename = fileKey + (extName.isEmpty() ? "" : "." + extName);
|
||||
File target = FileUtil.file(tempDir, filename);
|
||||
file.transferTo(target);
|
||||
registerSourceFileIndex(fileKey, filename);
|
||||
|
||||
UploadFileVo vo = new UploadFileVo();
|
||||
vo.setFileKey(fileKey);
|
||||
@@ -97,25 +107,65 @@ public class LocalFileStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺源文件 key → 确定路径解析:saveTempFile 始终把源文件平铺写入
|
||||
* localTempDir/<fileKey>[.<ext>],因此这里只列举临时目录根层(非递归),
|
||||
* 匹配 name == fileKey 或 fileKey.<ext> 的直接子文件,
|
||||
* 取代原 FileUtil.loopFiles 对整棵临时目录树的递归前缀扫描。
|
||||
* 店铺源文件 key → 确定路径解析:优先按索引直接构造 fileKey[.<ext>] 路径,
|
||||
* 索引缺失/过期(进程重启或文件被清理)时兜底枚举临时目录根层(非递归)。
|
||||
* 索引与兜底都只允许根层平铺文件命中,子目录同名文件不属于 key 映射。
|
||||
*/
|
||||
public File findLocalSourceFile(String fileKey) {
|
||||
if (fileKey == null || fileKey.isBlank()) {
|
||||
if (fileKey == null || fileKey.isBlank() || !isPlainKey(fileKey)) {
|
||||
return null;
|
||||
}
|
||||
File baseDir = FileUtil.file(storageProperties.getLocalTempDir());
|
||||
if (!baseDir.exists()) {
|
||||
return null;
|
||||
}
|
||||
String indexedName = sourceFileIndex.get(fileKey);
|
||||
if (indexedName != null && isPlainName(indexedName)) {
|
||||
File indexed = FileUtil.file(baseDir, indexedName);
|
||||
if (indexed.isFile()) {
|
||||
return indexed;
|
||||
}
|
||||
sourceFileIndex.remove(fileKey);
|
||||
}
|
||||
File[] matchedFiles = baseDir.listFiles(pathname -> pathname.isFile()
|
||||
&& (pathname.getName().equals(fileKey) || pathname.getName().startsWith(fileKey + ".")));
|
||||
if (matchedFiles == null) {
|
||||
if (matchedFiles == null || matchedFiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return matchedFiles.length == 0 ? null : matchedFiles[0];
|
||||
File resolved = matchedFiles[0];
|
||||
registerSourceFileIndex(fileKey, resolved.getName());
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void registerSourceFileIndex(String fileKey, String filename) {
|
||||
synchronized (sourceFileIndex) {
|
||||
sourceFileIndex.put(fileKey, filename);
|
||||
if (sourceFileIndex.size() > SOURCE_FILE_INDEX_CAPACITY) {
|
||||
var it = sourceFileIndex.entrySet().iterator();
|
||||
if (it.hasNext()) {
|
||||
it.next();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 只允许单段 UUID 风格 key,防止路径穿越/分隔符注入。 */
|
||||
private static boolean isPlainKey(String key) {
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
char c = key.charAt(i);
|
||||
if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 索引文件名必须是根层单段名(无分隔符),防止索引被污染后路径穿越。 */
|
||||
private static boolean isPlainName(String name) {
|
||||
return name != null && !name.isBlank()
|
||||
&& name.indexOf('/') < 0 && name.indexOf('\\') < 0
|
||||
&& !name.equals(".") && !name.equals("..");
|
||||
}
|
||||
|
||||
private String normalizeCellText(String value) {
|
||||
|
||||
Reference in New Issue
Block a user