fix: 支持可信内部文件引用读取
- 通过服务端文件记录和存储配置恢复可信物理读取句柄 - 文档解析与工作流文档节点优先读取已确认的内部存储对象 - 补齐路径校验、记录异常和读取边界测试
This commit is contained in:
@@ -10,6 +10,7 @@ import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadedFileReader;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
|
||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -164,7 +165,7 @@ public class DocumentSourceLoader {
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先打开经过上传记录验证的受管 URL,再执行普通公网 URL 校验与下载。
|
||||
* 优先打开经过上传记录验证的受管 URL 和服务端已登记附件,再执行普通公网 URL 校验与下载。
|
||||
*
|
||||
* @param remoteUrl 远端 URL
|
||||
* @param maxBytes 最大允许读取字节数
|
||||
@@ -176,6 +177,13 @@ public class DocumentSourceLoader {
|
||||
if (managed.isPresent()) {
|
||||
return DocumentInputStreamSupport.limit(managed.get(), maxBytes);
|
||||
}
|
||||
Optional<FileStorageWriteHandle> trusted =
|
||||
fileStorageService.resolveTrustedFile(remoteUrl);
|
||||
if (trusted.isPresent()) {
|
||||
return DocumentInputStreamSupport.limit(
|
||||
fileStorageService.readRecoverable(trusted.get()),
|
||||
maxBytes);
|
||||
}
|
||||
return DocumentInputStreamSupport.openRemote(remoteUrl, maxBytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import tech.easyflow.ai.document.support.DocumentInputStreamSupport;
|
||||
import tech.easyflow.ai.document.support.DocumentParseSourceType;
|
||||
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadedFileReader;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
@@ -326,20 +327,29 @@ public class DocNodeFileContentExtractor {
|
||||
private void copySourceToTemporaryFile(
|
||||
DocumentSourceRef sourceRef, Path target) throws IOException {
|
||||
String filePath = sourceRef.getFilePath();
|
||||
boolean managedUpload = StringUtil.hasText(filePath)
|
||||
&& uploadedFileReader != null
|
||||
&& uploadedFileReader.isManagedPathCandidate(filePath);
|
||||
Optional<FileStorageWriteHandle> trustedFile = Optional.empty();
|
||||
if (StringUtil.hasText(filePath)
|
||||
&& isRemoteUrl(filePath)
|
||||
&& !managedUpload) {
|
||||
trustedFile = fileStorageService.resolveTrustedFile(filePath);
|
||||
}
|
||||
boolean localStorage = StringUtil.hasText(filePath)
|
||||
&& (!isRemoteUrl(filePath)
|
||||
|| (uploadedFileReader != null
|
||||
&& uploadedFileReader.isManagedPathCandidate(filePath)));
|
||||
|| managedUpload
|
||||
|| trustedFile.isPresent());
|
||||
if (localStorage) {
|
||||
try (IoBulkhead.Permit ignored =
|
||||
IoBulkhead.storage().acquire("storage:document-read");
|
||||
InputStream inputStream = openInputStream(sourceRef);
|
||||
InputStream inputStream = openInputStream(sourceRef, trustedFile);
|
||||
OutputStream outputStream = Files.newOutputStream(target)) {
|
||||
copy(inputStream, outputStream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try (InputStream inputStream = openInputStream(sourceRef);
|
||||
try (InputStream inputStream = openInputStream(sourceRef, trustedFile);
|
||||
OutputStream outputStream = Files.newOutputStream(target)) {
|
||||
copy(inputStream, outputStream);
|
||||
}
|
||||
@@ -362,7 +372,17 @@ public class DocNodeFileContentExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openInputStream(DocumentSourceRef sourceRef) throws IOException {
|
||||
/**
|
||||
* 按可信受管上传、服务端文件记录、本地路径和普通公网 URL 的顺序打开源流。
|
||||
*
|
||||
* @param sourceRef 文档源
|
||||
* @param trustedFile 已通过服务端记录或存储配置确认的物理读取句柄
|
||||
* @return 受实际字节数限制的输入流
|
||||
* @throws IOException 文件无法安全读取时抛出
|
||||
*/
|
||||
private InputStream openInputStream(
|
||||
DocumentSourceRef sourceRef,
|
||||
Optional<FileStorageWriteHandle> trustedFile) throws IOException {
|
||||
String filePath = sourceRef.getFilePath();
|
||||
if (uploadedFileReader != null && StringUtil.hasText(filePath)) {
|
||||
Optional<InputStream> managed = uploadedFileReader.openVerified(filePath);
|
||||
@@ -372,6 +392,11 @@ public class DocNodeFileContentExtractor {
|
||||
FILE_MAX_SINGLE_SIZE);
|
||||
}
|
||||
}
|
||||
if (trustedFile.isPresent()) {
|
||||
return DocumentInputStreamSupport.limit(
|
||||
fileStorageService.readRecoverable(trustedFile.get()),
|
||||
FILE_MAX_SINGLE_SIZE);
|
||||
}
|
||||
if (StringUtil.hasText(filePath) && isRemoteUrl(filePath)) {
|
||||
return DocumentInputStreamSupport.openRemote(filePath, FILE_MAX_SINGLE_SIZE);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadedFileReader;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -73,6 +74,24 @@ public class DocumentSourceLoaderTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证服务端已登记的普通附件 URL 在公网地址校验前通过物理句柄直读。
|
||||
*/
|
||||
@Test
|
||||
public void shouldLoadRecordedInternalStorageUrlBeforeRemoteAddressGuard() {
|
||||
String fileUrl = "http://127.0.0.1:39000/easyflow/attachment/1/2026/8/26/demo.pdf";
|
||||
byte[] body = "recorded-pdf".getBytes(StandardCharsets.UTF_8);
|
||||
DocumentSourceLoader loader = new DocumentSourceLoader(
|
||||
new RecordedFileStorageService(fileUrl, body));
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setFileName("demo.pdf");
|
||||
sourceRef.setFilePath(fileUrl);
|
||||
|
||||
LoadedDocumentSource loadedSource = loader.load(sourceRef);
|
||||
|
||||
Assert.assertArrayEquals(body, loadedSource.getContentBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证已通过上传记录校验的内网存储 URL 会走恢复句柄读取。
|
||||
*
|
||||
@@ -217,4 +236,42 @@ public class DocumentSourceLoaderTest {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅允许通过服务端记录句柄读取内容的存储测试替身。
|
||||
*/
|
||||
private static class RecordedFileStorageService
|
||||
extends FailingFileStorageService {
|
||||
/** 允许解析的精确 URL。 */
|
||||
private final String recordedUrl;
|
||||
/** 固定文件内容。 */
|
||||
private final byte[] content;
|
||||
/** 固定可信读取句柄。 */
|
||||
private final FileStorageWriteHandle handle = new FileStorageWriteHandle(
|
||||
"recorded", "", "/storage", "attachment", "demo.pdf");
|
||||
|
||||
/**
|
||||
* 创建服务端记录存储替身。
|
||||
*
|
||||
* @param recordedUrl 允许解析的精确 URL
|
||||
* @param content 固定文件内容
|
||||
*/
|
||||
private RecordedFileStorageService(String recordedUrl, byte[] content) {
|
||||
this.recordedUrl = recordedUrl;
|
||||
this.content = content.clone();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Optional<FileStorageWriteHandle> resolveTrustedFile(String reference) {
|
||||
return recordedUrl.equals(reference) ? Optional.of(handle) : Optional.empty();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public InputStream readRecoverable(FileStorageWriteHandle requestedHandle) {
|
||||
Assert.assertSame(handle, requestedHandle);
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.service.DocumentParseBridgeService;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -203,6 +204,27 @@ public class DocNodeFileContentExtractorTest {
|
||||
Assert.assertNull(bridgeService.lastSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证默认读取器也能通过服务端文件记录读取内部附件 URL。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReadRecordedInternalUrlForUnsupportedType() {
|
||||
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
|
||||
String fileUrl = "http://127.0.0.1:39000/easyflow/attachment/1/2026/8/26/note.txt";
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
bridgeService,
|
||||
new RecordedFileStorageService(fileUrl, "recorded text"),
|
||||
new ReadingReaderManager());
|
||||
|
||||
String content = extractor.extract(buildFileValue(
|
||||
"note.txt",
|
||||
fileUrl,
|
||||
"text/plain"));
|
||||
|
||||
Assert.assertEquals("recorded text", content);
|
||||
Assert.assertNull(bridgeService.lastSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证受管上传 URL 的非桥接文件通过记录校验后走内部存储读取。
|
||||
*
|
||||
@@ -574,4 +596,42 @@ public class DocNodeFileContentExtractorTest {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅允许通过服务端记录句柄读取内容的存储测试替身。
|
||||
*/
|
||||
private static class RecordedFileStorageService
|
||||
extends FailingFileStorageService {
|
||||
/** 允许解析的精确 URL。 */
|
||||
private final String recordedUrl;
|
||||
/** 固定内容。 */
|
||||
private final byte[] content;
|
||||
/** 固定可信读取句柄。 */
|
||||
private final FileStorageWriteHandle handle = new FileStorageWriteHandle(
|
||||
"recorded", "", "/storage", "attachment", "note.txt");
|
||||
|
||||
/**
|
||||
* 创建服务端记录存储替身。
|
||||
*
|
||||
* @param recordedUrl 允许解析的精确 URL
|
||||
* @param content 固定文本内容
|
||||
*/
|
||||
private RecordedFileStorageService(String recordedUrl, String content) {
|
||||
this.recordedUrl = recordedUrl;
|
||||
this.content = content.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Optional<FileStorageWriteHandle> resolveTrustedFile(String reference) {
|
||||
return recordedUrl.equals(reference) ? Optional.of(handle) : Optional.empty();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public InputStream readRecoverable(FileStorageWriteHandle requestedHandle) {
|
||||
Assert.assertSame(handle, requestedHandle);
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user