130 lines
5.4 KiB
Java
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;
|
|
}
|
|
}
|