mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-27 19:19:35 +08:00
266 lines
9.1 KiB
Go
266 lines
9.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MasterClient 是 Agent 调用 Master HTTP API 的封装。
|
|
type MasterClient struct {
|
|
baseURL string
|
|
token string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewMasterClient 构造 Master 客户端。
|
|
func NewMasterClient(baseURL, token string, insecureTLS bool) *MasterClient {
|
|
transport := &http.Transport{}
|
|
if insecureTLS {
|
|
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
}
|
|
return &MasterClient{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
token: token,
|
|
httpClient: &http.Client{
|
|
Timeout: 120 * time.Second,
|
|
Transport: transport,
|
|
},
|
|
}
|
|
}
|
|
|
|
// HeartbeatRequest Agent 上报心跳的请求
|
|
type HeartbeatRequest struct {
|
|
Token string `json:"token"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
IPAddress string `json:"ipAddress,omitempty"`
|
|
AgentVersion string `json:"agentVersion,omitempty"`
|
|
OS string `json:"os,omitempty"`
|
|
Arch string `json:"arch,omitempty"`
|
|
}
|
|
|
|
// HeartbeatResponse Master 返回的心跳响应
|
|
type HeartbeatResponse struct {
|
|
Status string `json:"status"`
|
|
NodeID uint `json:"nodeId"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Heartbeat 上报心跳并获取节点元信息
|
|
func (c *MasterClient) Heartbeat(ctx context.Context, req HeartbeatRequest) (*HeartbeatResponse, error) {
|
|
var resp HeartbeatResponse
|
|
if err := c.do(ctx, http.MethodPost, "/api/agent/heartbeat", req, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// CommandPayload 与 service.AgentCommandPayload 对齐
|
|
type CommandPayload struct {
|
|
ID uint `json:"id"`
|
|
Type string `json:"type"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
// PollCommandResponse 轮询响应:无命令时 Command 为 nil
|
|
type PollCommandResponse struct {
|
|
Command *CommandPayload `json:"command"`
|
|
}
|
|
|
|
// PollCommand 拉取下一条待执行命令
|
|
func (c *MasterClient) PollCommand(ctx context.Context) (*CommandPayload, error) {
|
|
var resp PollCommandResponse
|
|
if err := c.do(ctx, http.MethodPost, "/api/agent/commands/poll", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Command, nil
|
|
}
|
|
|
|
// SubmitCommandResult 上报命令执行结果
|
|
func (c *MasterClient) SubmitCommandResult(ctx context.Context, cmdID uint, success bool, errorMsg string, result any) error {
|
|
var resultJSON json.RawMessage
|
|
if result != nil {
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal result: %w", err)
|
|
}
|
|
resultJSON = data
|
|
}
|
|
payload := map[string]any{
|
|
"success": success,
|
|
"errorMessage": errorMsg,
|
|
}
|
|
if resultJSON != nil {
|
|
payload["result"] = resultJSON
|
|
}
|
|
path := fmt.Sprintf("/api/agent/commands/%d/result", cmdID)
|
|
return c.do(ctx, http.MethodPost, path, payload, nil)
|
|
}
|
|
|
|
// TaskSpec 与 service.AgentTaskSpec 对齐
|
|
type TaskSpec struct {
|
|
TaskID uint `json:"taskId"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
SourcePath string `json:"sourcePath"`
|
|
SourcePaths string `json:"sourcePaths"`
|
|
ExcludePatterns string `json:"excludePatterns"`
|
|
DBHost string `json:"dbHost"`
|
|
DBPort int `json:"dbPort"`
|
|
DBUser string `json:"dbUser"`
|
|
DBPassword string `json:"dbPassword"`
|
|
DBName string `json:"dbName"`
|
|
DBPath string `json:"dbPath"`
|
|
ExtraConfig string `json:"extraConfig"`
|
|
Compression string `json:"compression"`
|
|
Encrypt bool `json:"encrypt"`
|
|
StorageTargets []StorageTargetConfig `json:"storageTargets"`
|
|
}
|
|
|
|
// StorageTargetConfig 与 service.AgentStorageTargetConfig 对齐
|
|
type StorageTargetConfig struct {
|
|
ID uint `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Config json.RawMessage `json:"config"`
|
|
}
|
|
|
|
// GetTaskSpec 拉取任务规格
|
|
func (c *MasterClient) GetTaskSpec(ctx context.Context, taskID uint) (*TaskSpec, error) {
|
|
var spec TaskSpec
|
|
path := fmt.Sprintf("/api/agent/tasks/%d", taskID)
|
|
if err := c.do(ctx, http.MethodGet, path, nil, &spec); err != nil {
|
|
return nil, err
|
|
}
|
|
return &spec, nil
|
|
}
|
|
|
|
// RecordUpdate 与 service.AgentRecordUpdate 对齐
|
|
type RecordUpdate struct {
|
|
Status string `json:"status,omitempty"`
|
|
FileName string `json:"fileName,omitempty"`
|
|
FileSize int64 `json:"fileSize,omitempty"`
|
|
Checksum string `json:"checksum,omitempty"`
|
|
StoragePath string `json:"storagePath,omitempty"`
|
|
StorageTargetID uint `json:"storageTargetId,omitempty"`
|
|
StorageUploadResults []StorageResultItem `json:"storageUploadResults,omitempty"`
|
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
|
LogAppend string `json:"logAppend,omitempty"`
|
|
}
|
|
|
|
type StorageResultItem struct {
|
|
StorageTargetID uint `json:"storageTargetId"`
|
|
StorageTargetName string `json:"storageTargetName"`
|
|
Status string `json:"status"`
|
|
StoragePath string `json:"storagePath,omitempty"`
|
|
FileSize int64 `json:"fileSize,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// UpdateRecord 上报备份记录的状态/日志
|
|
func (c *MasterClient) UpdateRecord(ctx context.Context, recordID uint, update RecordUpdate) error {
|
|
path := fmt.Sprintf("/api/agent/records/%d", recordID)
|
|
return c.do(ctx, http.MethodPost, path, update, nil)
|
|
}
|
|
|
|
// RestoreSpec 与 service.AgentRestoreSpec 对齐
|
|
type RestoreSpec struct {
|
|
RestoreRecordID uint `json:"restoreRecordId"`
|
|
BackupRecordID uint `json:"backupRecordId"`
|
|
TaskID uint `json:"taskId"`
|
|
TaskName string `json:"taskName"`
|
|
Type string `json:"type"`
|
|
SourcePath string `json:"sourcePath,omitempty"`
|
|
SourcePaths []string `json:"sourcePaths,omitempty"`
|
|
DBHost string `json:"dbHost,omitempty"`
|
|
DBPort int `json:"dbPort,omitempty"`
|
|
DBUser string `json:"dbUser,omitempty"`
|
|
DBPassword string `json:"dbPassword,omitempty"`
|
|
DBName string `json:"dbName,omitempty"`
|
|
DBPath string `json:"dbPath,omitempty"`
|
|
ExtraConfig string `json:"extraConfig,omitempty"`
|
|
Compression string `json:"compression"`
|
|
Encrypt bool `json:"encrypt"`
|
|
Storage StorageTargetConfig `json:"storage"`
|
|
StoragePath string `json:"storagePath"`
|
|
FileName string `json:"fileName"`
|
|
}
|
|
|
|
// RestoreUpdate 与 service.AgentRestoreUpdate 对齐
|
|
type RestoreUpdate struct {
|
|
Status string `json:"status,omitempty"`
|
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
|
LogAppend string `json:"logAppend,omitempty"`
|
|
}
|
|
|
|
// GetRestoreSpec 拉取恢复规格
|
|
func (c *MasterClient) GetRestoreSpec(ctx context.Context, restoreRecordID uint) (*RestoreSpec, error) {
|
|
var spec RestoreSpec
|
|
path := fmt.Sprintf("/api/agent/restores/%d/spec", restoreRecordID)
|
|
if err := c.do(ctx, http.MethodGet, path, nil, &spec); err != nil {
|
|
return nil, err
|
|
}
|
|
return &spec, nil
|
|
}
|
|
|
|
// UpdateRestore 上报恢复记录的状态/日志
|
|
func (c *MasterClient) UpdateRestore(ctx context.Context, restoreRecordID uint, update RestoreUpdate) error {
|
|
path := fmt.Sprintf("/api/agent/restores/%d", restoreRecordID)
|
|
return c.do(ctx, http.MethodPost, path, update, nil)
|
|
}
|
|
|
|
// do 是通用 HTTP 调用。所有 Agent API 都统一走 JSON + X-Agent-Token。
|
|
func (c *MasterClient) do(ctx context.Context, method, path string, body any, out any) error {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(data)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("X-Agent-Token", c.token)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s: %w", method, path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read response: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("%s %s: http %d: %s", method, path, resp.StatusCode, string(data))
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
// BackupX API 统一封装成 {code, data, message} 形式,需要解出 data 字段
|
|
var envelope struct {
|
|
Code string `json:"code"`
|
|
Data json.RawMessage `json:"data"`
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal(data, &envelope); err == nil && envelope.Data != nil {
|
|
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
|
return fmt.Errorf("decode data: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
// 兼容直接返回对象的情况
|
|
return json.Unmarshal(data, out)
|
|
}
|