1 parent
cec6828479
commit
29db7da4a6
1 file changed
+124
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl;
|
||||||
|
|
||||||
|
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStorageReadRequest;
|
||||||
|
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
|
||||||
|
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStorageReadResult;
|
||||||
|
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
|
||||||
|
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* local/local-replay 专用文件对象存储。
|
||||||
|
*
|
||||||
|
* <p>对象 key 只能落在配置的单一根目录下;返回 file URL 供同一进程内的 booking worker 读取,
|
||||||
|
* 不提供目录浏览或任意路径读取能力。</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
prefix = "debug.agentbus-eml-replay",
|
||||||
|
name = "local-storage-enabled",
|
||||||
|
havingValue = "true")
|
||||||
|
public class LocalReplayObjectStorageService implements ObjectStorageService {
|
||||||
|
|
||||||
|
private final Path root;
|
||||||
|
|
||||||
|
public LocalReplayObjectStorageService(
|
||||||
|
cn.nianxx.thhotel.platform.debug.service.impl.AgentBusEmlReplayProperties properties) {
|
||||||
|
String configured = properties.getLocalStoragePath();
|
||||||
|
if (configured == null || configured.isBlank()) {
|
||||||
|
throw new IllegalStateException("debug.agentbus-eml-replay.local-storage-path 不能为空。");
|
||||||
|
}
|
||||||
|
this.root = Path.of(configured).toAbsolutePath().normalize();
|
||||||
|
try {
|
||||||
|
Files.createDirectories(root);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("本地回放对象存储目录无法创建。", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ObjectStoragePutResult putObject(ObjectStoragePutRequest request) {
|
||||||
|
Objects.requireNonNull(request, "对象存储请求不能为空");
|
||||||
|
Path target = safeKey(request.objectKey());
|
||||||
|
try {
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
byte[] content = request.content() == null ? new byte[0] : request.content();
|
||||||
|
Files.write(target, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
|
StandardOpenOption.WRITE);
|
||||||
|
return new ObjectStoragePutResult(
|
||||||
|
request.objectKey(), target.toUri().toString(), request.contentType(), (long) content.length);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new ObjectStorageException("本地回放对象写入失败。", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ObjectStorageReadResult readObject(ObjectStorageReadRequest request) {
|
||||||
|
if (request == null || request.externalUrl() == null || request.externalUrl().isBlank()) {
|
||||||
|
throw new ObjectStorageException("本地回放对象读取地址不能为空。");
|
||||||
|
}
|
||||||
|
Path target;
|
||||||
|
try {
|
||||||
|
target = safeUri(request.externalUrl());
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
throw new ObjectStorageException("本地回放对象读取地址无效。", exception);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long maxBytes = request.maxBytes() == null || request.maxBytes() <= 0
|
||||||
|
? 10L * 1024L * 1024L
|
||||||
|
: request.maxBytes();
|
||||||
|
if (Files.size(target) > maxBytes) {
|
||||||
|
throw new ObjectStorageException("本地回放对象超过读取大小限制。");
|
||||||
|
}
|
||||||
|
byte[] content = Files.readAllBytes(target);
|
||||||
|
return new ObjectStorageReadResult(content, null, (long) content.length);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new ObjectStorageException("本地回放对象读取失败。", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteObject(String objectKey) {
|
||||||
|
Path target = safeKey(objectKey);
|
||||||
|
if (target.equals(root) || objectKey.endsWith("/") || objectKey.contains("*") || objectKey.contains("?")) {
|
||||||
|
throw new ObjectStorageException("本地回放对象删除目标格式错误。");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(target);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new ObjectStorageException("本地回放对象删除失败。", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path safeKey(String objectKey) {
|
||||||
|
if (objectKey == null || objectKey.isBlank() || objectKey.startsWith("/")
|
||||||
|
|| objectKey.contains("\\") || objectKey.contains("\u0000")) {
|
||||||
|
throw new ObjectStorageException("本地回放对象 key 格式错误。");
|
||||||
|
}
|
||||||
|
Path target = root.resolve(objectKey).normalize();
|
||||||
|
if (!target.startsWith(root) || target.equals(root)) {
|
||||||
|
throw new ObjectStorageException("本地回放对象 key 越过存储根目录。");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path safeUri(String externalUrl) {
|
||||||
|
URI uri = URI.create(externalUrl);
|
||||||
|
if (!"file".equalsIgnoreCase(uri.getScheme()) || uri.getQuery() != null || uri.getFragment() != null) {
|
||||||
|
throw new ObjectStorageException("本地回放对象只允许 file URL。");
|
||||||
|
}
|
||||||
|
Path target = Path.of(uri).toAbsolutePath().normalize();
|
||||||
|
if (!target.startsWith(root) || target.equals(root)) {
|
||||||
|
throw new ObjectStorageException("本地回放对象读取地址越过存储根目录。");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user