fix(file): 校验企业资质实际存储目录
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user