task-33: 将店铺源文件 key 映射改为确定路径,取消临时目录递归扫描
This commit is contained in:
+15
-2
@@ -96,13 +96,26 @@ public class LocalFileStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺源文件 key → 确定路径解析:saveTempFile 始终把源文件平铺写入
|
||||
* localTempDir/<fileKey>[.<ext>],因此这里只列举临时目录根层(非递归),
|
||||
* 匹配 name == fileKey 或 fileKey.<ext> 的直接子文件,
|
||||
* 取代原 FileUtil.loopFiles 对整棵临时目录树的递归前缀扫描。
|
||||
*/
|
||||
public File findLocalSourceFile(String fileKey) {
|
||||
if (fileKey == null || fileKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
File baseDir = FileUtil.file(storageProperties.getLocalTempDir());
|
||||
if (!baseDir.exists()) {
|
||||
return null;
|
||||
}
|
||||
List<File> matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey));
|
||||
return matchedFiles.isEmpty() ? null : matchedFiles.getFirst();
|
||||
File[] matchedFiles = baseDir.listFiles(pathname -> pathname.isFile()
|
||||
&& (pathname.getName().equals(fileKey) || pathname.getName().startsWith(fileKey + ".")));
|
||||
if (matchedFiles == null) {
|
||||
return null;
|
||||
}
|
||||
return matchedFiles.length == 0 ? null : matchedFiles[0];
|
||||
}
|
||||
|
||||
private String normalizeCellText(String value) {
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import com.nanri.aiimage.config.StorageProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 33:将店铺源文件 key 映射改为确定路径,取消临时目录递归扫描。
|
||||
* saveTempFile 始终把源文件平铺写入 localTempDir/<fileKey>[.<ext>],
|
||||
* 原 findLocalSourceFile 却用 FileUtil.loopFiles 对临时目录递归扫描匹配前缀;
|
||||
* 实现改为确定路径解析:只扫描临时目录根层(非递归),
|
||||
* 子目录中的同名文件不属于 key 映射,不再被递归命中。
|
||||
*/
|
||||
class LocalFileStorageServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private StorageProperties storageProperties;
|
||||
private LocalFileStorageService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
storageProperties = new StorageProperties();
|
||||
storageProperties.setLocalTempDir(tempDir.toString());
|
||||
service = new LocalFileStorageService(storageProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_normal_default_path() {
|
||||
// 正常路径:平铺写入的源文件按 key 解析到确定路径,返回真实文件。
|
||||
File source = writeSourceFile("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "xlsx", "sheet1");
|
||||
|
||||
File resolved = service.findLocalSourceFile("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6");
|
||||
|
||||
assertNotNull(resolved, "确定路径解析到源文件");
|
||||
assertTrue(resolved.isFile());
|
||||
assertEquals(source.getAbsolutePath(), resolved.getAbsolutePath(), "路径与平铺写入一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_normal_multiple_items() {
|
||||
// 批量场景:多个源文件 key 各自解析到自己的文件,互不串扰。
|
||||
for (int i = 0; i < 5; i++) {
|
||||
writeSourceFile(key(i), "xlsx", "content-" + i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
File resolved = service.findLocalSourceFile(key(i));
|
||||
assertNotNull(resolved, "key-" + i + " 可解析");
|
||||
assertEquals("content-" + i, readFile(resolved), "key-" + i + " 内容正确");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:同一 key 重复解析返回同一文件,结果稳定。
|
||||
writeSourceFile(key(0), "xlsx", "data");
|
||||
|
||||
File first = service.findLocalSourceFile(key(0));
|
||||
File second = service.findLocalSourceFile(key(0));
|
||||
File third = service.findLocalSourceFile(key(0));
|
||||
|
||||
assertNotNull(first);
|
||||
assertEquals(first.getAbsolutePath(), second.getAbsolutePath(), "重复解析路径一致");
|
||||
assertEquals(first.getAbsolutePath(), third.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_boundary_empty_input() {
|
||||
// 空输入:临时目录为空时返回 null;子目录中的同名文件不属于 key 映射(非递归)。
|
||||
File subDir = new File(tempDir.toFile(), "sub");
|
||||
assertTrue(subDir.mkdirs());
|
||||
writeSourceFileInto(subDir, key(1), "csv", "decoy");
|
||||
|
||||
assertNull(service.findLocalSourceFile(key(1)), "子目录文件不参与确定路径映射");
|
||||
assertNull(service.findLocalSourceFile(key(2)), "不存在的 key 返回 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_boundary_single_item() {
|
||||
// 单元素:单个源文件解析正确,不依赖批量路径。
|
||||
writeSourceFile(key(0), "csv", "single");
|
||||
|
||||
File resolved = service.findLocalSourceFile(key(0));
|
||||
assertNotNull(resolved);
|
||||
assertEquals("single", readFile(resolved));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_boundary_limit_and_overflow() {
|
||||
// 上限/超限:目录内大量文件时目标 key 仍解析正确,无内存无界增长。
|
||||
for (int i = 0; i < 300; i++) {
|
||||
writeSourceFile(key(i), "xlsx", "bulk-" + i);
|
||||
}
|
||||
|
||||
for (int probe : new int[]{0, 150, 299}) {
|
||||
File resolved = service.findLocalSourceFile(key(probe));
|
||||
assertNotNull(resolved, "大量文件中 key-" + probe + " 仍可解析");
|
||||
assertEquals("bulk-" + probe, readFile(resolved));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_invalid_input_rejected() {
|
||||
// 非法参数:null/空白 key 安全返回 null;含路径分隔符的 key 被拒绝,防止路径穿越。
|
||||
assertNull(service.findLocalSourceFile(null), "null key 安全返回 null");
|
||||
assertNull(service.findLocalSourceFile(" "), "空白 key 安全返回 null");
|
||||
assertNull(service.findLocalSourceFile("../../etc/passwd"), "路径穿越 key 被拒绝");
|
||||
assertNull(service.findLocalSourceFile("sub/" + key(0)), "含分隔符 key 被拒绝");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_033_task_dependency_failure_releases_resources() {
|
||||
// 依赖失败:临时目录不存在时安全返回 null 不抛异常;文件被清理后解析返回 null。
|
||||
StorageProperties missing = new StorageProperties();
|
||||
missing.setLocalTempDir(tempDir.resolve("not-exists").toString());
|
||||
LocalFileStorageService missingDirService = new LocalFileStorageService(missing);
|
||||
assertNull(missingDirService.findLocalSourceFile(key(0)), "目录缺失返回 null 不抛异常");
|
||||
|
||||
File source = writeSourceFile(key(0), "xlsx", "temp");
|
||||
assertNotNull(service.findLocalSourceFile(key(0)));
|
||||
assertTrue(source.delete());
|
||||
assertNull(service.findLocalSourceFile(key(0)), "文件清理后解析返回 null");
|
||||
}
|
||||
|
||||
private static String key(int index) {
|
||||
return String.format("%032d", index);
|
||||
}
|
||||
|
||||
private File writeSourceFile(String fileKey, String ext, String content) {
|
||||
return writeSourceFileInto(tempDir.toFile(), fileKey, ext, content);
|
||||
}
|
||||
|
||||
private File writeSourceFileInto(File dir, String fileKey, String ext, String content) {
|
||||
File file = FileUtil.file(dir, fileKey + "." + ext);
|
||||
FileUtil.writeUtf8String(content, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
private String readFile(File file) {
|
||||
try {
|
||||
return Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("读取测试文件失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user