fix(file): 校验企业资质实际存储目录

This commit is contained in:
2026-08-01 10:10:52 +08:00
parent 564ac8b320
commit f2e8935dbc
2 changed files with 43 additions and 15 deletions
@@ -27,31 +27,25 @@ public class LocalQualificationStorage {
private static final long MAX_SIZE = 5L * 1024 * 1024;
private static final Set<String> TYPES = Set.of("image/jpeg", "image/png");
private final Path root;
private final Path qualificationRoot;
public LocalQualificationStorage(@Value("${baiyue.file.storage-root}") String root) {
this.root = Path.of(root).toAbsolutePath().normalize();
this.qualificationRoot = this.root.resolve("qualifications").normalize();
}
/** 服务启动时校验存储根目录,避免健康检查正常但首次上传才暴露挂载或权限问题。 */
@PostConstruct
public void initialize() {
Path probe = null;
try {
Files.createDirectories(root);
if (!Files.isDirectory(root) || !Files.isWritable(root)) {
Files.createDirectories(qualificationRoot);
if (!qualificationRoot.startsWith(root)
|| !Files.isDirectory(qualificationRoot) || !Files.isWritable(qualificationRoot)) {
throw new IOException("存储根目录不可写");
}
probe = root.resolve(".qualification-storage-probe-" + UUID.randomUUID());
Files.write(probe, new byte[] {0x42}, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
Files.delete(probe);
verifyWriteAccess();
} catch (IOException exception) {
if (probe != null) {
try {
Files.deleteIfExists(probe);
} catch (IOException cleanupException) {
exception.addSuppressed(cleanupException);
}
}
LOGGER.error("企业资质存储根目录不可用:{}", root, exception);
throw new IllegalStateException("企业资质存储根目录不可用", exception);
}
@@ -67,7 +61,7 @@ public class LocalQualificationStorage {
}
String extension = "image/png".equals(contentType) ? ".png" : ".jpg";
String fileName = UUID.randomUUID() + extension;
Path directory = root.resolve("qualifications").resolve(Long.toString(tenantId)).normalize();
Path directory = tenantDirectory(tenantId);
Path target = directory.resolve(fileName).normalize();
if (!target.startsWith(directory)) throw new BizException("Invalid upload path");
try {
@@ -106,7 +100,7 @@ public class LocalQualificationStorage {
public Resource load(long tenantId, String fileName) {
if (!fileName.matches("[0-9a-fA-F-]{36}\\.(jpg|png)")) throw new BizException("Invalid file name");
Path directory = root.resolve("qualifications").resolve(Long.toString(tenantId)).normalize();
Path directory = tenantDirectory(tenantId);
Path target = directory.resolve(fileName).normalize();
if (!target.startsWith(directory) || !Files.isRegularFile(target)) throw new BizException("File not found");
try { return new UrlResource(target.toUri()); }
@@ -117,5 +111,26 @@ public class LocalQualificationStorage {
try { Files.deleteIfExists(file.path()); } catch (IOException ignored) { }
}
private Path tenantDirectory(long tenantId) {
Path directory = qualificationRoot.resolve(Long.toString(tenantId)).normalize();
if (!directory.startsWith(qualificationRoot)) {
throw new BizException("Invalid upload path");
}
return directory;
}
/** 验证实际挂载目录可创建和删除文件,避免仅凭 isWritable 掩盖 Docker bind mount 权限或只读问题。 */
private void verifyWriteAccess() throws IOException {
Path probe = Files.createTempFile(qualificationRoot, ".file-service-probe-", ".tmp");
try {
Files.delete(probe);
} catch (IOException exception) {
try { Files.deleteIfExists(probe); } catch (IOException cleanupException) {
exception.addSuppressed(cleanupException);
}
throw exception;
}
}
public record StoredFile(Path path, String fileName, String documentUrl) { }
}
@@ -20,6 +20,19 @@ class LocalQualificationStorageTest {
@TempDir
Path storageRoot;
@Test
void initializeVerifiesTheActualQualificationMountAndCleansUpItsProbe() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
Path qualificationRoot = storageRoot.resolve("qualifications");
assertTrue(Files.isDirectory(qualificationRoot));
try (var files = Files.list(qualificationRoot)) {
assertEquals(0, files.count());
}
}
@Test
void storeReturnsFileServiceUrlBackedByARealTenantFile() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
@@ -60,7 +73,7 @@ class LocalQualificationStorageTest {
void writeFailureReturnsServiceUnavailableInsteadOfBusinessError() throws Exception {
var storage = new LocalQualificationStorage(storageRoot.toString());
storage.initialize();
Files.writeString(storageRoot.resolve("qualifications"), "阻止创建租户目录");
Files.writeString(storageRoot.resolve("qualifications").resolve("10001"), "阻止创建租户目录");
byte[] png = {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x01};
var upload = new MockMultipartFile("file", "license.png", "image/png", png);