mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-07-13 00:12:52 +08:00
功能: 新增 SAP HANA 完整备份支持与 Backint 协议代理 (#37)
* chore: ignore web/dist directory in git repository * 功能: 新增 SAP HANA 完整备份支持与 Backint 协议代理 - 修复 service 层校验 bug,使 SAP HANA 类型可正常创建 - 增强 hdbsql Runner:支持完整/增量/差异/日志备份、并行通道、失败重试 - 新增 Backint 协议代理(backupx backint 子命令),HANA 原生接口直连 BackupX 存储后端 - 新增本地 SQLite 目录维护 EBID↔对象键映射 - 前端新增 SAP HANA 扩展字段表单(备份类型/级别/通道数/重试次数/实例编号) - README 中英文补充 SAP HANA 两种模式的使用说明
This commit is contained in:
217
server/internal/backint/agent_test.go
Normal file
217
server/internal/backint/agent_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package backint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"backupx/server/internal/storage"
|
||||
storageRclone "backupx/server/internal/storage/rclone"
|
||||
)
|
||||
|
||||
// newTestAgent 构造一个使用本地磁盘后端的 Agent,便于集成测试。
|
||||
func newTestAgent(t *testing.T, compress bool) (*Agent, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
storageDir := filepath.Join(dir, "storage")
|
||||
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
registry := storage.NewRegistry(storageRclone.NewLocalDiskFactory())
|
||||
provider, err := registry.Create(context.Background(), "local_disk", map[string]any{
|
||||
"basePath": storageDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create provider: %v", err)
|
||||
}
|
||||
cat, err := OpenCatalog(filepath.Join(dir, "catalog.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agent := &Agent{
|
||||
cfg: &Config{StorageType: "local_disk", KeyPrefix: "backint", Compress: compress, CatalogDB: filepath.Join(dir, "catalog.db")},
|
||||
provider: provider,
|
||||
catalog: cat,
|
||||
}
|
||||
t.Cleanup(func() { _ = agent.Close() })
|
||||
return agent, dir
|
||||
}
|
||||
|
||||
func TestAgent_BackupAndRestore_File(t *testing.T) {
|
||||
agent, dir := newTestAgent(t, false)
|
||||
ctx := context.Background()
|
||||
|
||||
// 准备源文件
|
||||
src := filepath.Join(dir, "src.bak")
|
||||
content := []byte("hello backint world")
|
||||
if err := os.WriteFile(src, content, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// BACKUP
|
||||
inPath := filepath.Join(dir, "backup.in")
|
||||
outPath := filepath.Join(dir, "backup.out")
|
||||
if err := os.WriteFile(inPath, []byte(src+"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := agent.Run(ctx, FunctionBackup, inPath, outPath); err != nil {
|
||||
t.Fatalf("backup: %v", err)
|
||||
}
|
||||
out, _ := os.ReadFile(outPath)
|
||||
if !bytes.HasPrefix(out, []byte("#SAVED ")) {
|
||||
t.Fatalf("expected #SAVED, got: %s", out)
|
||||
}
|
||||
// 提取 EBID:#SAVED <ebid> "<path>"
|
||||
parts := strings.Fields(string(out))
|
||||
if len(parts) < 3 {
|
||||
t.Fatalf("malformed output: %s", out)
|
||||
}
|
||||
ebid := parts[1]
|
||||
|
||||
// RESTORE
|
||||
restoreDst := filepath.Join(dir, "restored.bak")
|
||||
inPath2 := filepath.Join(dir, "restore.in")
|
||||
outPath2 := filepath.Join(dir, "restore.out")
|
||||
if err := os.WriteFile(inPath2, []byte(ebid+" \""+restoreDst+"\"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := agent.Run(ctx, FunctionRestore, inPath2, outPath2); err != nil {
|
||||
t.Fatalf("restore: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(restoreDst)
|
||||
if err != nil {
|
||||
t.Fatalf("read restored: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, content) {
|
||||
t.Errorf("restored content mismatch: %q vs %q", got, content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_BackupWithCompression(t *testing.T) {
|
||||
agent, dir := newTestAgent(t, true)
|
||||
ctx := context.Background()
|
||||
|
||||
src := filepath.Join(dir, "src.bak")
|
||||
content := bytes.Repeat([]byte("ABCDEFGH"), 1024)
|
||||
if err := os.WriteFile(src, content, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inPath := filepath.Join(dir, "backup.in")
|
||||
outPath := filepath.Join(dir, "backup.out")
|
||||
_ = os.WriteFile(inPath, []byte(src+"\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionBackup, inPath, outPath); err != nil {
|
||||
t.Fatalf("backup: %v", err)
|
||||
}
|
||||
parts := strings.Fields(string(mustRead(t, outPath)))
|
||||
ebid := parts[1]
|
||||
|
||||
// 验证 catalog 记录的对象键以 .gz 结尾
|
||||
entry, _ := agent.catalog.Get(ebid)
|
||||
if entry == nil || !strings.HasSuffix(entry.ObjectKey, ".gz") {
|
||||
t.Fatalf("expected .gz suffix: %+v", entry)
|
||||
}
|
||||
|
||||
// RESTORE 应能解压回原始内容
|
||||
dst := filepath.Join(dir, "restored.bak")
|
||||
in2 := filepath.Join(dir, "restore.in")
|
||||
out2 := filepath.Join(dir, "restore.out")
|
||||
_ = os.WriteFile(in2, []byte(ebid+" \""+dst+"\"\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionRestore, in2, out2); err != nil {
|
||||
t.Fatalf("restore: %v", err)
|
||||
}
|
||||
got := mustRead(t, dst)
|
||||
if !bytes.Equal(got, content) {
|
||||
t.Errorf("decompressed content mismatch (len=%d vs %d)", len(got), len(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_Inquire(t *testing.T) {
|
||||
agent, dir := newTestAgent(t, false)
|
||||
ctx := context.Background()
|
||||
|
||||
// 注入两条目录记录
|
||||
_ = agent.catalog.Put(CatalogEntry{EBID: "bid-a", ObjectKey: "k/a"})
|
||||
_ = agent.catalog.Put(CatalogEntry{EBID: "bid-b", ObjectKey: "k/b"})
|
||||
|
||||
// INQUIRE #NULL 应列出全部
|
||||
in := filepath.Join(dir, "inq.in")
|
||||
out := filepath.Join(dir, "inq.out")
|
||||
_ = os.WriteFile(in, []byte("#NULL\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionInquire, in, out); err != nil {
|
||||
t.Fatalf("inquire: %v", err)
|
||||
}
|
||||
text := string(mustRead(t, out))
|
||||
if !strings.Contains(text, "bid-a") || !strings.Contains(text, "bid-b") {
|
||||
t.Errorf("expected both ebids, got: %s", text)
|
||||
}
|
||||
|
||||
// INQUIRE 不存在的 ebid → #NOTFOUND
|
||||
_ = os.WriteFile(in, []byte("bid-missing\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionInquire, in, out); err != nil {
|
||||
t.Fatalf("inquire missing: %v", err)
|
||||
}
|
||||
text = string(mustRead(t, out))
|
||||
if !strings.Contains(text, "#NOTFOUND") {
|
||||
t.Errorf("expected #NOTFOUND, got: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_Delete(t *testing.T) {
|
||||
agent, dir := newTestAgent(t, false)
|
||||
ctx := context.Background()
|
||||
|
||||
// 先做一次 BACKUP
|
||||
src := filepath.Join(dir, "src.bak")
|
||||
_ = os.WriteFile(src, []byte("data"), 0644)
|
||||
inPath := filepath.Join(dir, "b.in")
|
||||
outPath := filepath.Join(dir, "b.out")
|
||||
_ = os.WriteFile(inPath, []byte(src+"\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionBackup, inPath, outPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ebid := strings.Fields(string(mustRead(t, outPath)))[1]
|
||||
|
||||
// DELETE
|
||||
delIn := filepath.Join(dir, "d.in")
|
||||
delOut := filepath.Join(dir, "d.out")
|
||||
_ = os.WriteFile(delIn, []byte(ebid+"\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionDelete, delIn, delOut); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(mustRead(t, delOut)), "#DELETED") {
|
||||
t.Errorf("expected #DELETED, got: %s", mustRead(t, delOut))
|
||||
}
|
||||
// catalog 条目应已删除
|
||||
if entry, _ := agent.catalog.Get(ebid); entry != nil {
|
||||
t.Errorf("catalog entry should be removed, got: %+v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_RestoreUnknownEBID(t *testing.T) {
|
||||
agent, dir := newTestAgent(t, false)
|
||||
ctx := context.Background()
|
||||
|
||||
in := filepath.Join(dir, "r.in")
|
||||
out := filepath.Join(dir, "r.out")
|
||||
_ = os.WriteFile(in, []byte("bid-unknown \""+filepath.Join(dir, "dst")+"\"\n"), 0644)
|
||||
if err := agent.Run(ctx, FunctionRestore, in, out); err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(mustRead(t, out)), "#ERROR") {
|
||||
t.Errorf("expected #ERROR for unknown ebid, got: %s", mustRead(t, out))
|
||||
}
|
||||
}
|
||||
|
||||
func mustRead(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user