feat(cluster): 支持远程源服务器集中备份

为 Master 本地磁盘增加可选流式中转,远程 Agent 可直接把产物写入中央存储并通过反向通道恢复。

持久化并校验传输模式,兼容既有 Agent 本机磁盘目标,补齐鉴权、完整性、配额、访问保护、前端配置及双向链路测试。

Closes #101
This commit is contained in:
Awuqing
2026-08-07 05:54:34 +08:00
parent 05dd1baa61
commit cc50637b4b
28 changed files with 1012 additions and 180 deletions
@@ -1,8 +1,10 @@
package service
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
@@ -427,16 +429,27 @@ func TestRestoreServiceAgentRestoreAccessUsesRestoreRecordNode(t *testing.T) {
}
startedAt := time.Now().UTC()
completedAt := startedAt.Add(time.Second)
artifact := []byte("central backup artifact")
storagePath := "file/2026/05/09/remote.tar.gz"
artifactPath := filepath.Join(h.storageDir, filepath.FromSlash(storagePath))
if err := os.MkdirAll(filepath.Dir(artifactPath), 0o755); err != nil {
t.Fatalf("MkdirAll artifact parent: %v", err)
}
if err := os.WriteFile(artifactPath, artifact, 0o600); err != nil {
t.Fatalf("WriteFile artifact: %v", err)
}
backupRecord := &model.BackupRecord{
TaskID: task.ID,
StorageTargetID: task.StorageTargetID,
NodeID: owner.ID,
Status: model.BackupRecordStatusSuccess,
FileName: "remote.tar.gz",
StoragePath: "file/2026/05/09/remote.tar.gz",
Checksum: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
StartedAt: startedAt,
CompletedAt: &completedAt,
TaskID: task.ID,
StorageTargetID: task.StorageTargetID,
NodeID: owner.ID,
Status: model.BackupRecordStatusSuccess,
FileName: "remote.tar.gz",
StoragePath: storagePath,
FileSize: int64(len(artifact)),
StorageTransferMode: storage.TransferModeMasterRelay,
Checksum: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
StartedAt: startedAt,
CompletedAt: &completedAt,
}
if err := h.records.Create(ctx, backupRecord); err != nil {
t.Fatalf("Create backup record: %v", err)
@@ -464,6 +477,21 @@ func TestRestoreServiceAgentRestoreAccessUsesRestoreRecordNode(t *testing.T) {
if spec.Checksum != backupRecord.Checksum {
t.Fatalf("expected spec.Checksum=%q, got %q", backupRecord.Checksum, spec.Checksum)
}
if spec.Storage.TransferMode != storage.TransferModeMasterRelay {
t.Fatalf("expected Master relay restore, got %#v", spec.Storage)
}
download, err := h.service.DownloadAgentArtifact(ctx, owner, restore.ID)
if err != nil {
t.Fatalf("DownloadAgentArtifact returned error: %v", err)
}
downloaded, readErr := io.ReadAll(download.Reader)
closeErr := download.Reader.Close()
if readErr != nil || closeErr != nil {
t.Fatalf("read relayed restore artifact: read=%v close=%v", readErr, closeErr)
}
if !bytes.Equal(downloaded, artifact) {
t.Fatalf("relayed restore artifact differs: %q", downloaded)
}
if _, err := h.service.GetAgentRestoreSpec(ctx, other, restore.ID); err == nil {
t.Fatal("expected non-owner node to be forbidden from restore spec")
}