fix: 支持可信内部文件引用读取
- 通过服务端文件记录和存储配置恢复可信物理读取句柄 - 文档解析与工作流文档节点优先读取已确认的内部存储对象 - 补齐路径校验、记录异常和读取边界测试
This commit is contained in:
@@ -11,6 +11,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -151,6 +152,19 @@ public class FileStorageManager implements FileStorageService {
|
||||
return serviceForHandle(handle).readRecoverable(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前后端解析服务端可信文件引用。
|
||||
*
|
||||
* @param reference 文件 URL 或其他后端可识别引用
|
||||
* @return 可信文件的物理读取句柄;引用无法确认时为空
|
||||
* @throws IOException 文件记录无法安全解析时抛出
|
||||
*/
|
||||
@Override
|
||||
public Optional<FileStorageWriteHandle> resolveTrustedFile(String reference)
|
||||
throws IOException {
|
||||
return currentService().resolveTrustedFile(reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格按句柄中的后端精确删除物理对象。
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* EasyFlow 文件存储统一接口。
|
||||
@@ -105,6 +106,21 @@ public interface FileStorageService {
|
||||
throw unsupportedRecoverableOperation("readRecoverable");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将服务端可信文件引用解析为物理读取句柄。
|
||||
*
|
||||
* <p>实现必须以服务端持久化记录或存储平台配置为信任来源,并要求外部引用与可信来源
|
||||
* 精确匹配;不得仅根据客户端传入的 URL、路径或 locator 构造句柄。</p>
|
||||
*
|
||||
* @param reference 文件 URL 或其他后端可识别引用
|
||||
* @return 可信文件的物理读取句柄;引用无法确认时为空
|
||||
* @throws IOException 文件记录损坏或存储配置不兼容时抛出
|
||||
*/
|
||||
default Optional<FileStorageWriteHandle> resolveTrustedFile(String reference)
|
||||
throws IOException {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 精确且幂等地删除句柄对应的物理对象。
|
||||
*
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.io.*;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 基于 x-file-storage 的 EasyFlow 文件存储实现。
|
||||
@@ -268,6 +269,47 @@ public class XFIleStorageServiceImpl implements FileStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 x-file-storage 文件记录或服务端平台配置恢复可信物理读取句柄。
|
||||
*
|
||||
* <p>优先使用 recorder 的精确记录。记录不存在时,仅允许与已配置平台 domain、basePath
|
||||
* 及重建后的完整 URL 完全一致的引用。其他 URL 返回空,由上层继续执行公网地址安全校验。</p>
|
||||
*
|
||||
* @param reference 文件 URL
|
||||
* @return 可信文件的物理读取句柄;引用无法确认时为空
|
||||
* @throws IOException 文件记录损坏或平台配置不兼容时抛出
|
||||
*/
|
||||
@Override
|
||||
public Optional<FileStorageWriteHandle> resolveTrustedFile(String reference)
|
||||
throws IOException {
|
||||
if (!StringUtils.hasText(reference)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
FileInfo fileInfo = null;
|
||||
try {
|
||||
fileInfo = fileStorageService.getFileInfoByUrl(reference);
|
||||
} catch (RuntimeException exception) {
|
||||
// 存储平台配置本身仍可提供精确可信边界,记录器异常不应阻断内部对象读取。
|
||||
LOG.warn("查询 x-file-storage 文件记录失败,继续按服务端存储配置识别,reference={}",
|
||||
reference, exception);
|
||||
}
|
||||
if (fileInfo != null) {
|
||||
if (!reference.equals(fileInfo.getUrl())) {
|
||||
throw new IOException("x-file-storage 文件记录 URL 与请求引用不一致");
|
||||
}
|
||||
try {
|
||||
FileStorageWriteHandle handle = handleFromFileInfo(fileInfo);
|
||||
FileStorage storage = requireStorage(handle);
|
||||
requirePersistedBasePathSupport(storage, handle);
|
||||
verifyRecordedLocation(fileInfo, handle);
|
||||
return Optional.of(handle);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IOException("x-file-storage 文件记录无法恢复为安全读取位置", exception);
|
||||
}
|
||||
}
|
||||
return resolveConfiguredStorageReference(reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用句柄指定平台的物理删除与存在检查,绕过依赖 URL 记录的聚合删除路径。
|
||||
*
|
||||
@@ -391,6 +433,108 @@ public class XFIleStorageServiceImpl implements FileStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 recorder 中的物理定位字段可由恢复句柄无损重建。
|
||||
*
|
||||
* @param fileInfo 服务端文件记录
|
||||
* @param handle 恢复出的物理读取句柄
|
||||
*/
|
||||
private void verifyRecordedLocation(FileInfo fileInfo, FileStorageWriteHandle handle) {
|
||||
String actualBasePath = fileInfo.getBasePath() == null ? "" : fileInfo.getBasePath();
|
||||
String actualPath = fileInfo.getPath() == null ? "" : fileInfo.getPath();
|
||||
if (!handle.getPlatform().equals(fileInfo.getPlatform())
|
||||
|| !handle.getBasePath().equals(actualBasePath)
|
||||
|| !physicalPath(handle).equals(actualPath)
|
||||
|| !handle.getFilename().equals(fileInfo.getFilename())) {
|
||||
throw new IllegalStateException("x-file-storage 文件记录包含非规范物理位置");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 FileInfo 的物理定位字段构造严格校验的读取句柄。
|
||||
*
|
||||
* @param fileInfo 服务端文件信息
|
||||
* @return 可信物理读取句柄
|
||||
*/
|
||||
private FileStorageWriteHandle handleFromFileInfo(FileInfo fileInfo) {
|
||||
String basePath = fileInfo.getBasePath() == null ? "" : fileInfo.getBasePath();
|
||||
return new FileStorageWriteHandle(
|
||||
RECOVERABLE_BACKEND,
|
||||
fileInfo.getPlatform(),
|
||||
basePath,
|
||||
recordedRelativePath(basePath, fileInfo.getPath()),
|
||||
fileInfo.getFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按服务端配置的平台 domain 与 basePath 识别内部存储 URL。
|
||||
*
|
||||
* <p>解析后会再次通过平台自身的 getFileKey 重建完整 URL 并进行精确比较,避免仅凭
|
||||
* host 或字符串前缀放行其他私网目标。</p>
|
||||
*
|
||||
* @param reference 待识别 URL
|
||||
* @return 精确匹配配置的读取句柄;不匹配任何平台时为空
|
||||
* @throws IOException 匹配平台前缀但路径无法安全恢复时抛出
|
||||
*/
|
||||
private Optional<FileStorageWriteHandle> resolveConfiguredStorageReference(
|
||||
String reference) throws IOException {
|
||||
if (fileStorageService.getFileStorageList() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
for (FileStorage storage : fileStorageService.getFileStorageList()) {
|
||||
String domain = readDomainBestEffort(storage);
|
||||
if (!StringUtils.hasText(domain)) {
|
||||
continue;
|
||||
}
|
||||
String basePath = readRequiredBasePath(storage);
|
||||
String prefix = domain + basePath;
|
||||
if (!reference.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String remainder = reference.substring(prefix.length());
|
||||
int filenameIndex = remainder.lastIndexOf('/') + 1;
|
||||
FileInfo fileInfo = new FileInfo()
|
||||
.setUrl(reference)
|
||||
.setPlatform(storage.getPlatform())
|
||||
.setBasePath(basePath)
|
||||
.setPath(remainder.substring(0, filenameIndex))
|
||||
.setFilename(remainder.substring(filenameIndex));
|
||||
FileStorageWriteHandle handle = handleFromFileInfo(fileInfo);
|
||||
requirePersistedBasePathSupport(storage, handle);
|
||||
verifyRecordedLocation(fileInfo, handle);
|
||||
if (!reference.equals(deriveUrlBestEffort(storage, toFileInfo(handle)))) {
|
||||
throw new IllegalArgumentException("重建 URL 与请求引用不一致");
|
||||
}
|
||||
return Optional.of(handle);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IOException("服务端存储 URL 无法恢复为安全读取位置", exception);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 recorder 保存的 x-file-storage 物理路径还原为句柄相对路径。
|
||||
*
|
||||
* @param basePath 平台基础路径
|
||||
* @param recordedPath recorder 中保存的物理目录
|
||||
* @return 不带前导斜杠的相对目录
|
||||
*/
|
||||
private String recordedRelativePath(String basePath, String recordedPath) {
|
||||
String path = recordedPath == null ? "" : recordedPath;
|
||||
if (basePath.isEmpty() || basePath.endsWith("/")) {
|
||||
if (path.startsWith("/")) {
|
||||
throw new IllegalArgumentException("文件记录路径与平台基础路径格式不一致");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
throw new IllegalArgumentException("文件记录路径缺少必要的前导斜杠");
|
||||
}
|
||||
return path.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造仅包含精确物理定位字段的 FileInfo。
|
||||
*
|
||||
@@ -487,16 +631,26 @@ public class XFIleStorageServiceImpl implements FileStorageService {
|
||||
* @return 可推导 URL;平台不支持时返回 null
|
||||
*/
|
||||
private String deriveUrlBestEffort(FileStorage storage, FileInfo fileInfo) {
|
||||
String domain = readDomainBestEffort(storage);
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
return domain + storage.getFileKey(fileInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用平台公开的 getDomain 方法读取文件访问域名。
|
||||
*
|
||||
* @param storage 具体平台存储
|
||||
* @return 平台访问域名;平台不支持时返回 null
|
||||
*/
|
||||
private String readDomainBestEffort(FileStorage storage) {
|
||||
try {
|
||||
Method method = storage.getClass().getMethod("getDomain");
|
||||
if (!String.class.equals(method.getReturnType())) {
|
||||
return null;
|
||||
}
|
||||
String domain = (String) method.invoke(storage);
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
return domain + storage.getFileKey(fileInfo);
|
||||
return (String) method.invoke(storage);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | RuntimeException exception) {
|
||||
LOG.debug("当前 x-file-storage 平台无法推导 recorder URL: {}", storage.getClass().getName());
|
||||
return null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -49,6 +50,27 @@ public class FileStorageManagerTest {
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证服务端文件记录解析使用当前配置的具体存储后端。
|
||||
*
|
||||
* @throws IOException 文件记录解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void recordedFileResolutionUsesCurrentBackend() throws IOException {
|
||||
RecordingStorage local = new RecordingStorage("local");
|
||||
RecordingStorage xFile = new RecordingStorage("xFileStorage");
|
||||
FileStorageManager manager = new FileStorageManager(
|
||||
() -> "xFileStorage",
|
||||
backend -> Map.of("local", local, "xFileStorage", xFile).get(backend));
|
||||
|
||||
Optional<FileStorageWriteHandle> resolved = manager.resolveTrustedFile(
|
||||
"http://127.0.0.1:39000/easyflow/attachment/demo.pdf");
|
||||
|
||||
assertSame(xFile.recordedHandle, resolved.orElseThrow());
|
||||
assertEquals(1, xFile.resolveCalls);
|
||||
assertEquals(0, local.resolveCalls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可记录可恢复调用的存储测试替身。
|
||||
*/
|
||||
@@ -57,6 +79,8 @@ public class FileStorageManagerTest {
|
||||
private final String backend;
|
||||
/** 固定结果。 */
|
||||
private final FileStorageWriteResult result;
|
||||
/** 固定服务端文件记录句柄。 */
|
||||
private final FileStorageWriteHandle recordedHandle;
|
||||
/** 固定可恢复读取流。 */
|
||||
private final InputStream recoverableInput = InputStream.nullInputStream();
|
||||
/** prepare 调用次数。 */
|
||||
@@ -69,6 +93,8 @@ public class FileStorageManagerTest {
|
||||
private int deleteCalls;
|
||||
/** exists 调用次数。 */
|
||||
private int existsCalls;
|
||||
/** 服务端文件记录解析调用次数。 */
|
||||
private int resolveCalls;
|
||||
|
||||
/**
|
||||
* 创建指定名称的存储替身。
|
||||
@@ -80,6 +106,8 @@ public class FileStorageManagerTest {
|
||||
FileStorageWriteHandle handle = new FileStorageWriteHandle(
|
||||
backend, "", "/tmp/easyflow", "skill-content", "content.bin");
|
||||
this.result = new FileStorageWriteResult("/files/content.bin", handle.encodeLocator());
|
||||
this.recordedHandle = new FileStorageWriteHandle(
|
||||
backend, "", "/tmp/easyflow", "attachment", "demo.pdf");
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@@ -114,6 +142,13 @@ public class FileStorageManagerTest {
|
||||
return recoverableInput;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Optional<FileStorageWriteHandle> resolveTrustedFile(String reference) {
|
||||
resolveCalls++;
|
||||
return Optional.of(recordedHandle);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void deleteRecoverable(FileStorageWriteHandle handle) {
|
||||
|
||||
@@ -212,6 +212,123 @@ public class XFIleStorageServiceImplTest {
|
||||
client.lastArgs.object());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 recorder 登记的回环地址附件可恢复为可信句柄并通过 MinIO 客户端直读。
|
||||
*
|
||||
* @throws Exception 测试替身配置或流读取失败
|
||||
*/
|
||||
@Test
|
||||
public void recordedLoopbackUrlUsesExactMinioObject() throws Exception {
|
||||
byte[] content = "workflow-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
RecordingMinioClient client = new RecordingMinioClient(content);
|
||||
MinioFileStorage platform = new MinioFileStorage();
|
||||
platform.setPlatform("minio-main");
|
||||
platform.setBucketName("easyflow");
|
||||
platform.setBasePath("attachment");
|
||||
platform.setDomain("http://127.0.0.1:39000/easyflow/");
|
||||
platform.setClientFactory(new FixedMinioClientFactory(client));
|
||||
RecoverableStorageService delegate = new RecoverableStorageService(platform);
|
||||
String fileUrl = "http://127.0.0.1:39000/easyflow/attachment/1/2026/8/26/"
|
||||
+ "d6186b17-4ab7-4f99-9299-b19df7ff0a3b/投标文件否决(废标)违规事项汇总.pdf";
|
||||
delegate.recordedFileInfo = new FileInfo()
|
||||
.setUrl(fileUrl)
|
||||
.setPlatform("minio-main")
|
||||
.setBasePath("attachment")
|
||||
.setPath("/1/2026/8/26/d6186b17-4ab7-4f99-9299-b19df7ff0a3b/")
|
||||
.setFilename("投标文件否决(废标)违规事项汇总.pdf");
|
||||
XFIleStorageServiceImpl service = createService(delegate);
|
||||
|
||||
FileStorageWriteHandle handle = service.resolveTrustedFile(fileUrl).orElseThrow();
|
||||
byte[] actual;
|
||||
try (InputStream inputStream = service.readRecoverable(handle)) {
|
||||
actual = inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
assertEquals("attachment", handle.getBasePath());
|
||||
assertEquals(
|
||||
"1/2026/8/26/d6186b17-4ab7-4f99-9299-b19df7ff0a3b/",
|
||||
handle.getPath());
|
||||
assertArrayEquals(content, actual);
|
||||
assertEquals("easyflow", client.lastArgs.bucket());
|
||||
assertEquals(
|
||||
"attachment/1/2026/8/26/d6186b17-4ab7-4f99-9299-b19df7ff0a3b/"
|
||||
+ "投标文件否决(废标)违规事项汇总.pdf",
|
||||
client.lastArgs.object());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 recorder 没有记录时,服务端配置的存储 URL 仍可通过 MinIO 客户端直读。
|
||||
*
|
||||
* @throws Exception 测试替身配置或流读取失败
|
||||
*/
|
||||
@Test
|
||||
public void configuredStorageUrlWithoutRecorderUsesExactMinioObject() throws Exception {
|
||||
byte[] content = "configured-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
RecordingMinioClient client = new RecordingMinioClient(content);
|
||||
MinioFileStorage platform = new MinioFileStorage();
|
||||
platform.setPlatform("minio-main");
|
||||
platform.setBucketName("easyflow");
|
||||
platform.setBasePath("attachment");
|
||||
platform.setDomain("http://127.0.0.1:39000/easyflow/");
|
||||
platform.setClientFactory(new FixedMinioClientFactory(client));
|
||||
XFIleStorageServiceImpl service = createService(new RecoverableStorageService(platform));
|
||||
String fileUrl = "http://127.0.0.1:39000/easyflow/attachment/1/2026/8/26/"
|
||||
+ "0f0db465-7fa0-46b1-9fae-4f5a8c85f881/投标文件否决(废标)违规事项汇总.pdf";
|
||||
|
||||
FileStorageWriteHandle handle = service.resolveTrustedFile(fileUrl).orElseThrow();
|
||||
byte[] actual;
|
||||
try (InputStream inputStream = service.readRecoverable(handle)) {
|
||||
actual = inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
assertArrayEquals(content, actual);
|
||||
assertEquals(
|
||||
"attachment/1/2026/8/26/0f0db465-7fa0-46b1-9fae-4f5a8c85f881/"
|
||||
+ "投标文件否决(废标)违规事项汇总.pdf",
|
||||
client.lastArgs.object());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未配置为存储地址的回环 URL 不会被识别为可信附件。
|
||||
*
|
||||
* @throws Exception 测试替身注入失败
|
||||
*/
|
||||
@Test
|
||||
public void unconfiguredLoopbackUrlIsNotResolved() throws Exception {
|
||||
RecoverablePlatform platform = new RecoverablePlatform(
|
||||
"minio-main", "attachment", "http://127.0.0.1:39000/easyflow/");
|
||||
XFIleStorageServiceImpl service = createService(new RecoverableStorageService(platform));
|
||||
|
||||
assertTrue(service.resolveTrustedFile(
|
||||
"http://127.0.0.1:39000/other/unconfigured.pdf").isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 recorder 中非规范物理路径会失败关闭。
|
||||
*
|
||||
* @throws Exception 测试替身注入失败
|
||||
*/
|
||||
@Test
|
||||
public void corruptedRecordedLocationIsRejected() throws Exception {
|
||||
RecoverablePlatform platform = new RecoverablePlatform(
|
||||
"minio-main", "attachment", "http://127.0.0.1:39000/easyflow/");
|
||||
RecoverableStorageService delegate = new RecoverableStorageService(platform);
|
||||
String fileUrl = "http://127.0.0.1:39000/easyflow/attachment/demo.pdf";
|
||||
delegate.recordedFileInfo = new FileInfo()
|
||||
.setUrl(fileUrl)
|
||||
.setPlatform("minio-main")
|
||||
.setBasePath("attachment")
|
||||
.setPath("missing-leading-slash/")
|
||||
.setFilename("demo.pdf");
|
||||
XFIleStorageServiceImpl service = createService(delegate);
|
||||
|
||||
IOException exception = assertThrows(
|
||||
IOException.class,
|
||||
() -> service.resolveTrustedFile(fileUrl));
|
||||
|
||||
assertTrue(exception.getMessage().contains("无法恢复"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 recorder 完全缺失目标记录时,精确删除仍直接作用于物理平台并成功。
|
||||
*
|
||||
@@ -471,6 +588,8 @@ public class XFIleStorageServiceImplTest {
|
||||
private int recorderDeleteCalls;
|
||||
/** recorder 删除是否抛出异常。 */
|
||||
private boolean recorderDeleteThrows;
|
||||
/** recorder 返回的服务端文件记录。 */
|
||||
private FileInfo recordedFileInfo;
|
||||
|
||||
/**
|
||||
* 创建聚合服务替身。
|
||||
@@ -479,10 +598,17 @@ public class XFIleStorageServiceImplTest {
|
||||
*/
|
||||
private RecoverableStorageService(FileStorage platform) {
|
||||
this.platform = platform;
|
||||
setFileStorageList(new java.util.concurrent.CopyOnWriteArrayList<>(
|
||||
java.util.List.of(platform)));
|
||||
setFileRecorder(new FileRecorder() {
|
||||
@Override public boolean save(FileInfo fileInfo) { return true; }
|
||||
@Override public void update(FileInfo fileInfo) { }
|
||||
@Override public FileInfo getByUrl(String url) { return null; }
|
||||
@Override public FileInfo getByUrl(String url) {
|
||||
return recordedFileInfo != null
|
||||
&& url.equals(recordedFileInfo.getUrl())
|
||||
? recordedFileInfo
|
||||
: null;
|
||||
}
|
||||
@Override public boolean delete(String url) {
|
||||
recorderDeleteCalls++;
|
||||
if (recorderDeleteThrows) {
|
||||
@@ -506,6 +632,15 @@ public class XFIleStorageServiceImplTest {
|
||||
return platform.getPlatform().equals(name) ? (T) platform : null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public FileInfo getFileInfoByUrl(String url) {
|
||||
return recordedFileInfo != null
|
||||
&& url.equals(recordedFileInfo.getUrl())
|
||||
? recordedFileInfo
|
||||
: null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public org.dromara.x.file.storage.core.upload.UploadPretreatment of(Object file) {
|
||||
|
||||
Reference in New Issue
Block a user