fix(aihr): sanitize multipart filenames before storage
This commit is contained in:
+5
-2
@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.AsrResponse;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsRequest;
|
||||
import org.dromara.aihr.domain.AihrSpeechDto.TtsResponse;
|
||||
import org.dromara.aihr.service.AihrMultipartFiles;
|
||||
import org.dromara.aihr.service.AihrSpeechService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.system.domain.vo.SysOssVo;
|
||||
@@ -52,10 +53,12 @@ public class AihrSpeechController {
|
||||
if (file.getSize() > MAX_ASR_BYTES) {
|
||||
return R.fail("音频过大(上限 5MB),请缩短录音时长");
|
||||
}
|
||||
return speechService.transcribe(file.getBytes(), file.getOriginalFilename(), file.getContentType())
|
||||
String fileName = AihrMultipartFiles.sanitizeFileName(file.getOriginalFilename(), "audio.webm");
|
||||
MultipartFile sanitizedFile = AihrMultipartFiles.withOriginalFilename(file, fileName);
|
||||
return speechService.transcribe(sanitizedFile.getBytes(), fileName, sanitizedFile.getContentType())
|
||||
.map(text -> {
|
||||
try {
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
SysOssVo oss = ossService.upload(sanitizedFile);
|
||||
return R.ok(new AsrResponse(text, "openai-compatible", oss.getOssId(), oss.getUrl()));
|
||||
} catch (Exception e) {
|
||||
log.warn("asr audio upload failed(处理错误已隐藏)");
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ public class AihrCandidateMaterialService {
|
||||
throw new IllegalArgumentException("候选人资料仅支持 PDF、Word 或 JPG/PNG/WebP 图片");
|
||||
}
|
||||
String type = clean(materialType, materialType(fileName));
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
SysOssVo oss = ossService.upload(AihrMultipartFiles.withOriginalFilename(file, fileName));
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement("""
|
||||
|
||||
+3
-3
@@ -67,7 +67,7 @@ public class AihrCaseService {
|
||||
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
||||
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
|
||||
transcript = maskSensitiveText(transcript);
|
||||
MediaRef media = storeSourceAudio(file);
|
||||
MediaRef media = storeSourceAudio(file, fileName);
|
||||
String caseId = "case-" + UUID.randomUUID();
|
||||
CaseState state = new CaseState(caseId, fileName, scopedProject, transcript, tagsFromText(transcript), null,
|
||||
media.ossId(), media.url(), currentCaseOwner());
|
||||
@@ -440,12 +440,12 @@ public class AihrCaseService {
|
||||
return String.join(",", Collections.nCopies(size, "?"));
|
||||
}
|
||||
|
||||
private MediaRef storeSourceAudio(MultipartFile file) {
|
||||
private MediaRef storeSourceAudio(MultipartFile file, String fileName) {
|
||||
// Unit tests can construct this service without the system OSS bean; production always wires it.
|
||||
if (ossService == null) {
|
||||
return new MediaRef(null, "");
|
||||
}
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
SysOssVo oss = ossService.upload(AihrMultipartFiles.withOriginalFilename(file, fileName));
|
||||
return new MediaRef(oss.getOssId(), firstNonBlank(oss.getUrl(), ""));
|
||||
}
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Multipart 文件边界工具:清理后的文件名必须继续贯穿到 OSS 元数据和外部请求。
|
||||
*/
|
||||
public final class AihrMultipartFiles {
|
||||
|
||||
private AihrMultipartFiles() {
|
||||
}
|
||||
|
||||
public static MultipartFile withOriginalFilename(MultipartFile delegate, String originalFilename) {
|
||||
if (delegate == null) {
|
||||
throw new IllegalArgumentException("文件不能为空");
|
||||
}
|
||||
String safeName = sanitizeFileName(originalFilename, "upload.bin");
|
||||
return new MultipartFile() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return safeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return delegate.getContentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return delegate.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return delegate.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
return delegate.getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return delegate.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(File dest) throws IOException, IllegalStateException {
|
||||
delegate.transferTo(dest);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static String sanitizeFileName(String originalFilename, String fallback) {
|
||||
String name = originalFilename == null || originalFilename.isBlank()
|
||||
? fallback
|
||||
: originalFilename.trim();
|
||||
name = name.replaceAll("[\\\\/\\r\\n\\t]", "_");
|
||||
if (name.length() > 200) {
|
||||
name = name.substring(name.length() - 200);
|
||||
}
|
||||
return name.isBlank() ? fallback : name;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -389,7 +389,7 @@ public class AihrSopSeedService {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
String fileName = Optional.ofNullable(file.getOriginalFilename()).orElse("knowledge.txt").trim();
|
||||
String fileName = AihrMultipartFiles.sanitizeFileName(file.getOriginalFilename(), "knowledge.txt");
|
||||
if (!supportedFile(fileName)) {
|
||||
throw new ServiceException("仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件");
|
||||
}
|
||||
@@ -397,7 +397,8 @@ public class AihrSopSeedService {
|
||||
throw new ServiceException("文件不能超过 100MB");
|
||||
}
|
||||
|
||||
return saveDocument(fileName, category, () -> ossService.upload(file), () -> readContent(file, fileName), () -> fileFingerprint(file));
|
||||
MultipartFile sanitizedFile = AihrMultipartFiles.withOriginalFilename(file, fileName);
|
||||
return saveDocument(fileName, category, () -> ossService.upload(sanitizedFile), () -> readContent(sanitizedFile, fileName), () -> fileFingerprint(sanitizedFile));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class AihrMultipartFilesTest {
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void sanitizedNameMustReachTheMultipartPassedToStorage() {
|
||||
MockMultipartFile source = new MockMultipartFile(
|
||||
"file", "../../case\r\n.webm", "audio/webm", new byte[]{1, 2, 3}
|
||||
);
|
||||
|
||||
var sanitized = AihrMultipartFiles.withOriginalFilename(source, AihrMultipartFiles.sanitizeFileName(
|
||||
source.getOriginalFilename(), "audio.webm"
|
||||
));
|
||||
|
||||
assertEquals(".._.._case__.webm", sanitized.getOriginalFilename());
|
||||
assertEquals(source.getSize(), sanitized.getSize());
|
||||
assertEquals("audio/webm", sanitized.getContentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void blankNameUsesSafeFallback() {
|
||||
assertEquals("upload.bin", AihrMultipartFiles.sanitizeFileName(" \t", "upload.bin"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user