FtpTool/src/main/java/com/ftptool/sync/service/ProdConfigApiService.java
2026-04-20 14:51:59 +08:00

130 lines
5.4 KiB
Java

package com.ftptool.sync.service;
import com.ftptool.sync.config.ProdApiProperties;
import com.ftptool.sync.config.SyncProperties;
import com.ftptool.sync.model.PackageManifest;
import com.ftptool.sync.model.ProdPullResult;
import com.ftptool.sync.util.FileHashUtils;
import com.ftptool.sync.util.FileTreeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Service
public class ProdConfigApiService {
private static final Logger log = LoggerFactory.getLogger(ProdConfigApiService.class);
private final ProdApiProperties prodApiProperties;
private final SyncProperties syncProperties;
private final RestTemplate restTemplate;
private final WorkDirectoryService workDirectoryService;
public ProdConfigApiService(
ProdApiProperties prodApiProperties,
SyncProperties syncProperties,
RestTemplate restTemplate,
WorkDirectoryService workDirectoryService
) {
this.prodApiProperties = prodApiProperties;
this.syncProperties = syncProperties;
this.restTemplate = restTemplate;
this.workDirectoryService = workDirectoryService;
}
public void pushPackage(PackageManifest manifest, Path zipFile) {
String url = buildUrl(prodApiProperties.getPushPath());
HttpHeaders headers = defaultHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 当前协议约定 push 使用 multipart/form-data 上传标准同步包。
MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
body.add("file", new FileSystemResource(zipFile.toFile()));
body.add("traceId", manifest.getTraceId());
body.add("direction", manifest.getDirection().name());
body.add("sourceVersion", manifest.getSourceVersion());
body.add("contentHash", manifest.getContentHash());
ResponseEntity<String> response = restTemplate.postForEntity(url, new HttpEntity<MultiValueMap<String, Object>>(body, headers), String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException("Prod push API failed with status " + response.getStatusCodeValue());
}
log.info("Prod push API finished. traceId={}, status={}", manifest.getTraceId(), response.getStatusCodeValue());
}
public ProdPullResult pullConfigSnapshot() throws IOException {
String url = buildUrl(prodApiProperties.getPullPath());
HttpHeaders headers = defaultHeaders();
// 当前协议约定 pull 直接返回原始配置字节流,由同步工具落成本地文件后再回写 Git。
ResponseEntity<byte[]> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<Void>(headers),
byte[].class
);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException("Prod pull API failed with status " + response.getStatusCodeValue());
}
byte[] body = response.getBody();
if (body == null || body.length == 0) {
throw new IllegalStateException("Prod pull API returned empty content");
}
Path tempDir = Files.createTempDirectory(workDirectoryService.getProdToDevStagingDir(), "pull-");
FileTreeUtils.ensureDirectory(tempDir);
Path targetFile = tempDir.resolve(syncProperties.getPullResponseFileName());
Files.write(targetFile, body);
String contentHash = FileHashUtils.sha256(body);
// 优先取服务端显式版本号;如果服务端没给,就退化为内容哈希做幂等判断。
String sourceVersion = firstNonBlank(
response.getHeaders().getFirst("X-Config-Version"),
response.getHeaders().getETag(),
contentHash
);
return new ProdPullResult(tempDir, sourceVersion, contentHash);
}
private HttpHeaders defaultHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
if (prodApiProperties.getToken() != null && !prodApiProperties.getToken().trim().isEmpty()) {
headers.setBearerAuth(prodApiProperties.getToken().trim());
}
return headers;
}
private String buildUrl(String path) {
String base = prodApiProperties.getBaseUrl();
if (base.endsWith("/") && path.startsWith("/")) {
return base.substring(0, base.length() - 1) + path;
}
if (!base.endsWith("/") && !path.startsWith("/")) {
return base + "/" + path;
}
return base + path;
}
private String firstNonBlank(String... candidates) {
for (String candidate : candidates) {
if (candidate != null && !candidate.trim().isEmpty()) {
return candidate;
}
}
return null;
}
}