mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-07 05:42:52 +08:00
* 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 两种模式的使用说明
218 lines
6.4 KiB
Go
218 lines
6.4 KiB
Go
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
|
||
}
|