feat:备份恢复
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "tms.backup")
|
||||
public class ResourceBackupProperties {
|
||||
|
||||
private String outputDir = "/home/tms/tmp/resource-backup-packages";
|
||||
private String precheckStoreDir = "/home/tms/tmp/resource-restore-precheck";
|
||||
private String restoreTaskRootDir = "/home/tms/tmp/resource-restore";
|
||||
private String tmsScriptPath = "/home/tms/scripts/tms.sh";
|
||||
private boolean runnerAutoLaunchEnabled = false;
|
||||
private int runnerHeartbeatTimeoutSeconds = 300;
|
||||
private int storageEncKeyIndex = 1;
|
||||
private int authenticitySignKeyIndex = 1;
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
public void setOutputDir(String outputDir) {
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
public String getPrecheckStoreDir() {
|
||||
return precheckStoreDir;
|
||||
}
|
||||
|
||||
public void setPrecheckStoreDir(String precheckStoreDir) {
|
||||
this.precheckStoreDir = precheckStoreDir;
|
||||
}
|
||||
|
||||
public String getRestoreTaskRootDir() {
|
||||
return restoreTaskRootDir;
|
||||
}
|
||||
|
||||
public void setRestoreTaskRootDir(String restoreTaskRootDir) {
|
||||
this.restoreTaskRootDir = restoreTaskRootDir;
|
||||
}
|
||||
|
||||
public String getTmsScriptPath() {
|
||||
return tmsScriptPath;
|
||||
}
|
||||
|
||||
public void setTmsScriptPath(String tmsScriptPath) {
|
||||
this.tmsScriptPath = tmsScriptPath;
|
||||
}
|
||||
|
||||
public boolean getRunnerAutoLaunchEnabled() {
|
||||
return runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public void setRunnerAutoLaunchEnabled(boolean runnerAutoLaunchEnabled) {
|
||||
this.runnerAutoLaunchEnabled = runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public int getRunnerHeartbeatTimeoutSeconds() {
|
||||
return runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setRunnerHeartbeatTimeoutSeconds(int runnerHeartbeatTimeoutSeconds) {
|
||||
this.runnerHeartbeatTimeoutSeconds = runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public int getStorageEncKeyIndex() {
|
||||
return storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyIndex(int storageEncKeyIndex) {
|
||||
this.storageEncKeyIndex = storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public int getAuthenticitySignKeyIndex() {
|
||||
return authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) {
|
||||
this.authenticitySignKeyIndex = authenticitySignKeyIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-backups")
|
||||
@Tag(name = "资源备份", description = "资源备份创建、状态查询和步骤日志接口")
|
||||
public class ResourceBackupController {
|
||||
|
||||
private final ResourceBackupService resourceBackupService;
|
||||
|
||||
public ResourceBackupController(ResourceBackupService resourceBackupService) {
|
||||
this.resourceBackupService = resourceBackupService;
|
||||
}
|
||||
|
||||
@GetMapping("/keyset/status")
|
||||
@Operation(summary = "查询资源备份密钥状态", description = "返回资源备份恢复前置密钥状态和公钥指纹。")
|
||||
public ApiResponse<ResourceKeysetStatusResponse> keysetStatus() {
|
||||
return ApiResponse.success(resourceBackupService.getKeysetStatus());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ReplayProtected
|
||||
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
|
||||
return ApiResponse.success(resourceBackupService.createBackupTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@Operation(summary = "查询资源备份任务详情", description = "返回资源备份任务详情。")
|
||||
public ApiResponse<ResourceBackupTaskDetailResponse> taskDetail(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/download")
|
||||
@Operation(summary = "下载资源备份包", description = "以浏览器附件方式下载资源备份包。")
|
||||
public ResponseEntity<Resource> downloadPackage(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
ResourceBackupDownloadResult result = resourceBackupService.downloadBackupPackage(taskId);
|
||||
FileSystemResource resource = new FileSystemResource(result.getPackagePath());
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.contentLength(result.getContentLength())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(result.getFilename()).build().toString())
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@Operation(summary = "查询资源备份任务步骤", description = "返回资源备份任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTaskSteps(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@Operation(summary = "查询资源备份步骤日志", description = "返回资源备份任务步骤日志。")
|
||||
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId,
|
||||
@Parameter(description = "步骤序号")
|
||||
@PathVariable int stepNo
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTaskStepLog(taskId, stepNo));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-restores")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
|
||||
public class ResourceRestoreController {
|
||||
|
||||
private final ResourcePrecheckService resourcePrecheckService;
|
||||
private final ResourceRestoreService resourceRestoreService;
|
||||
private final ResourceTaskQueryService resourceTaskQueryService;
|
||||
|
||||
public ResourceRestoreController(
|
||||
ResourcePrecheckService resourcePrecheckService,
|
||||
ResourceRestoreService resourceRestoreService,
|
||||
ResourceTaskQueryService resourceTaskQueryService
|
||||
) {
|
||||
this.resourcePrecheckService = resourcePrecheckService;
|
||||
this.resourceRestoreService = resourceRestoreService;
|
||||
this.resourceTaskQueryService = resourceTaskQueryService;
|
||||
}
|
||||
|
||||
@PostMapping("/precheck")
|
||||
@Operation(summary = "预检资源恢复请求", description = "根据 fileId 校验备份包头、签名和恢复范围摘要。")
|
||||
public ApiResponse<ResourceRestorePrecheckResponse> precheck(@Valid @RequestBody ResourceRestorePrecheckRequest request) {
|
||||
return ApiResponse.success(resourcePrecheckService.precheck(request));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ReplayProtected
|
||||
@Operation(summary = "创建资源恢复任务", description = "根据预检号创建资源恢复任务。")
|
||||
public ApiResponse<CreateResourceRestoreResponse> createRestoreTask(@Valid @RequestBody CreateResourceRestoreRequest request) {
|
||||
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
|
||||
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@Operation(summary = "查询资源恢复步骤日志", description = "返回资源恢复步骤日志。")
|
||||
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId,
|
||||
@Parameter(description = "步骤序号")
|
||||
@PathVariable int stepNo
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskStepLog(taskId, stepNo));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
public interface ResourceEnvelopeCryptoService {
|
||||
|
||||
ResourceSealedPayload seal(byte[] payloadBytes);
|
||||
|
||||
byte[] sign(byte[] dataToSign);
|
||||
|
||||
void verify(byte[] dataToVerify, byte[] signature);
|
||||
|
||||
byte[] unsealAndVerify(
|
||||
java.util.Map<String, Object> envelope,
|
||||
byte[] cipherBytes,
|
||||
byte[] dataToVerify,
|
||||
byte[] signature
|
||||
);
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCryptoService {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceEnvelopeCryptoServiceImpl(
|
||||
PcieCryptoService pcieCryptoService,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceSealedPayload seal(byte[] payloadBytes) {
|
||||
if (payloadBytes == null) {
|
||||
throw new IllegalArgumentException("payloadBytes is required");
|
||||
}
|
||||
// 资源包正文使用随机 SM4 DEK 加密,再由用户密钥把 DEK 封装成 envelope。
|
||||
// 这样大 payload 仍走对称加密,ECC 只负责保护一次性数据密钥。
|
||||
byte[] sm4Key = randomBytes(16);
|
||||
byte[] iv = randomBytes(16);
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
|
||||
|
||||
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
|
||||
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
|
||||
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
encryptRequest.setData(sm4Key);
|
||||
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
|
||||
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
envelope.put("version", 1);
|
||||
envelope.put("payloadAlg", "SM4-CBC");
|
||||
envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
|
||||
envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
|
||||
envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek)));
|
||||
envelope.put("dekLength", sm4Key.length);
|
||||
|
||||
ResourceSealedPayload sealedPayload = new ResourceSealedPayload();
|
||||
sealedPayload.setCipherBytes(cipher.doFinal(payloadBytes));
|
||||
sealedPayload.setEnvelope(envelope);
|
||||
sealedPayload.setStorageEncKeyFingerprint(buildFingerprint("USER_KEY_ENC", resourceBackupProperties.getStorageEncKeyIndex()));
|
||||
sealedPayload.setAuthenticityKeyFingerprint(buildFingerprint("USER_KEY_SIGN", resourceBackupProperties.getAuthenticitySignKeyIndex()));
|
||||
return sealedPayload;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to seal resource payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(byte[] dataToSign) {
|
||||
if (dataToSign == null) {
|
||||
throw new IllegalArgumentException("dataToSign is required");
|
||||
}
|
||||
EccInternalSignRequest request = new EccInternalSignRequest();
|
||||
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
|
||||
request.setData(sm3(dataToSign));
|
||||
return normalize(pcieCryptoService.eccInternalSign(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(byte[] dataToVerify, byte[] signature) {
|
||||
if (dataToVerify == null) {
|
||||
throw new IllegalArgumentException("dataToVerify is required");
|
||||
}
|
||||
if (signature == null || signature.length == 0) {
|
||||
throw new IllegalArgumentException("signature is required");
|
||||
}
|
||||
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
|
||||
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
verifyRequest.setData(sm3(dataToVerify));
|
||||
verifyRequest.setSignature(signature);
|
||||
pcieCryptoService.eccInternalVerify(verifyRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] unsealAndVerify(Map<String, Object> envelope, byte[] cipherBytes, byte[] dataToVerify, byte[] signature) {
|
||||
if (envelope == null) {
|
||||
throw new IllegalArgumentException("envelope is required");
|
||||
}
|
||||
if (cipherBytes == null) {
|
||||
throw new IllegalArgumentException("cipherBytes is required");
|
||||
}
|
||||
if (dataToVerify == null) {
|
||||
throw new IllegalArgumentException("dataToVerify is required");
|
||||
}
|
||||
if (signature == null || signature.length == 0) {
|
||||
throw new IllegalArgumentException("signature is required");
|
||||
}
|
||||
verify(dataToVerify, signature);
|
||||
|
||||
byte[] wrappedDek = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedDekBase64")));
|
||||
int dekLength = parseInt(envelope.get("dekLength"), 16);
|
||||
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
|
||||
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
|
||||
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
decryptRequest.setCipherBlob(wrappedDek);
|
||||
decryptRequest.setOutBufferSize(dekLength);
|
||||
byte[] sm4Key = normalize(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")));
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
|
||||
return cipher.doFinal(cipherBytes);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to unseal resource payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
new java.security.SecureRandom().nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] sm3(byte[] data) {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(data, 0, data.length);
|
||||
byte[] out = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] normalize(BackupDataResult result) {
|
||||
if (result == null || result.getData() == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
|
||||
? result.getLength()
|
||||
: result.getData().length;
|
||||
byte[] normalized = new byte[safeLength];
|
||||
System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static int parseInt(Object value, int defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildFingerprint(String prefix, int keyIndex) {
|
||||
return prefix + ":" + keyIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourceSealedPayload {
|
||||
|
||||
private byte[] cipherBytes;
|
||||
private Map<String, Object> envelope;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
|
||||
public byte[] getCipherBytes() {
|
||||
return cipherBytes;
|
||||
}
|
||||
|
||||
public void setCipherBytes(byte[] cipherBytes) {
|
||||
this.cipherBytes = cipherBytes;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEnvelope() {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void setEnvelope(Map<String, Object> envelope) {
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceDiscoveryService {
|
||||
|
||||
List<DiscoveredResourceItem> discover(InitTaskEntity initTask);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
|
||||
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiscoveredResourceItem> discover(InitTaskEntity initTask) {
|
||||
if (initTask == null) {
|
||||
throw new IllegalArgumentException("initTask is required");
|
||||
}
|
||||
String productType = upper(initTask.getProductType());
|
||||
String mqType = upper(initTask.getMqType());
|
||||
if (productType.isEmpty() || mqType.isEmpty()) {
|
||||
throw new IllegalArgumentException("productType and mqType are required");
|
||||
}
|
||||
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true));
|
||||
items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true));
|
||||
items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true));
|
||||
|
||||
if (mqType.startsWith("RABBITMQ")) {
|
||||
items.add(item("MQ", "RABBITMQ_DEFINITIONS", initExecutorProperties.getRabbitmqAdminCommand(), true));
|
||||
}
|
||||
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
|
||||
items.add(item("FILE", "TLQ_LICENSE", resolveTlqLicensePath(productType), true));
|
||||
}
|
||||
if ("RABBITMQ_CFMQ".equals(mqType)) {
|
||||
items.add(item("FILE", "CFMQ_CONFIG", initExecutorProperties.getCfmqTargetDir(), true));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private String resolveCpConfigPath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectCpconfigTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardCpconfigPath();
|
||||
}
|
||||
|
||||
private String resolveReceiverLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private String resolveTlqLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectTlqLicenseTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardTlqLicenseTargetPath();
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem item(String type, String code, String path, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType(type);
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static String upper(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源备份任务请求")
|
||||
public class CreateResourceBackupRequest {
|
||||
|
||||
@Schema(description = "是否包含升级历史", example = "true")
|
||||
private Boolean includeUpgradeHistory;
|
||||
|
||||
@Schema(description = "是否包含审计备份", example = "false")
|
||||
private Boolean includeAuditBackups;
|
||||
|
||||
@Schema(description = "备注", example = "before hardware replacement")
|
||||
private String remark;
|
||||
|
||||
public Boolean getIncludeUpgradeHistory() {
|
||||
return includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public void setIncludeUpgradeHistory(Boolean includeUpgradeHistory) {
|
||||
this.includeUpgradeHistory = includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public Boolean getIncludeAuditBackups() {
|
||||
return includeAuditBackups;
|
||||
}
|
||||
|
||||
public void setIncludeAuditBackups(Boolean includeAuditBackups) {
|
||||
this.includeAuditBackups = includeAuditBackups;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "创建资源恢复任务请求")
|
||||
public class CreateResourceRestoreRequest {
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String precheckId;
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "确认的备份号", example = "RBKP-20260408-153000-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String confirmBackupId;
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "是否确认接受恢复风险", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean confirmRiskAccepted;
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getConfirmBackupId() {
|
||||
return confirmBackupId;
|
||||
}
|
||||
|
||||
public void setConfirmBackupId(String confirmBackupId) {
|
||||
this.confirmBackupId = confirmBackupId;
|
||||
}
|
||||
|
||||
public Boolean getConfirmRiskAccepted() {
|
||||
return confirmRiskAccepted;
|
||||
}
|
||||
|
||||
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
|
||||
this.confirmRiskAccepted = confirmRiskAccepted;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "资源恢复预检请求")
|
||||
public class ResourceRestorePrecheckRequest {
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "备份包 fileId", example = "FILE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String fileId;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源备份任务响应")
|
||||
public class CreateResourceBackupResponse {
|
||||
|
||||
@Schema(description = "备份任务号", example = "RBKP-TASK-20260408-000001")
|
||||
private String taskId;
|
||||
@Schema(description = "备份号", example = "RBKP-20260408-153000-000001")
|
||||
private String backupId;
|
||||
@Schema(description = "任务状态", example = "PENDING")
|
||||
private String status;
|
||||
@Schema(description = "创建时间", example = "2026-04-08T15:30:00+08:00")
|
||||
private String createdAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源恢复任务响应")
|
||||
public class CreateResourceRestoreResponse {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源备份任务详情响应")
|
||||
public class ResourceBackupTaskDetailResponse {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String productType;
|
||||
private String mqType;
|
||||
private String orgCode;
|
||||
private String sourceDeviceId;
|
||||
private String packageName;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private Object manifestSummary;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String remark;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String createdBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getMqType() {
|
||||
return mqType;
|
||||
}
|
||||
|
||||
public void setMqType(String mqType) {
|
||||
this.mqType = mqType;
|
||||
}
|
||||
|
||||
public String getOrgCode() {
|
||||
return orgCode;
|
||||
}
|
||||
|
||||
public void setOrgCode(String orgCode) {
|
||||
this.orgCode = orgCode;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public Object getManifestSummary() {
|
||||
return manifestSummary;
|
||||
}
|
||||
|
||||
public void setManifestSummary(Object manifestSummary) {
|
||||
this.manifestSummary = manifestSummary;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源备份密钥状态响应")
|
||||
public class ResourceKeysetStatusResponse {
|
||||
|
||||
@Schema(description = "LMK 是否就绪")
|
||||
private Boolean lmkReady;
|
||||
@Schema(description = "IK 是否就绪")
|
||||
private Boolean ikReady;
|
||||
@Schema(description = "数据存储加密密钥对是否就绪")
|
||||
private Boolean storageEncKeyReady;
|
||||
@Schema(description = "数据真实性保护密钥对是否就绪")
|
||||
private Boolean authenticityKeyReady;
|
||||
@Schema(description = "数据存储加密公钥指纹")
|
||||
private String storageEncKeyFingerprint;
|
||||
@Schema(description = "真实性保护公钥指纹")
|
||||
private String authenticityKeyFingerprint;
|
||||
@Schema(description = "设备产品类型")
|
||||
private String productType;
|
||||
@Schema(description = "来源")
|
||||
private String source;
|
||||
|
||||
public Boolean getLmkReady() {
|
||||
return lmkReady;
|
||||
}
|
||||
|
||||
public void setLmkReady(Boolean lmkReady) {
|
||||
this.lmkReady = lmkReady;
|
||||
}
|
||||
|
||||
public Boolean getIkReady() {
|
||||
return ikReady;
|
||||
}
|
||||
|
||||
public void setIkReady(Boolean ikReady) {
|
||||
this.ikReady = ikReady;
|
||||
}
|
||||
|
||||
public Boolean getStorageEncKeyReady() {
|
||||
return storageEncKeyReady;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyReady(Boolean storageEncKeyReady) {
|
||||
this.storageEncKeyReady = storageEncKeyReady;
|
||||
}
|
||||
|
||||
public Boolean getAuthenticityKeyReady() {
|
||||
return authenticityKeyReady;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyReady(Boolean authenticityKeyReady) {
|
||||
this.authenticityKeyReady = authenticityKeyReady;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "资源恢复预检响应")
|
||||
public class ResourceRestorePrecheckResponse {
|
||||
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String sourceProductType;
|
||||
private String sourceMqType;
|
||||
private String sourceOrgCode;
|
||||
private String sourceBackupTime;
|
||||
private String targetProductType;
|
||||
private Boolean keysetMatched;
|
||||
private Boolean signatureValid;
|
||||
private Boolean compatible;
|
||||
private Boolean payloadDecrypted;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private List<String> restorePlan;
|
||||
private Object restoreScopeSummary;
|
||||
private String expiresAt;
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getSourceMqType() {
|
||||
return sourceMqType;
|
||||
}
|
||||
|
||||
public void setSourceMqType(String sourceMqType) {
|
||||
this.sourceMqType = sourceMqType;
|
||||
}
|
||||
|
||||
public String getSourceOrgCode() {
|
||||
return sourceOrgCode;
|
||||
}
|
||||
|
||||
public void setSourceOrgCode(String sourceOrgCode) {
|
||||
this.sourceOrgCode = sourceOrgCode;
|
||||
}
|
||||
|
||||
public String getSourceBackupTime() {
|
||||
return sourceBackupTime;
|
||||
}
|
||||
|
||||
public void setSourceBackupTime(String sourceBackupTime) {
|
||||
this.sourceBackupTime = sourceBackupTime;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public Boolean getKeysetMatched() {
|
||||
return keysetMatched;
|
||||
}
|
||||
|
||||
public void setKeysetMatched(Boolean keysetMatched) {
|
||||
this.keysetMatched = keysetMatched;
|
||||
}
|
||||
|
||||
public Boolean getSignatureValid() {
|
||||
return signatureValid;
|
||||
}
|
||||
|
||||
public void setSignatureValid(Boolean signatureValid) {
|
||||
this.signatureValid = signatureValid;
|
||||
}
|
||||
|
||||
public Boolean getCompatible() {
|
||||
return compatible;
|
||||
}
|
||||
|
||||
public void setCompatible(Boolean compatible) {
|
||||
this.compatible = compatible;
|
||||
}
|
||||
|
||||
public Boolean getPayloadDecrypted() {
|
||||
return payloadDecrypted;
|
||||
}
|
||||
|
||||
public void setPayloadDecrypted(Boolean payloadDecrypted) {
|
||||
this.payloadDecrypted = payloadDecrypted;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public List<String> getRestorePlan() {
|
||||
return restorePlan;
|
||||
}
|
||||
|
||||
public void setRestorePlan(List<String> restorePlan) {
|
||||
this.restorePlan = restorePlan;
|
||||
}
|
||||
|
||||
public Object getRestoreScopeSummary() {
|
||||
return restoreScopeSummary;
|
||||
}
|
||||
|
||||
public void setRestoreScopeSummary(Object restoreScopeSummary) {
|
||||
this.restoreScopeSummary = restoreScopeSummary;
|
||||
}
|
||||
|
||||
public String getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(String expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "资源恢复任务详情响应")
|
||||
public class ResourceRestoreTaskDetailResponse {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private Boolean packagePathVisible;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private String runnerStatePath;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private List<String> restorePlan;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String createdBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getControlPhase() {
|
||||
return controlPhase;
|
||||
}
|
||||
|
||||
public void setControlPhase(String controlPhase) {
|
||||
this.controlPhase = controlPhase;
|
||||
}
|
||||
|
||||
public String getStateSource() {
|
||||
return stateSource;
|
||||
}
|
||||
|
||||
public void setStateSource(String stateSource) {
|
||||
this.stateSource = stateSource;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Boolean getPackagePathVisible() {
|
||||
return packagePathVisible;
|
||||
}
|
||||
|
||||
public void setPackagePathVisible(Boolean packagePathVisible) {
|
||||
this.packagePathVisible = packagePathVisible;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getLastHeartbeatAt() {
|
||||
return lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public void setLastHeartbeatAt(String lastHeartbeatAt) {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
public void setOrphaned(Boolean orphaned) {
|
||||
this.orphaned = orphaned;
|
||||
}
|
||||
|
||||
public List<String> getCleanupWarnings() {
|
||||
return cleanupWarnings;
|
||||
}
|
||||
|
||||
public void setCleanupWarnings(List<String> cleanupWarnings) {
|
||||
this.cleanupWarnings = cleanupWarnings;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public List<String> getRestorePlan() {
|
||||
return restorePlan;
|
||||
}
|
||||
|
||||
public void setRestorePlan(List<String> restorePlan) {
|
||||
this.restorePlan = restorePlan;
|
||||
}
|
||||
|
||||
public Object getConsistencyReport() {
|
||||
return consistencyReport;
|
||||
}
|
||||
|
||||
public void setConsistencyReport(Object consistencyReport) {
|
||||
this.consistencyReport = consistencyReport;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源任务步骤项响应")
|
||||
public class ResourceTaskStepItemResponse {
|
||||
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String status;
|
||||
private String message;
|
||||
private Integer exitCode;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
private Boolean logAvailable;
|
||||
|
||||
public Integer getStepNo() {
|
||||
return stepNo;
|
||||
}
|
||||
|
||||
public void setStepNo(Integer stepNo) {
|
||||
this.stepNo = stepNo;
|
||||
}
|
||||
|
||||
public String getStepCode() {
|
||||
return stepCode;
|
||||
}
|
||||
|
||||
public void setStepCode(String stepCode) {
|
||||
this.stepCode = stepCode;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Integer getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(Integer exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
|
||||
public Boolean getLogAvailable() {
|
||||
return logAvailable;
|
||||
}
|
||||
|
||||
public void setLogAvailable(Boolean logAvailable) {
|
||||
this.logAvailable = logAvailable;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源任务步骤日志响应")
|
||||
public class ResourceTaskStepLogResponse {
|
||||
|
||||
private String taskId;
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String content;
|
||||
private Boolean truncated;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Integer getStepNo() {
|
||||
return stepNo;
|
||||
}
|
||||
|
||||
public void setStepNo(Integer stepNo) {
|
||||
this.stepNo = stepNo;
|
||||
}
|
||||
|
||||
public String getStepCode() {
|
||||
return stepCode;
|
||||
}
|
||||
|
||||
public void setStepCode(String stepCode) {
|
||||
this.stepCode = stepCode;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Boolean getTruncated() {
|
||||
return truncated;
|
||||
}
|
||||
|
||||
public void setTruncated(Boolean truncated) {
|
||||
this.truncated = truncated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
public class DiscoveredResourceItem {
|
||||
|
||||
private String resourceType;
|
||||
private String resourceCode;
|
||||
private String resolvedPath;
|
||||
private boolean required;
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String resourceType) {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public String getResourceCode() {
|
||||
return resourceCode;
|
||||
}
|
||||
|
||||
public void setResourceCode(String resourceCode) {
|
||||
this.resourceCode = resourceCode;
|
||||
}
|
||||
|
||||
public String getResolvedPath() {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public void setResolvedPath(String resolvedPath) {
|
||||
this.resolvedPath = resolvedPath;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.cisd.tms.modules.backup.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_resource_backup_task")
|
||||
public class ResourceBackupTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String productType;
|
||||
private String mqType;
|
||||
private String orgCode;
|
||||
private String sourceDeviceId;
|
||||
private String packageName;
|
||||
private String packagePath;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String manifestSummaryJson;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getMqType() {
|
||||
return mqType;
|
||||
}
|
||||
|
||||
public void setMqType(String mqType) {
|
||||
this.mqType = mqType;
|
||||
}
|
||||
|
||||
public String getOrgCode() {
|
||||
return orgCode;
|
||||
}
|
||||
|
||||
public void setOrgCode(String orgCode) {
|
||||
this.orgCode = orgCode;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getManifestSummaryJson() {
|
||||
return manifestSummaryJson;
|
||||
}
|
||||
|
||||
public void setManifestSummaryJson(String manifestSummaryJson) {
|
||||
this.manifestSummaryJson = manifestSummaryJson;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(LocalDateTime finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.cisd.tms.modules.backup.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_resource_restore_task")
|
||||
public class ResourceRestoreTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String restorePlanJson;
|
||||
private String consistencyReportJson;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public String getStagingDir() {
|
||||
return stagingDir;
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
this.stagingDir = stagingDir;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
|
||||
public String getConsistencyReportJson() {
|
||||
return consistencyReportJson;
|
||||
}
|
||||
|
||||
public void setConsistencyReportJson(String consistencyReportJson) {
|
||||
this.consistencyReportJson = consistencyReportJson;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(LocalDateTime finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.cisd.tms.modules.backup.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_resource_task_step")
|
||||
public class ResourceTaskStepEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String taskType;
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String status;
|
||||
private String message;
|
||||
private String logPath;
|
||||
private Integer exitCode;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public Integer getStepNo() {
|
||||
return stepNo;
|
||||
}
|
||||
|
||||
public void setStepNo(Integer stepNo) {
|
||||
this.stepNo = stepNo;
|
||||
}
|
||||
|
||||
public String getStepCode() {
|
||||
return stepCode;
|
||||
}
|
||||
|
||||
public void setStepCode(String stepCode) {
|
||||
this.stepCode = stepCode;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getLogPath() {
|
||||
return logPath;
|
||||
}
|
||||
|
||||
public void setLogPath(String logPath) {
|
||||
this.logPath = logPath;
|
||||
}
|
||||
|
||||
public Integer getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(Integer exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(LocalDateTime finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 恢复任务控制阶段。
|
||||
*/
|
||||
public enum ResourceControlPhase {
|
||||
TMS_PRECHECK,
|
||||
HANDOFF_READY,
|
||||
RUNNER_OWNED,
|
||||
RECONCILE_PENDING,
|
||||
DONE
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务步骤状态。
|
||||
*/
|
||||
public enum ResourceStepStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
SUCCESS_WITH_WARNINGS,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源备份/恢复任务主状态。
|
||||
*/
|
||||
public enum ResourceTaskStatus {
|
||||
PENDING,
|
||||
PRECHECKING,
|
||||
PRECHECK_FAILED,
|
||||
READY,
|
||||
HANDOFF,
|
||||
RUNNING,
|
||||
VERIFYING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务类型。
|
||||
*/
|
||||
public enum ResourceTaskType {
|
||||
RESOURCE_BACKUP,
|
||||
RESOURCE_RESTORE
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceBackupTaskMapper extends BaseMapperX<ResourceBackupTaskEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, backup_id, status, product_type, mq_type, org_code, source_device_id, "
|
||||
+ "package_name, package_path, package_size, package_hash, storage_enc_key_fingerprint, "
|
||||
+ "authenticity_key_fingerprint, manifest_summary_json, error_code, error_message, remark, "
|
||||
+ "create_by, create_time, update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_backup_task WHERE task_id = #{taskId}"
|
||||
)
|
||||
ResourceBackupTaskEntity selectByTaskId(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceRestoreTaskMapper extends BaseMapperX<ResourceRestoreTaskEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, precheck_id, backup_id, status, source_device_id, source_product_type, "
|
||||
+ "target_product_type, package_file_id, package_path, staging_dir, runner_state_path, "
|
||||
+ "storage_enc_key_fingerprint, authenticity_key_fingerprint, restore_plan_json, "
|
||||
+ "consistency_report_json, error_code, error_message, remark, create_by, create_time, "
|
||||
+ "update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_restore_task WHERE task_id = #{taskId}"
|
||||
)
|
||||
ResourceRestoreTaskEntity selectByTaskId(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceTaskStepMapper extends BaseMapperX<ResourceTaskStepEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, task_type, step_no, step_code, status, message, log_path, exit_code, "
|
||||
+ "create_time, update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_task_step WHERE task_id = #{taskId} ORDER BY step_no ASC"
|
||||
)
|
||||
List<ResourceTaskStepEntity> selectByTaskIdOrderByStepNo(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
final class ResourcePackageFormatSupport {
|
||||
|
||||
private ResourcePackageFormatSupport() {
|
||||
}
|
||||
|
||||
static byte[] buildSignatureSource(byte[] manifestBytes, byte[] envelopeBytes, byte[] cipherBytes) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
writePart(outputStream, manifestBytes);
|
||||
writePart(outputStream, envelopeBytes);
|
||||
writePart(outputStream, cipherBytes);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to build package signature source", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePart(ByteArrayOutputStream outputStream, byte[] data) throws IOException {
|
||||
byte[] safeData = data == null ? new byte[0] : data;
|
||||
outputStream.write(ByteBuffer.allocate(4).putInt(safeData.length).array());
|
||||
outputStream.write(safeData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourcePackageInspectResult {
|
||||
|
||||
private Map<String, Object> manifest;
|
||||
private Map<String, Object> envelope;
|
||||
private Map<String, Object> payload;
|
||||
private byte[] payloadArchiveBytes;
|
||||
private byte[] signature;
|
||||
|
||||
public Map<String, Object> getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public void setManifest(Map<String, Object> manifest) {
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEnvelope() {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void setEnvelope(Map<String, Object> envelope) {
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public byte[] getPayloadArchiveBytes() {
|
||||
return payloadArchiveBytes;
|
||||
}
|
||||
|
||||
public void setPayloadArchiveBytes(byte[] payloadArchiveBytes) {
|
||||
this.payloadArchiveBytes = payloadArchiveBytes;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public void setSignature(byte[] signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public class ResourcePackageResult {
|
||||
|
||||
private String packageName;
|
||||
private String packagePath;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String manifestSummaryJson;
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getManifestSummaryJson() {
|
||||
return manifestSummaryJson;
|
||||
}
|
||||
|
||||
public void setManifestSummaryJson(String manifestSummaryJson) {
|
||||
this.manifestSummaryJson = manifestSummaryJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourcePackageService {
|
||||
|
||||
ResourcePackageResult createPackage(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
);
|
||||
|
||||
ResourcePackageInspectResult inspectPackageHeader(Path packagePath);
|
||||
|
||||
ResourcePackageInspectResult inspectPackage(Path packagePath);
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoService;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceSealedPayload;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageResult createPackage(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
) {
|
||||
if (isBlank(taskId) || isBlank(backupId)) {
|
||||
throw new IllegalArgumentException("taskId and backupId are required");
|
||||
}
|
||||
if (initTask == null) {
|
||||
throw new IllegalArgumentException("initTask is required");
|
||||
}
|
||||
if (discoveredItems == null) {
|
||||
throw new IllegalArgumentException("discoveredItems is required");
|
||||
}
|
||||
try {
|
||||
Path taskDir = Path.of(resourceBackupProperties.getOutputDir()).resolve(taskId).normalize();
|
||||
Files.createDirectories(taskDir);
|
||||
|
||||
// 资源恢复真正消费的是“受保护的内部归档”,而不是一段摘要 JSON。
|
||||
// 这里先把可采集的文件资源打成 payload.zip,再统一交给数字信封加密,
|
||||
// 后续即使补数据库导出/MQ definitions,也仍然沿着这个边界扩展。
|
||||
PayloadArchiveBuildResult payloadArchive = buildPayloadArchive(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
discoveredItems,
|
||||
request
|
||||
);
|
||||
ResourceSealedPayload sealedPayload = resourceEnvelopeCryptoService.seal(payloadArchive.archiveBytes());
|
||||
|
||||
// manifest 只保留恢复前需要快速识别的元数据,不直接展开敏感资源内容。
|
||||
Map<String, Object> manifest = new LinkedHashMap<>();
|
||||
manifest.put("backupId", backupId);
|
||||
manifest.put("taskId", taskId);
|
||||
manifest.put("backupTime", LocalDateTime.now().toString());
|
||||
manifest.put("productType", initTask.getProductType());
|
||||
manifest.put("mqType", initTask.getMqType());
|
||||
manifest.put("orgCode", initTask.getOrgCode());
|
||||
manifest.put("resourceCount", discoveredItems.size());
|
||||
manifest.put("collectedResourceCount", payloadArchive.collectedResourceCount());
|
||||
manifest.put("storageEncKeyFingerprint", sealedPayload.getStorageEncKeyFingerprint());
|
||||
manifest.put("authenticityKeyFingerprint", sealedPayload.getAuthenticityKeyFingerprint());
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] envelopeBytes = objectMapper.writeValueAsBytes(sealedPayload.getEnvelope());
|
||||
// 签名覆盖 manifest + envelope + ciphertext 三段,恢复端按同一字节拼装规则重建验签输入。
|
||||
byte[] signatureBytes = resourceEnvelopeCryptoService.sign(
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, sealedPayload.getCipherBytes())
|
||||
);
|
||||
|
||||
String packageName = backupId + ".tmsbak";
|
||||
Path packagePath = taskDir.resolve(packageName);
|
||||
try (OutputStream outputStream = Files.newOutputStream(packagePath);
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
// 当前包结构已经固定成 manifest + envelope + ciphertext,后续即使替换加密实现也不必重做外层格式。
|
||||
addZipEntry(zipOutputStream, "manifest.json", manifestBytes);
|
||||
addZipEntry(zipOutputStream, "envelope.json", envelopeBytes);
|
||||
addZipEntry(zipOutputStream, "payload.enc", sealedPayload.getCipherBytes());
|
||||
addZipEntry(zipOutputStream, "signature.sig", signatureBytes);
|
||||
}
|
||||
|
||||
Map<String, Object> manifestSummary = new LinkedHashMap<>();
|
||||
manifestSummary.put("resourceCount", discoveredItems.size());
|
||||
manifestSummary.put("collectedResourceCount", payloadArchive.collectedResourceCount());
|
||||
manifestSummary.put("productType", initTask.getProductType());
|
||||
manifestSummary.put("mqType", initTask.getMqType());
|
||||
manifestSummary.put("orgCode", initTask.getOrgCode());
|
||||
manifestSummary.put("packageName", packageName);
|
||||
|
||||
ResourcePackageResult result = new ResourcePackageResult();
|
||||
result.setPackageName(packageName);
|
||||
result.setPackagePath(packagePath.toString());
|
||||
result.setPackageSize(Files.size(packagePath));
|
||||
// 这里记录的是整个备份包文件的 SM3,方便后续下载校验和审计追踪。
|
||||
result.setPackageHash(sm3Hex(Files.readAllBytes(packagePath)));
|
||||
result.setStorageEncKeyFingerprint(sealedPayload.getStorageEncKeyFingerprint());
|
||||
result.setAuthenticityKeyFingerprint(sealedPayload.getAuthenticityKeyFingerprint());
|
||||
result.setManifestSummaryJson(objectMapper.writeValueAsString(manifestSummary));
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to create backup package", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageInspectResult inspectPackageHeader(Path packagePath) {
|
||||
if (packagePath == null) {
|
||||
throw new IllegalArgumentException("packagePath is required");
|
||||
}
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
|
||||
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
|
||||
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
|
||||
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
|
||||
resourceEnvelopeCryptoService.verify(
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
|
||||
signatureBytes
|
||||
);
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setSignature(signatureBytes);
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to inspect backup package header", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageInspectResult inspectPackage(Path packagePath) {
|
||||
if (packagePath == null) {
|
||||
throw new IllegalArgumentException("packagePath is required");
|
||||
}
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
|
||||
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
|
||||
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
|
||||
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
|
||||
byte[] payloadBytes = resourceEnvelopeCryptoService.unsealAndVerify(
|
||||
objectMapper.readValue(envelopeBytes, new TypeReference<Map<String, Object>>() {
|
||||
}),
|
||||
cipherBytes,
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
|
||||
signatureBytes
|
||||
);
|
||||
Map<String, Object> payloadManifest = readPayloadManifest(payloadBytes);
|
||||
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setPayload(payloadManifest);
|
||||
result.setPayloadArchiveBytes(payloadBytes);
|
||||
result.setSignature(signatureBytes);
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to inspect backup package", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private PayloadArchiveBuildResult buildPayloadArchive(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
) throws IOException {
|
||||
Map<String, Object> payloadManifest = new LinkedHashMap<>();
|
||||
payloadManifest.put("archiveFormat", "ZIP");
|
||||
payloadManifest.put("backupId", backupId);
|
||||
payloadManifest.put("taskId", taskId);
|
||||
payloadManifest.put("productType", initTask.getProductType());
|
||||
payloadManifest.put("mqType", initTask.getMqType());
|
||||
payloadManifest.put("orgCode", initTask.getOrgCode());
|
||||
payloadManifest.put("remark", request == null ? null : request.getRemark());
|
||||
payloadManifest.put("includeUpgradeHistory", request != null && Boolean.TRUE.equals(request.getIncludeUpgradeHistory()));
|
||||
payloadManifest.put("includeAuditBackups", request != null && Boolean.TRUE.equals(request.getIncludeAuditBackups()));
|
||||
|
||||
List<Map<String, Object>> resourceEntries = new ArrayList<>();
|
||||
int collectedCount = 0;
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
for (DiscoveredResourceItem item : discoveredItems) {
|
||||
Map<String, Object> resourceEntry = new LinkedHashMap<>();
|
||||
resourceEntry.put("resourceType", item.getResourceType());
|
||||
resourceEntry.put("resourceCode", item.getResourceCode());
|
||||
resourceEntry.put("resolvedPath", item.getResolvedPath());
|
||||
resourceEntry.put("required", item.isRequired());
|
||||
|
||||
int collectedFiles = collectResourceItem(zipOutputStream, item, resourceEntry);
|
||||
if (collectedFiles > 0) {
|
||||
collectedCount++;
|
||||
}
|
||||
resourceEntries.add(resourceEntry);
|
||||
}
|
||||
payloadManifest.put("resources", resourceEntries);
|
||||
addZipEntry(
|
||||
zipOutputStream,
|
||||
"payload-manifest.json",
|
||||
objectMapper.writeValueAsBytes(payloadManifest)
|
||||
);
|
||||
}
|
||||
return new PayloadArchiveBuildResult(outputStream.toByteArray(), collectedCount);
|
||||
}
|
||||
|
||||
private int collectResourceItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
return 0;
|
||||
}
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> archivedFiles = new ArrayList<>();
|
||||
if (Files.isRegularFile(resolvedPath)) {
|
||||
archivedFiles.add(writeRegularFileEntry(zipOutputStream, item, resolvedPath, resolvedPath.getFileName().toString()));
|
||||
} else if (Files.isDirectory(resolvedPath)) {
|
||||
// 目录资源恢复时真正需要的是内部文件集合,而不是目录本身的绝对路径。
|
||||
// 这里把目录拍平成 zip entry,并把相对路径写进 payload-manifest,后续恢复端按 manifest 还原。
|
||||
try (Stream<Path> pathStream = Files.walk(resolvedPath)) {
|
||||
pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.forEach(path -> archivedFiles.add(writeUnchecked(
|
||||
() -> writeRegularFileEntry(
|
||||
zipOutputStream,
|
||||
item,
|
||||
path,
|
||||
resolvedPath.relativize(path).toString()
|
||||
)
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "UNSUPPORTED_FILE_TYPE");
|
||||
return 0;
|
||||
}
|
||||
|
||||
resourceEntry.put("collected", !archivedFiles.isEmpty());
|
||||
resourceEntry.put("fileCount", archivedFiles.size());
|
||||
resourceEntry.put("files", archivedFiles);
|
||||
if (archivedFiles.size() == 1) {
|
||||
resourceEntry.put("entryPath", archivedFiles.get(0).get("entryPath"));
|
||||
}
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private Map<String, Object> writeRegularFileEntry(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Path file,
|
||||
String relativeName
|
||||
) throws IOException {
|
||||
byte[] content = Files.readAllBytes(file);
|
||||
String entryPath = buildArchiveEntryPath(item, relativeName);
|
||||
addZipEntry(zipOutputStream, entryPath, content);
|
||||
|
||||
Map<String, Object> fileEntry = new LinkedHashMap<>();
|
||||
fileEntry.put("entryPath", entryPath);
|
||||
fileEntry.put("sourcePath", file.toString());
|
||||
fileEntry.put("relativeName", relativeName.replace('\\', '/'));
|
||||
fileEntry.put("size", content.length);
|
||||
fileEntry.put("contentSm3", sm3Hex(content));
|
||||
return fileEntry;
|
||||
}
|
||||
|
||||
private Map<String, Object> readPayloadManifest(byte[] payloadArchiveBytes) throws IOException {
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
if ("payload-manifest.json".equals(entry.getName())) {
|
||||
return objectMapper.readValue(zipInputStream.readAllBytes(), new TypeReference<HashMap<String, Object>>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("payload archive entry missing: payload-manifest.json");
|
||||
}
|
||||
|
||||
private static String buildArchiveEntryPath(DiscoveredResourceItem item, String relativeName) {
|
||||
String code = trim(item.getResourceCode()).isEmpty() ? "UNKNOWN" : trim(item.getResourceCode());
|
||||
String normalizedRelativeName = trim(relativeName).replace('\\', '/');
|
||||
if (normalizedRelativeName.isEmpty()) {
|
||||
normalizedRelativeName = "content.bin";
|
||||
}
|
||||
return "resources/" + code + "/" + normalizedRelativeName;
|
||||
}
|
||||
|
||||
private static Path toResolvedPath(String path) {
|
||||
String value = trim(path);
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Path.of(value).normalize();
|
||||
}
|
||||
|
||||
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write payload archive entry", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws IOException {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(entryName));
|
||||
zipOutputStream.write(content);
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
private static byte[] readRequiredEntry(ZipFile zipFile, String entryName) throws IOException {
|
||||
ZipEntry entry = zipFile.getEntry(entryName);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("backup package entry missing: " + entryName);
|
||||
}
|
||||
try (InputStream inputStream = zipFile.getInputStream(entry)) {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static String sm3Hex(byte[] data) {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(data, 0, data.length);
|
||||
byte[] out = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(out, 0);
|
||||
StringBuilder builder = new StringBuilder(out.length * 2);
|
||||
for (byte item : out) {
|
||||
builder.append(String.format("%02x", item));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface CheckedSupplier<T> {
|
||||
T get() throws IOException;
|
||||
}
|
||||
|
||||
private record PayloadArchiveBuildResult(byte[] archiveBytes, int collectedResourceCount) {
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourceBackupTaskRepository {
|
||||
|
||||
void save(ResourceBackupTaskEntity entity);
|
||||
|
||||
Optional<ResourceBackupTaskEntity> findByTaskId(String taskId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourceRestoreTaskRepository {
|
||||
|
||||
void save(ResourceRestoreTaskEntity entity);
|
||||
|
||||
void update(ResourceRestoreTaskEntity entity);
|
||||
|
||||
Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId);
|
||||
|
||||
List<ResourceRestoreTaskEntity> findAll();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceTaskStepRepository {
|
||||
|
||||
void save(ResourceTaskStepEntity entity);
|
||||
|
||||
List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceBackupTaskRepositoryImpl implements ResourceBackupTaskRepository {
|
||||
|
||||
private final ResourceBackupTaskMapper resourceBackupTaskMapper;
|
||||
|
||||
public ResourceBackupTaskRepositoryImpl(ResourceBackupTaskMapper resourceBackupTaskMapper) {
|
||||
this.resourceBackupTaskMapper = resourceBackupTaskMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceBackupTaskEntity entity) {
|
||||
resourceBackupTaskMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourceBackupTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.ofNullable(resourceBackupTaskMapper.selectByTaskId(taskId));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceRestoreTaskMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceRestoreTaskRepositoryImpl implements ResourceRestoreTaskRepository {
|
||||
|
||||
private final ResourceRestoreTaskMapper resourceRestoreTaskMapper;
|
||||
|
||||
public ResourceRestoreTaskRepositoryImpl(ResourceRestoreTaskMapper resourceRestoreTaskMapper) {
|
||||
this.resourceRestoreTaskMapper = resourceRestoreTaskMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceRestoreTaskEntity entity) {
|
||||
resourceRestoreTaskMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ResourceRestoreTaskEntity entity) {
|
||||
resourceRestoreTaskMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.ofNullable(resourceRestoreTaskMapper.selectByTaskId(taskId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceRestoreTaskEntity> findAll() {
|
||||
List<ResourceRestoreTaskEntity> tasks = resourceRestoreTaskMapper.selectList(null);
|
||||
return tasks == null ? List.of() : tasks;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceTaskStepRepository;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceTaskStepRepositoryImpl implements ResourceTaskStepRepository {
|
||||
|
||||
private final ResourceTaskStepMapper resourceTaskStepMapper;
|
||||
|
||||
public ResourceTaskStepRepositoryImpl(ResourceTaskStepMapper resourceTaskStepMapper) {
|
||||
this.resourceTaskStepMapper = resourceTaskStepMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceTaskStepEntity entity) {
|
||||
resourceTaskStepMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId) {
|
||||
List<ResourceTaskStepEntity> steps = resourceTaskStepMapper.selectByTaskIdOrderByStepNo(taskId);
|
||||
return steps == null ? List.of() : steps;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AsyncResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
|
||||
|
||||
private final ResourceRestoreRunner resourceRestoreRunner;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public AsyncResourceRestoreRunnerLauncher(
|
||||
ResourceRestoreRunner resourceRestoreRunner,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.resourceRestoreRunner = resourceRestoreRunner;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Path handoffFile) {
|
||||
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
|
||||
return;
|
||||
}
|
||||
Thread thread = new Thread(() -> resourceRestoreRunner.run(handoffFile), "resource-restore-runner");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PayloadArchiveRunnerStepExecutor(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_FILES".equals(stepCode) || "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if ("RESTORE_FILES".equals(context.stepCode())) {
|
||||
return restoreFiles(context);
|
||||
}
|
||||
if ("CONSISTENCY_VERIFY".equals(context.stepCode())) {
|
||||
return verifyExtractedPayload(context);
|
||||
}
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult restoreFiles(RunnerStepContext context) {
|
||||
ResourceRestoreHandoffSnapshot handoff = readRequiredHandoff(context.handoffFile());
|
||||
Path packagePath = resolveRequiredPackagePath(handoff);
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
|
||||
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
|
||||
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
|
||||
Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize();
|
||||
extractPayloadArchive(payloadArchiveBytes, payloadDir);
|
||||
|
||||
int resourceCount = readResourceCount(payloadDir.resolve("payload-manifest.json"));
|
||||
return RunnerStepExecutionResult.success("payload extracted to staging, resourceCount=" + resourceCount);
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult verifyExtractedPayload(RunnerStepContext context) {
|
||||
Path manifestFile = context.taskDir().resolve("staging").resolve("payload").resolve("payload-manifest.json").normalize();
|
||||
if (!Files.isRegularFile(manifestFile)) {
|
||||
throw new IllegalStateException("payload manifest missing: " + manifestFile);
|
||||
}
|
||||
int resourceCount = readResourceCount(manifestFile);
|
||||
return RunnerStepExecutionResult.success("payload manifest verified, resourceCount=" + resourceCount);
|
||||
}
|
||||
|
||||
private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) {
|
||||
return handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
}
|
||||
|
||||
private Path resolveRequiredPackagePath(ResourceRestoreHandoffSnapshot handoff) {
|
||||
String packagePath = trim(handoff.getPackagePath());
|
||||
if (packagePath.isEmpty()) {
|
||||
throw new IllegalStateException("restore handoff packagePath is empty");
|
||||
}
|
||||
Path resolved = Path.of(packagePath).normalize();
|
||||
if (!Files.isRegularFile(resolved)) {
|
||||
throw new IllegalStateException("restore package file not found: " + resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) {
|
||||
try {
|
||||
Files.createDirectories(payloadDir);
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
Path target = resolveSafePayloadTarget(payloadDir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(target);
|
||||
continue;
|
||||
}
|
||||
Files.createDirectories(target.getParent());
|
||||
// 备份包来自外部输入,即使已经验签,也要防御 zip-slip,确保只落到 staging/payload 下。
|
||||
Files.write(target, readAllBytes(zipInputStream));
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to extract payload archive", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
|
||||
Path target = payloadDir.resolve(entryName).normalize();
|
||||
if (!target.startsWith(payloadDir)) {
|
||||
throw new IllegalStateException("payload archive entry escapes staging dir: " + entryName);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private int readResourceCount(Path manifestFile) {
|
||||
try {
|
||||
Map<String, Object> manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (resourcesValue instanceof List<?> resources) {
|
||||
return resources.size();
|
||||
}
|
||||
return 0;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read payload manifest: " + manifestFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker {
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
try {
|
||||
if (logFile != null && logFile.getParent() != null) {
|
||||
Files.createDirectories(logFile.getParent());
|
||||
}
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
if (workingDirectory != null) {
|
||||
Files.createDirectories(workingDirectory);
|
||||
pb.directory(workingDirectory.toFile());
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
if (logFile != null) {
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
}
|
||||
Process process = pb.start();
|
||||
return process.waitFor();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to start runner command: " + String.join(" ", command), ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("runner command interrupted: " + String.join(" ", command), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreRunner {
|
||||
|
||||
private static final String INITIAL_STEP_CODE = "RESTORE_LOCK";
|
||||
private static final int INITIAL_STEP_NO = 1;
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final List<RunnerStepExecutor> runnerStepExecutors;
|
||||
|
||||
public ResourceRestoreRunner(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper,
|
||||
List<RunnerStepExecutor> runnerStepExecutors
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.objectMapper = objectMapper;
|
||||
this.runnerStepExecutors = runnerStepExecutors;
|
||||
}
|
||||
|
||||
public void run(Path handoffFile) {
|
||||
ResourceRestoreHandoffSnapshot handoff = handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
Path stateFile = Path.of(handoff.getRunnerStatePath()).normalize();
|
||||
String runnerInstanceId = nextRunnerInstanceId();
|
||||
long runnerPid = ProcessHandle.current().pid();
|
||||
resourceRestoreStateFileStore.claimOwnership(stateFile, runnerInstanceId, runnerPid, INITIAL_STEP_CODE, INITIAL_STEP_NO);
|
||||
|
||||
List<String> restorePlan = readRestorePlan(handoff.getRestorePlanJson());
|
||||
try {
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
if (INITIAL_STEP_CODE.equals(stepCode)) {
|
||||
continue;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStepRunning(stateFile, stepCode, stepNo, runnerInstanceId);
|
||||
RunnerStepExecutor executor = resolveExecutor(stepCode);
|
||||
RunnerStepContext context = new RunnerStepContext(
|
||||
handoff.getTaskId(),
|
||||
stepCode,
|
||||
stepNo,
|
||||
handoffFile.getParent(),
|
||||
handoffFile,
|
||||
stateFile
|
||||
);
|
||||
RunnerStepExecutionResult result = executor.execute(context);
|
||||
writeStepOutput(context, result);
|
||||
resourceRestoreStateFileStore.touchHeartbeat(stateFile, runnerInstanceId);
|
||||
}
|
||||
String lastStepCode = restorePlan.isEmpty() ? INITIAL_STEP_CODE : restorePlan.get(restorePlan.size() - 1);
|
||||
int lastStepNo = restorePlan.isEmpty() ? INITIAL_STEP_NO : restorePlan.size();
|
||||
resourceRestoreStateFileStore.markReconcilePending(stateFile, "SUCCESS", null, null, lastStepCode, lastStepNo, runnerInstanceId);
|
||||
} catch (RuntimeException ex) {
|
||||
int failedStepNo = detectFailedStepNo(handoff, stateFile);
|
||||
String failedStepCode = detectFailedStepCode(handoff, stateFile);
|
||||
writeStepFailureOutput(handoffFile.getParent(), failedStepCode, failedStepNo, ex);
|
||||
resourceRestoreStateFileStore.markReconcilePending(
|
||||
stateFile,
|
||||
"FAILED",
|
||||
"RUNNER_EXECUTION_FAILED",
|
||||
ex.getMessage(),
|
||||
failedStepCode,
|
||||
failedStepNo,
|
||||
runnerInstanceId
|
||||
);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String nextRunnerInstanceId() {
|
||||
return "RRUN-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
private List<String> readRestorePlan(String restorePlanJson) {
|
||||
if (restorePlanJson == null || restorePlanJson.isBlank()) {
|
||||
return List.of(INITIAL_STEP_CODE);
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(restorePlanJson, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse restore plan json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private RunnerStepExecutor resolveExecutor(String stepCode) {
|
||||
return runnerStepExecutors.stream()
|
||||
.filter(executor -> executor.supports(stepCode))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("no runner step executor for step: " + stepCode));
|
||||
}
|
||||
|
||||
private int detectFailedStepNo(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepNo() == null ? INITIAL_STEP_NO : snapshot.getCurrentStepNo())
|
||||
.orElse(INITIAL_STEP_NO);
|
||||
}
|
||||
|
||||
private String detectFailedStepCode(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepCode() == null || snapshot.getCurrentStepCode().isBlank()
|
||||
? INITIAL_STEP_CODE
|
||||
: snapshot.getCurrentStepCode())
|
||||
.orElse(INITIAL_STEP_CODE);
|
||||
}
|
||||
|
||||
private void writeStepOutput(RunnerStepContext context, RunnerStepExecutionResult result) {
|
||||
try {
|
||||
Path outputDir = context.taskDir().resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", context.stepNo(), context.stepCode()));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", context.stepCode());
|
||||
json.put("result", result.getResult());
|
||||
json.put("summary", result.getSummary());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write step output for " + context.stepCode(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStepFailureOutput(Path taskDir, String stepCode, int stepNo, RuntimeException ex) {
|
||||
try {
|
||||
Path outputDir = taskDir.resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", stepCode);
|
||||
json.put("result", "FAILED");
|
||||
json.put("summary", ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ioException) {
|
||||
throw new IllegalStateException("failed to write failed step output for " + stepCode, ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceRestoreRunnerLauncher {
|
||||
|
||||
void launch(Path handoffFile);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RestoreStateReconciler {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
public RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void reconcileOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
java.nio.file.Path stateFile = java.nio.file.Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RECONCILE_PENDING".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
entity.setStatus(snapshot.getStatus());
|
||||
entity.setErrorCode(blankToNull(snapshot.getErrorCode()));
|
||||
entity.setErrorMessage(blankToNull(snapshot.getErrorMessage()));
|
||||
entity.setFinishTime(nowSupplier.get());
|
||||
entity.setUpdateTime(nowSupplier.get());
|
||||
resourceRestoreTaskRepository.update(entity);
|
||||
resourceRestoreStateFileStore.markDone(stateFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface RunnerCommandInvoker {
|
||||
|
||||
int run(List<String> command, Path logFile, Path workingDirectory);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RunnerStallDetector {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
public RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, resourceBackupProperties, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void detectOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Path stateFile = Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
if (snapshot.getLastHeartbeatAt() == null || snapshot.getLastHeartbeatAt().isBlank()) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime heartbeat = LocalDateTime.parse(snapshot.getLastHeartbeatAt());
|
||||
if (Duration.between(heartbeat, nowSupplier.get()).getSeconds() <= resourceBackupProperties.getRunnerHeartbeatTimeoutSeconds()) {
|
||||
return;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStalled(stateFile, "RUNNER_STALLED", "restore runner heartbeat timed out");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record RunnerStepContext(
|
||||
String taskId,
|
||||
String stepCode,
|
||||
int stepNo,
|
||||
Path taskDir,
|
||||
Path handoffFile,
|
||||
Path stateFile
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public class RunnerStepExecutionResult {
|
||||
|
||||
private String result;
|
||||
private String summary;
|
||||
|
||||
public static RunnerStepExecutionResult success(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("SUCCESS");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RunnerStepExecutionResult failed(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("FAILED");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public interface RunnerStepExecutor {
|
||||
|
||||
boolean supports(String stepCode);
|
||||
|
||||
RunnerStepExecutionResult execute(RunnerStepContext context);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ServiceCommandRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
|
||||
public ServiceCommandRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
InitExecutorProperties initExecutorProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if (!supports(context.stepCode())) {
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
List<List<String>> commands = resolveCommands(context.stepCode());
|
||||
int executedCount = 0;
|
||||
for (List<String> command : commands) {
|
||||
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("runner step " + context.stepCode() + " failed, exitCode=" + exitCode);
|
||||
}
|
||||
executedCount++;
|
||||
}
|
||||
return RunnerStepExecutionResult.success("executed " + executedCount + " command(s)");
|
||||
}
|
||||
|
||||
private List<List<String>> resolveCommands(String stepCode) {
|
||||
return switch (stepCode) {
|
||||
case "STOP_SERVICES" -> buildStopCommands();
|
||||
case "START_SERVICES" -> buildStartCommands();
|
||||
default -> throw new IllegalArgumentException("unsupported runner step: " + stepCode);
|
||||
};
|
||||
}
|
||||
|
||||
private List<List<String>> buildStopCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "stop"));
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStopCommand());
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardRabbitStopCommand());
|
||||
return commands;
|
||||
}
|
||||
|
||||
private List<List<String>> buildStartCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStartCommand());
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "start"));
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void appendShellCommand(List<List<String>> commands, String shellCommand) {
|
||||
if (shellCommand == null || shellCommand.isBlank()) {
|
||||
return;
|
||||
}
|
||||
commands.add(List.of("/bin/zsh", "-lc", shellCommand.trim()));
|
||||
}
|
||||
|
||||
private Path resolveLogFile(RunnerStepContext context) {
|
||||
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.cisd.tms.modules.backup.runner.model;
|
||||
|
||||
public class ResourceRestoreHandoffSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String restorePlanJson;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public String getStagingDir() {
|
||||
return stagingDir;
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
this.stagingDir = stagingDir;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public String getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceBackupService {
|
||||
|
||||
ResourceKeysetStatusResponse getKeysetStatus();
|
||||
|
||||
CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request);
|
||||
|
||||
ResourceBackupTaskDetailResponse getBackupTask(String taskId);
|
||||
|
||||
ResourceBackupDownloadResult downloadBackupPackage(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
|
||||
public interface ResourcePrecheckService {
|
||||
|
||||
ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
|
||||
public interface ResourceRestoreService {
|
||||
|
||||
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceTaskQueryService {
|
||||
|
||||
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.discovery.ResourceDiscoveryService;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
|
||||
private final InitTaskRepository initTaskRepository;
|
||||
private final ResourceDiscoveryService resourceDiscoveryService;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourceBackupServiceImpl(
|
||||
InitTaskRepository initTaskRepository,
|
||||
ResourceDiscoveryService resourceDiscoveryService,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.initTaskRepository = initTaskRepository;
|
||||
this.resourceDiscoveryService = resourceDiscoveryService;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.resourceBackupTaskRepository = resourceBackupTaskRepository;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceKeysetStatusResponse getKeysetStatus() {
|
||||
ResourceKeysetStatusResponse response = new ResourceKeysetStatusResponse();
|
||||
// 第一版先把接口骨架和本地备份主链打通;真实密钥状态后续接入密码卡/UKey恢复结果。
|
||||
response.setLmkReady(false);
|
||||
response.setIkReady(false);
|
||||
response.setStorageEncKeyReady(false);
|
||||
response.setAuthenticityKeyReady(false);
|
||||
response.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
response.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
response.setSource("CONFIGURED_KEY_INDEX");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request) {
|
||||
// 资源备份必须基于“最近一次成功初始化”快照收集上下文,避免前端重新传一套可能失真的参数。
|
||||
InitTaskEntity initTask = initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"));
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
|
||||
String backupId = resourceTaskIdGenerator.nextBackupId();
|
||||
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
|
||||
ResourcePackageResult packageResult = resourcePackageService.createPackage(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
resourceDiscoveryService.discover(initTask),
|
||||
request
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
// 第一版先按同步成功模型落库,后续切换为真正异步任务时继续复用这张任务表。
|
||||
entity.setTaskId(taskId);
|
||||
entity.setBackupId(backupId);
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType(initTask.getProductType());
|
||||
entity.setMqType(initTask.getMqType());
|
||||
entity.setOrgCode(initTask.getOrgCode());
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setPackageName(packageResult.getPackageName());
|
||||
entity.setPackagePath(packageResult.getPackagePath());
|
||||
entity.setPackageSize(packageResult.getPackageSize());
|
||||
entity.setPackageHash(packageResult.getPackageHash());
|
||||
entity.setStorageEncKeyFingerprint(packageResult.getStorageEncKeyFingerprint());
|
||||
entity.setAuthenticityKeyFingerprint(packageResult.getAuthenticityKeyFingerprint());
|
||||
entity.setManifestSummaryJson(packageResult.getManifestSummaryJson());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
entity.setFinishTime(now);
|
||||
resourceBackupTaskRepository.save(entity);
|
||||
|
||||
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setBackupId(backupId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceBackupTaskDetailResponse getBackupTask(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceBackupTaskDetailResponse response = new ResourceBackupTaskDetailResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setMqType(entity.getMqType());
|
||||
response.setOrgCode(entity.getOrgCode());
|
||||
response.setSourceDeviceId(entity.getSourceDeviceId());
|
||||
response.setPackageName(entity.getPackageName());
|
||||
response.setPackageSize(entity.getPackageSize());
|
||||
response.setPackageHash(entity.getPackageHash());
|
||||
response.setStorageEncKeyFingerprint(entity.getStorageEncKeyFingerprint());
|
||||
response.setAuthenticityKeyFingerprint(entity.getAuthenticityKeyFingerprint());
|
||||
response.setManifestSummary(entity.getManifestSummaryJson());
|
||||
response.setRemark(entity.getRemark());
|
||||
response.setErrorCode(entity.getErrorCode());
|
||||
response.setErrorMessage(entity.getErrorMessage());
|
||||
response.setCreatedBy(entity.getCreateBy());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceBackupDownloadResult downloadBackupPackage(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
Path packagePath = Path.of(trim(entity.getPackagePath())).normalize().toAbsolutePath();
|
||||
if (trim(entity.getPackagePath()).isEmpty() || !Files.exists(packagePath) || !Files.isRegularFile(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found");
|
||||
}
|
||||
try {
|
||||
ResourceBackupDownloadResult result = new ResourceBackupDownloadResult();
|
||||
result.setFilename(trim(entity.getPackageName()).isEmpty() ? packagePath.getFileName().toString() : entity.getPackageName());
|
||||
result.setPackagePath(packagePath);
|
||||
result.setContentLength(Files.size(packagePath));
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read resource backup package");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(1);
|
||||
item.setStepCode("PACKAGE_CREATE");
|
||||
item.setStatus(entity.getStatus());
|
||||
item.setMessage("generated backup package " + trim(entity.getPackageName()));
|
||||
item.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
item.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
item.setLogAvailable(Boolean.FALSE);
|
||||
return List.of(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(trim(taskId));
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode("PACKAGE_CREATE");
|
||||
response.setContent(stepNo == 1 ? objectToJsonLine(Map.of(
|
||||
"packageName", trim(entity.getPackageName()),
|
||||
"packagePath", trim(entity.getPackagePath()),
|
||||
"packageHash", trim(entity.getPackageHash())
|
||||
)) : "");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String resolveSourceDeviceId() {
|
||||
try {
|
||||
// 先用主机名兜底,后续如有更稳定的设备唯一标识,可在这里统一替换。
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
} catch (Exception ex) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String objectToJsonLine(Map<String, Object> value) {
|
||||
try {
|
||||
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(value);
|
||||
} catch (Exception ex) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourcePrecheckServiceImpl(
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.cisdPresetProperties = cisdPresetProperties;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
|
||||
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(trim(request.getFileId()))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "backup package file not found: " + trim(request.getFileId())));
|
||||
// 预检阶段只解析包头、验签和比对指纹,不提前解密完整 payload。
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackageHeader(Path.of(fileRecord.getStoragePath()));
|
||||
String storageFingerprint = stringValue(inspectResult.getManifest().get("storageEncKeyFingerprint"));
|
||||
String authenticityFingerprint = stringValue(inspectResult.getManifest().get("authenticityKeyFingerprint"));
|
||||
if (!expectedStorageFingerprint().equals(storageFingerprint) || !expectedAuthenticityFingerprint().equals(authenticityFingerprint)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包密钥指纹不匹配");
|
||||
}
|
||||
ResourceRestorePrecheckResponse response = new ResourceRestorePrecheckResponse();
|
||||
response.setPrecheckId(resourceTaskIdGenerator.nextPrecheckId());
|
||||
response.setBackupId(stringValue(inspectResult.getManifest().get("backupId")));
|
||||
response.setSourceProductType(stringValue(inspectResult.getManifest().get("productType")));
|
||||
response.setSourceMqType(stringValue(inspectResult.getManifest().get("mqType")));
|
||||
response.setSourceOrgCode(stringValue(inspectResult.getManifest().get("orgCode")));
|
||||
response.setSourceBackupTime(stringValue(inspectResult.getManifest().get("backupTime")));
|
||||
response.setTargetProductType(defaultProductType());
|
||||
response.setKeysetMatched(true);
|
||||
response.setSignatureValid(true);
|
||||
response.setCompatible(isCompatible(response.getSourceProductType(), response.getTargetProductType()));
|
||||
response.setPayloadDecrypted(false);
|
||||
response.setWarnings(List.of());
|
||||
response.setBlockingIssues(List.of());
|
||||
response.setRestorePlan(List.of(
|
||||
"RESTORE_LOCK",
|
||||
"STOP_SERVICES",
|
||||
"RESTORE_FILES",
|
||||
"RESTORE_DATABASES",
|
||||
"START_SERVICES",
|
||||
"CONSISTENCY_VERIFY"
|
||||
));
|
||||
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
|
||||
response.setExpiresAt(LocalDateTime.now().plusMinutes(30).toString());
|
||||
resourcePrecheckStateStore.save(new ResourcePrecheckContext(request.getFileId(), response));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String defaultProductType() {
|
||||
return cisdPresetProperties.getProductType() == null || cisdPresetProperties.getProductType().isBlank()
|
||||
? "UNKNOWN"
|
||||
: cisdPresetProperties.getProductType().trim();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String expectedStorageFingerprint() {
|
||||
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
|
||||
}
|
||||
|
||||
private String expectedAuthenticityFingerprint() {
|
||||
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
|
||||
}
|
||||
|
||||
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
|
||||
java.util.LinkedHashMap<String, Object> summary = new java.util.LinkedHashMap<>();
|
||||
summary.put("resourceCount", inspectResult.getManifest().get("resourceCount"));
|
||||
summary.put("productType", inspectResult.getManifest().get("productType"));
|
||||
summary.put("mqType", inspectResult.getManifest().get("mqType"));
|
||||
summary.put("orgCode", inspectResult.getManifest().get("orgCode"));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static boolean isCompatible(String sourceProductType, String targetProductType) {
|
||||
if (targetProductType == null || targetProductType.isBlank() || "UNKNOWN".equalsIgnoreCase(targetProductType)) {
|
||||
return true;
|
||||
}
|
||||
return targetProductType.equalsIgnoreCase(sourceProductType);
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher;
|
||||
|
||||
public ResourceRestoreServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceRestoreRunnerLauncher = resourceRestoreRunnerLauncher;
|
||||
}
|
||||
|
||||
public ResourceRestoreServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this(
|
||||
resourceRestoreTaskRepository,
|
||||
fileRecordRepository,
|
||||
resourcePrecheckStateStore,
|
||||
resourceTaskIdGenerator,
|
||||
handoffFileStore,
|
||||
resourceRestoreStateFileStore,
|
||||
resourceBackupProperties,
|
||||
handoffFile -> {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request) {
|
||||
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "restore risk must be accepted");
|
||||
}
|
||||
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
|
||||
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"));
|
||||
ResourceRestorePrecheckResponse precheck = context.getResponse();
|
||||
if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result");
|
||||
}
|
||||
FileRecordEntity packageFile = fileRecordRepository.findByFileId(context.getFileId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found: " + trim(context.getFileId())));
|
||||
String packagePath = trim(packageFile.getStoragePath());
|
||||
if (packagePath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package path is empty");
|
||||
}
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextRestoreTaskId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPrecheckId(precheck.getPrecheckId());
|
||||
entity.setBackupId(precheck.getBackupId());
|
||||
// 恢复任务创建完成后,就应该已经进入“等待 runner 接管”的交接态,
|
||||
// 前端和运维看到的状态不能还停留在 PRECHECKED。
|
||||
entity.setStatus("HANDOFF");
|
||||
entity.setSourceDeviceId("UNKNOWN");
|
||||
entity.setSourceProductType(precheck.getSourceProductType());
|
||||
entity.setTargetProductType(precheck.getTargetProductType());
|
||||
entity.setPackageFileId(context.getFileId());
|
||||
Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize();
|
||||
// 恢复 runner 的工作目录和状态文件路径在任务创建时就固定下来,
|
||||
// 这样后续即使 TMS 重启,控制面路径也不会漂移。
|
||||
// 这里必须把真实备份包路径写进任务和 handoff,后续 runner 才能真正解包,
|
||||
// 否则它只能拿到 fileId,却不知道恢复时去哪里取浏览器上传/下载回来的备份文件。
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
entity.setRestorePlanJson(joinAsJsonArray(precheck.getRestorePlan()));
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
handoffFileStore.write(taskDir, entity);
|
||||
resourceRestoreStateFileStore.initialize(taskDir, entity);
|
||||
resourceRestoreStateFileStore.markHandoffReady(taskDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
resourceRestoreRunnerLauncher.launch(taskDir.resolve("handoff.json"));
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String joinAsJsonArray(List<String> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
// 这里先用最轻量的 JSON 数组序列化,避免为了一个占位字段引入额外 DTO。
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append('"').append(items.get(i)).append('"');
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.FileSystemResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateSnapshot;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ResourceTaskStepLogService resourceTaskStepLogService;
|
||||
|
||||
public ResourceTaskQueryServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, objectMapper, new FileSystemResourceTaskStepLogService());
|
||||
}
|
||||
|
||||
public ResourceTaskQueryServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper,
|
||||
ResourceTaskStepLogService resourceTaskStepLogService
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.objectMapper = objectMapper;
|
||||
this.resourceTaskStepLogService = resourceTaskStepLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setPrecheckId(entity.getPrecheckId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setControlPhase("PRECHECKED");
|
||||
response.setStateSource("DB");
|
||||
response.setSourceDeviceId(entity.getSourceDeviceId());
|
||||
response.setSourceProductType(entity.getSourceProductType());
|
||||
response.setTargetProductType(entity.getTargetProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackagePathVisible(Boolean.FALSE);
|
||||
response.setRunnerStatePath(entity.getRunnerStatePath());
|
||||
response.setRestorePlan(readStringList(entity.getRestorePlanJson()));
|
||||
response.setConsistencyReport(readObject(entity.getConsistencyReportJson()));
|
||||
response.setErrorCode(entity.getErrorCode());
|
||||
response.setErrorMessage(entity.getErrorMessage());
|
||||
response.setCreatedBy(entity.getCreateBy());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
mergeRunnerState(response, entity);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
ResourceRestoreStateSnapshot snapshot = resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())).orElse(null);
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
List<ResourceTaskStepItemResponse> responses = new ArrayList<>();
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(stepNo);
|
||||
item.setStepCode(stepCode);
|
||||
applyStepState(item, taskDir, snapshot, stepCode, stepNo);
|
||||
responses.add(item);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
|
||||
return resourceTaskStepLogService.read(resolveTaskDir(entity), trim(taskId), stepNo, stepCode);
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private void mergeRunnerState(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
// 恢复任务详情以数据库记录为基线,再用 runner 状态文件覆盖实时字段。
|
||||
// 这样即使 runner 尚未接管,前端也能看到稳定的任务基本信息。
|
||||
resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath()))
|
||||
.ifPresent(snapshot -> applySnapshot(response, snapshot));
|
||||
}
|
||||
|
||||
private void applySnapshot(ResourceRestoreTaskDetailResponse response, ResourceRestoreStateSnapshot snapshot) {
|
||||
response.setStatus(defaultIfBlank(snapshot.getStatus(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.getControlPhase(), response.getControlPhase()));
|
||||
response.setStateSource(defaultIfBlank(snapshot.getStateSource(), response.getStateSource()));
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.getCurrentStepCode(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(snapshot.getCurrentStepNo() == null ? response.getCurrentStepNo() : snapshot.getCurrentStepNo());
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.getLastHeartbeatAt(), response.getLastHeartbeatAt()));
|
||||
response.setOrphaned(snapshot.getOrphaned() == null ? response.getOrphaned() : snapshot.getOrphaned());
|
||||
response.setCleanupWarnings(defaultIfNull(snapshot.getCleanupWarnings(), response.getCleanupWarnings()));
|
||||
response.setWarnings(defaultIfNull(snapshot.getWarnings(), response.getWarnings()));
|
||||
response.setBlockingIssues(defaultIfNull(snapshot.getBlockingIssues(), response.getBlockingIssues()));
|
||||
response.setConsistencyReport(snapshot.getConsistencyReport() == null ? response.getConsistencyReport() : snapshot.getConsistencyReport());
|
||||
response.setErrorCode(defaultIfBlank(snapshot.getErrorCode(), response.getErrorCode()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.getErrorMessage(), response.getErrorMessage()));
|
||||
}
|
||||
|
||||
private Path resolveStatePath(String runnerStatePath) {
|
||||
return trim(runnerStatePath).isEmpty() ? null : Path.of(runnerStatePath).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
Path statePath = resolveStatePath(entity.getRunnerStatePath());
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private void applyStepState(
|
||||
ResourceTaskStepItemResponse item,
|
||||
Path taskDir,
|
||||
ResourceRestoreStateSnapshot snapshot,
|
||||
String stepCode,
|
||||
int stepNo
|
||||
) {
|
||||
Map<String, Object> stepOutput = readStepOutput(taskDir, stepNo, stepCode);
|
||||
item.setLogAvailable(taskDir != null && Files.isRegularFile(taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode))));
|
||||
if (stepOutput != null) {
|
||||
item.setStatus(stringValue(stepOutput.get("result"), "UNKNOWN"));
|
||||
item.setMessage(stringValue(stepOutput.get("summary"), ""));
|
||||
return;
|
||||
}
|
||||
if (snapshot != null && stepNo == safeInt(snapshot.getCurrentStepNo())) {
|
||||
if ("RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
item.setStatus("RUNNING");
|
||||
} else if ("RECONCILE_PENDING".equals(snapshot.getControlPhase()) && "FAILED".equals(snapshot.getStatus())) {
|
||||
item.setStatus("FAILED");
|
||||
item.setMessage(defaultIfBlank(snapshot.getErrorMessage(), ""));
|
||||
} else {
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
return;
|
||||
}
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
|
||||
private Map<String, Object> readStepOutput(Path taskDir, int stepNo, String stepCode) {
|
||||
if (taskDir == null) {
|
||||
return null;
|
||||
}
|
||||
Path outputFile = taskDir.resolve("step-output").resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
if (!Files.isRegularFile(outputFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(outputFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse resource task step output", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeInt(Integer value) {
|
||||
return value == null ? -1 : value;
|
||||
}
|
||||
|
||||
private static String stringValue(Object value, String defaultValue) {
|
||||
return value == null ? defaultValue : String.valueOf(value);
|
||||
}
|
||||
|
||||
private List<String> readStringList(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse restore plan json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Object readObject(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, Object.class);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse consistency report json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static <T> T defaultIfNull(T candidate, T fallback) {
|
||||
return candidate == null ? fallback : candidate;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class InMemoryResourcePrecheckStateStore implements ResourcePrecheckStateStore {
|
||||
|
||||
private final ConcurrentMap<String, ResourcePrecheckContext> contexts = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(ResourcePrecheckContext context) {
|
||||
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
|
||||
throw new IllegalArgumentException("context with precheckId is required");
|
||||
}
|
||||
contexts.put(context.getResponse().getPrecheckId(), context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourcePrecheckContext> find(String precheckId) {
|
||||
return Optional.ofNullable(contexts.get(precheckId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ResourceBackupDownloadResult {
|
||||
|
||||
private String filename;
|
||||
private Path packagePath;
|
||||
private long contentLength;
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public Path getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(Path packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public long getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public void setContentLength(long contentLength) {
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
|
||||
public class ResourcePrecheckContext {
|
||||
|
||||
private final String fileId;
|
||||
private final ResourceRestorePrecheckResponse response;
|
||||
|
||||
public ResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
|
||||
this.fileId = fileId;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public ResourceRestorePrecheckResponse getResponse() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourcePrecheckStateStore {
|
||||
|
||||
void save(ResourcePrecheckContext context);
|
||||
|
||||
Optional<ResourcePrecheckContext> find(String precheckId);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceTaskIdGenerator {
|
||||
|
||||
private static final DateTimeFormatter TASK_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
private static final DateTimeFormatter BACKUP_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
|
||||
|
||||
public String nextBackupTaskId() {
|
||||
return "RBKP-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextBackupId() {
|
||||
return "RBKP-" + LocalDateTime.now().format(BACKUP_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextPrecheckId() {
|
||||
return "RPRE-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextRestoreTaskId() {
|
||||
return "RRST-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
private static String randomSuffix() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceTaskStepLogService {
|
||||
|
||||
ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode);
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
public class FileSystemResourcePrecheckStateStore implements ResourcePrecheckStateStore {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public FileSystemResourcePrecheckStateStore(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourcePrecheckContext context) {
|
||||
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
|
||||
throw new IllegalArgumentException("context with precheckId is required");
|
||||
}
|
||||
try {
|
||||
Path file = resolvePath(context.getResponse().getPrecheckId());
|
||||
Files.createDirectories(file.getParent());
|
||||
objectMapper.writeValue(file.toFile(), new StoredResourcePrecheckContext(context.getFileId(), context.getResponse()));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to persist resource precheck context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourcePrecheckContext> find(String precheckId) {
|
||||
Path file = resolvePath(precheckId);
|
||||
if (!Files.exists(file)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(file.toFile());
|
||||
String fileId = root.path("fileId").asText();
|
||||
ResourceRestorePrecheckResponse response = objectMapper.treeToValue(root.path("response"), ResourceRestorePrecheckResponse.class);
|
||||
return Optional.of(new ResourcePrecheckContext(fileId, response));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to load resource precheck context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolvePath(String precheckId) {
|
||||
return Path.of(resourceBackupProperties.getPrecheckStoreDir()).resolve(precheckId + ".json").normalize();
|
||||
}
|
||||
|
||||
private record StoredResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class FileSystemResourceTaskStepLogService implements ResourceTaskStepLogService {
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode) {
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
if (taskDir == null || stepCode == null || stepCode.isBlank()) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
Path logFile = taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode));
|
||||
try {
|
||||
if (!Files.isRegularFile(logFile)) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
response.setContent(Files.readString(logFile));
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to read resource task step log", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class HandoffFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public HandoffFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void write(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Path file = taskDir.resolve("handoff.json");
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("precheckId", entity.getPrecheckId());
|
||||
json.put("backupId", entity.getBackupId());
|
||||
json.put("packageFileId", entity.getPackageFileId());
|
||||
json.put("packagePath", entity.getPackagePath());
|
||||
json.put("stagingDir", entity.getStagingDir());
|
||||
json.put("runnerStatePath", entity.getRunnerStatePath());
|
||||
json.put("restorePlanJson", entity.getRestorePlanJson());
|
||||
objectMapper.writeValue(file.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ResourceRestoreHandoffSnapshot> read(Path handoffFile) {
|
||||
if (handoffFile == null || !Files.isRegularFile(handoffFile)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.ofNullable(objectMapper.readValue(handoffFile.toFile(), ResourceRestoreHandoffSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreStateFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourceRestoreStateFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void initialize(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("status", entity.getStatus());
|
||||
json.put("controlPhase", "PRECHECKED");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("runnerInstanceId", "");
|
||||
json.put("runnerPid", 0);
|
||||
json.put("ownershipToken", UUID.randomUUID().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(taskDir.resolve("restore-state.json").toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to initialize restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Optional<ResourceRestoreStateSnapshot> read(Path stateFile) {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
try {
|
||||
return java.util.Optional.ofNullable(objectMapper.readValue(stateFile.toFile(), ResourceRestoreStateSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markHandoffReady(Path stateFile, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("status", "HANDOFF");
|
||||
json.put("controlPhase", "HANDOFF_READY");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state as handoff ready", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void claimOwnership(Path stateFile, String runnerInstanceId, long runnerPid, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String controlPhase = String.valueOf(json.getOrDefault("controlPhase", ""));
|
||||
if (!"HANDOFF_READY".equals(controlPhase)) {
|
||||
throw new IllegalStateException("restore state is not handoff ready: " + controlPhase);
|
||||
}
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("runnerInstanceId", runnerInstanceId);
|
||||
json.put("runnerPid", runnerPid);
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to claim restore runner ownership", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStepRunning(Path stateFile, String currentStepCode, Integer currentStepNo, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore step running");
|
||||
}
|
||||
|
||||
public void touchHeartbeat(Path stateFile, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json ->
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()), "failed to touch restore heartbeat");
|
||||
}
|
||||
|
||||
public void markReconcilePending(
|
||||
Path stateFile,
|
||||
String status,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String currentStepCode,
|
||||
Integer currentStepNo,
|
||||
String runnerInstanceId
|
||||
) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", status);
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("errorCode", errorCode == null ? "" : errorCode);
|
||||
json.put("errorMessage", errorMessage == null ? "" : errorMessage);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore reconcile pending");
|
||||
}
|
||||
|
||||
private void updateOwnedState(
|
||||
Path stateFile,
|
||||
String runnerInstanceId,
|
||||
JsonMutator mutator,
|
||||
String errorMessage
|
||||
) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String currentRunnerInstanceId = String.valueOf(json.getOrDefault("runnerInstanceId", ""));
|
||||
if (!runnerInstanceId.equals(currentRunnerInstanceId)) {
|
||||
throw new IllegalStateException("restore runner ownership changed: " + currentRunnerInstanceId);
|
||||
}
|
||||
mutator.accept(json);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markDone(Path stateFile) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("controlPhase", "DONE");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state done", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStalled(Path stateFile, String errorCode, String errorMessage) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
if (!"RUNNER_OWNED".equals(String.valueOf(json.getOrDefault("controlPhase", "")))) {
|
||||
return;
|
||||
}
|
||||
json.put("status", "FAILED");
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("orphaned", true);
|
||||
json.put("errorCode", errorCode);
|
||||
json.put("errorMessage", errorMessage);
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark stalled restore state", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface JsonMutator {
|
||||
void accept(Map<String, Object> json);
|
||||
}
|
||||
|
||||
private Map<String, Object> readJson(Path stateFile) throws IOException {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
throw new IllegalStateException("restore state file not found: " + stateFile);
|
||||
}
|
||||
return objectMapper.readValue(
|
||||
stateFile.toFile(),
|
||||
objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ResourceRestoreStateSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String runnerInstanceId;
|
||||
private Long runnerPid;
|
||||
private String ownershipToken;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getControlPhase() {
|
||||
return controlPhase;
|
||||
}
|
||||
|
||||
public void setControlPhase(String controlPhase) {
|
||||
this.controlPhase = controlPhase;
|
||||
}
|
||||
|
||||
public String getStateSource() {
|
||||
return stateSource;
|
||||
}
|
||||
|
||||
public void setStateSource(String stateSource) {
|
||||
this.stateSource = stateSource;
|
||||
}
|
||||
|
||||
public String getRunnerInstanceId() {
|
||||
return runnerInstanceId;
|
||||
}
|
||||
|
||||
public void setRunnerInstanceId(String runnerInstanceId) {
|
||||
this.runnerInstanceId = runnerInstanceId;
|
||||
}
|
||||
|
||||
public Long getRunnerPid() {
|
||||
return runnerPid;
|
||||
}
|
||||
|
||||
public void setRunnerPid(Long runnerPid) {
|
||||
this.runnerPid = runnerPid;
|
||||
}
|
||||
|
||||
public String getOwnershipToken() {
|
||||
return ownershipToken;
|
||||
}
|
||||
|
||||
public void setOwnershipToken(String ownershipToken) {
|
||||
this.ownershipToken = ownershipToken;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getLastHeartbeatAt() {
|
||||
return lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public void setLastHeartbeatAt(String lastHeartbeatAt) {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
public void setOrphaned(Boolean orphaned) {
|
||||
this.orphaned = orphaned;
|
||||
}
|
||||
|
||||
public List<String> getCleanupWarnings() {
|
||||
return cleanupWarnings;
|
||||
}
|
||||
|
||||
public void setCleanupWarnings(List<String> cleanupWarnings) {
|
||||
this.cleanupWarnings = cleanupWarnings;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public Object getConsistencyReport() {
|
||||
return consistencyReport;
|
||||
}
|
||||
|
||||
public void setConsistencyReport(Object consistencyReport) {
|
||||
this.consistencyReport = consistencyReport;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public class InitExecutorProperties {
|
||||
private int timeoutSec = 600;
|
||||
// 所有步骤日志会写入该目录,读取日志接口会做同目录约束校验
|
||||
private String logDir = "/tmp/tms-init-logs";
|
||||
// TMS 外置配置目录(资源备份恢复时使用)
|
||||
private String tmsConfigDir = "/home/tms/config";
|
||||
// 标准版(企业/间参)安装根目录(统一入口)
|
||||
private String standardHomeDir = DEFAULT_STANDARD_HOME_DIR;
|
||||
// 标准版(企业/间参)默认 cpconfig.cfg 路径
|
||||
@@ -133,6 +135,14 @@ public class InitExecutorProperties {
|
||||
this.logDir = logDir;
|
||||
}
|
||||
|
||||
public String getTmsConfigDir() {
|
||||
return tmsConfigDir;
|
||||
}
|
||||
|
||||
public void setTmsConfigDir(String tmsConfigDir) {
|
||||
this.tmsConfigDir = tmsConfigDir;
|
||||
}
|
||||
|
||||
public String getStandardHomeDir() {
|
||||
return isBlank(standardHomeDir) ? DEFAULT_STANDARD_HOME_DIR : standardHomeDir.trim();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ tms:
|
||||
timeout-sec: ${TMS_INIT_EXECUTOR_TIMEOUT_SEC:600}
|
||||
# 初始化步骤日志目录;按 taskId/stepNo 生成日志文件。
|
||||
log-dir: ${TMS_INIT_EXECUTOR_LOG_DIR:/home/tms/tmp/tms-init-logs}
|
||||
# TMS 外置配置目录;资源备份恢复会按此目录收集配置。
|
||||
tms-config-dir: ${TMS_INIT_EXECUTOR_TMS_CONFIG_DIR:/home/tms/config}
|
||||
# 标准版(企业/间参)安装根目录统一变量。
|
||||
standard-home-dir: ${TMS_INIT_STANDARD_HOME_DIR:/home/cmep4i}
|
||||
# 标准版 cpconfig.cfg 路径;默认取 standard-home-dir 下文件。
|
||||
@@ -179,6 +181,15 @@ tms:
|
||||
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
|
||||
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
|
||||
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:/home/tms/cert/public_key.pem}
|
||||
backup:
|
||||
# 资源备份包输出目录;第一版先在本机生成可见的 .tmsbak 文件。
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 恢复 runner 启停 TMS 时使用的脚本路径。
|
||||
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
|
||||
# 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。
|
||||
runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false}
|
||||
# 恢复 runner 心跳失联超时(秒)。
|
||||
runner-heartbeat-timeout-seconds: ${TMS_BACKUP_RUNNER_HEARTBEAT_TIMEOUT_SECONDS:300}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
|
||||
@@ -51,6 +51,90 @@ CREATE TABLE IF NOT EXISTS tms_init_task_step (
|
||||
KEY idx_tms_init_task_step_task_id (task_id)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 资源备份与恢复
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
product_type VARCHAR(16) NOT NULL,
|
||||
mq_type VARCHAR(32) NULL,
|
||||
org_code VARCHAR(32) NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
package_name VARCHAR(255) NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
package_size BIGINT NULL,
|
||||
package_hash VARCHAR(128) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
manifest_summary_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
|
||||
KEY idx_tms_resource_backup_task_status (status),
|
||||
KEY idx_tms_resource_backup_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
precheck_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
source_product_type VARCHAR(16) NOT NULL,
|
||||
target_product_type VARCHAR(16) NOT NULL,
|
||||
package_file_id VARCHAR(64) NOT NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
staging_dir VARCHAR(1024) NULL,
|
||||
runner_state_path VARCHAR(1024) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
restore_plan_json JSON NULL,
|
||||
consistency_report_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 认证与设备基础表
|
||||
-- -----------------------------------------------------------------------------
|
||||
@@ -365,4 +449,4 @@ CREATE TABLE IF NOT EXISTS tms_access_whitelist (
|
||||
mask VARCHAR(10) NOT NULL,
|
||||
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
);
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Resource backup and restore tables
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
product_type VARCHAR(16) NOT NULL,
|
||||
mq_type VARCHAR(32) NULL,
|
||||
org_code VARCHAR(32) NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
package_name VARCHAR(255) NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
package_size BIGINT NULL,
|
||||
package_hash VARCHAR(128) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
manifest_summary_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
|
||||
KEY idx_tms_resource_backup_task_status (status),
|
||||
KEY idx_tms_resource_backup_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
precheck_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
source_product_type VARCHAR(16) NOT NULL,
|
||||
target_product_type VARCHAR(16) NOT NULL,
|
||||
package_file_id VARCHAR(64) NOT NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
staging_dir VARCHAR(1024) NULL,
|
||||
runner_state_path VARCHAR(1024) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
restore_plan_json JSON NULL,
|
||||
consistency_report_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
Reference in New Issue
Block a user