mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-05 15:37:03 +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:
@@ -35,6 +35,12 @@ func (r *SAPHANARunner) Type() string {
|
||||
// Run executes a SAP HANA data-level backup using hdbsql + BACKUP DATA USING FILE.
|
||||
// The backup files are written to a temporary directory, then packaged into a tar
|
||||
// archive as the artifact for BackupX to compress/encrypt/upload.
|
||||
//
|
||||
// 支持以下增强(通过 task.Database 字段配置):
|
||||
// - BackupLevel: full / incremental / differential
|
||||
// - BackupType: data / log
|
||||
// - BackupChannels: 并行通道数(>1 时生成多路径 SQL)
|
||||
// - MaxRetries: hdbsql 执行失败的重试次数
|
||||
func (r *SAPHANARunner) Run(ctx context.Context, task TaskSpec, writer LogWriter) (*RunResult, error) {
|
||||
if _, err := r.executor.LookPath("hdbsql"); err != nil {
|
||||
return nil, fmt.Errorf("未找到 hdbsql 命令 (请确保服务器已安装 SAP HANA Client)")
|
||||
@@ -68,32 +74,46 @@ func (r *SAPHANARunner) Run(ctx context.Context, task TaskSpec, writer LogWriter
|
||||
port = 30015
|
||||
}
|
||||
|
||||
writer.WriteLine(fmt.Sprintf("连接到 SAP HANA: %s:%d", task.Database.Host, port))
|
||||
backupLevel := normalizeBackupLevel(task.Database.BackupLevel)
|
||||
backupType := normalizeBackupType(task.Database.BackupType)
|
||||
channels := task.Database.BackupChannels
|
||||
if channels < 1 {
|
||||
channels = 1
|
||||
}
|
||||
maxRetries := task.Database.MaxRetries
|
||||
if maxRetries < 1 {
|
||||
maxRetries = 3
|
||||
}
|
||||
instance := task.Database.InstanceNumber
|
||||
if strings.TrimSpace(instance) == "" {
|
||||
instance = hanaInstanceNumber(port)
|
||||
}
|
||||
|
||||
writer.WriteLine(fmt.Sprintf("连接到 SAP HANA: %s:%d (实例 %s)", task.Database.Host, port, instance))
|
||||
writer.WriteLine(fmt.Sprintf("备份数据库: %s", tenantDB))
|
||||
writer.WriteLine(fmt.Sprintf("备份配置: 类型=%s, 级别=%s, 通道数=%d, 最大重试=%d", backupType, backupLevel, channels, maxRetries))
|
||||
|
||||
// Build backup prefix — HANA will create files like <prefix>_databackup_<N>_1.
|
||||
timestamp := startedAt.UTC().Format("20060102_150405")
|
||||
backupPrefix := filepath.Join(backupDir, fmt.Sprintf("hana_%s_%s", strings.ToLower(tenantDB), timestamp))
|
||||
|
||||
// Build `BACKUP DATA USING FILE` SQL.
|
||||
backupSQL := fmt.Sprintf(`BACKUP DATA USING FILE ('%s')`, backupPrefix)
|
||||
if strings.ToUpper(tenantDB) != "SYSTEMDB" {
|
||||
backupSQL = fmt.Sprintf(`BACKUP DATA FOR %s USING FILE ('%s')`, tenantDB, backupPrefix)
|
||||
prefixes, err := buildBackupPrefixes(backupDir, tenantDB, timestamp, channels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build SQL based on backup type and level.
|
||||
backupSQL := buildBackupSQL(tenantDB, prefixes, backupType, backupLevel)
|
||||
writer.WriteLine(fmt.Sprintf("生成 SQL: %s", backupSQL))
|
||||
|
||||
// Construct hdbsql connection arguments.
|
||||
args := buildHdbsqlArgs(task.Database.Host, port, task.Database.User, task.Database.Password, tenantDB, backupSQL)
|
||||
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
writer.WriteLine("开始执行 SAP HANA BACKUP DATA USING FILE")
|
||||
writer.WriteLine("开始执行 SAP HANA 备份命令")
|
||||
|
||||
if err := r.executor.Run(ctx, "hdbsql", args, CommandOptions{
|
||||
Stderr: stderrWriter,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("run hdbsql BACKUP DATA: %w: %s", err, stderrWriter.collected())
|
||||
if err := r.runHdbsqlWithRetry(ctx, "hdbsql", args, maxRetries, writer); err != nil {
|
||||
return nil, fmt.Errorf("run hdbsql backup: %w", err)
|
||||
}
|
||||
|
||||
writer.WriteLine("SAP HANA BACKUP DATA 命令执行完成,开始打包备份文件")
|
||||
writer.WriteLine("SAP HANA 备份命令执行完成,开始打包备份文件")
|
||||
|
||||
// Package all generated backup files into a tar archive.
|
||||
if err := packageBackupFiles(backupDir, artifactPath, writer); err != nil {
|
||||
@@ -166,12 +186,12 @@ func (r *SAPHANARunner) Restore(ctx context.Context, task TaskSpec, artifactPath
|
||||
|
||||
args := buildHdbsqlArgs(task.Database.Host, port, task.Database.User, task.Database.Password, tenantDB, recoverSQL)
|
||||
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
if err := r.executor.Run(ctx, "hdbsql", args, CommandOptions{
|
||||
Stderr: stderrWriter,
|
||||
}); err != nil {
|
||||
errMsg := stderrWriter.collected()
|
||||
return fmt.Errorf("run hdbsql RECOVER DATA: %w: %s", err, strings.TrimSpace(errMsg))
|
||||
maxRetries := task.Database.MaxRetries
|
||||
if maxRetries < 1 {
|
||||
maxRetries = 3
|
||||
}
|
||||
if err := r.runHdbsqlWithRetry(ctx, "hdbsql", args, maxRetries, writer); err != nil {
|
||||
return fmt.Errorf("run hdbsql RECOVER DATA: %w", err)
|
||||
}
|
||||
|
||||
writer.WriteLine("SAP HANA 恢复完成")
|
||||
@@ -188,6 +208,111 @@ func hanaInstanceNumber(port int) string {
|
||||
return "00"
|
||||
}
|
||||
|
||||
// normalizeBackupLevel 规范化备份级别值,无效或空值默认为 "full"。
|
||||
func normalizeBackupLevel(level string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(level)) {
|
||||
case "incremental":
|
||||
return "incremental"
|
||||
case "differential":
|
||||
return "differential"
|
||||
default:
|
||||
return "full"
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeBackupType 规范化备份类型,无效或空值默认为 "data"。
|
||||
func normalizeBackupType(t string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(t)) {
|
||||
case "log":
|
||||
return "log"
|
||||
default:
|
||||
return "data"
|
||||
}
|
||||
}
|
||||
|
||||
// buildBackupPrefixes 为每个并行通道生成独立子目录和路径前缀。
|
||||
// 当 channels=1 时返回单个直接位于 backupDir 下的前缀;
|
||||
// 当 channels>1 时为每个通道创建 chan_N/ 子目录。
|
||||
func buildBackupPrefixes(backupDir, tenantDB, timestamp string, channels int) ([]string, error) {
|
||||
tenantLower := strings.ToLower(tenantDB)
|
||||
if channels <= 1 {
|
||||
return []string{filepath.Join(backupDir, fmt.Sprintf("hana_%s_%s", tenantLower, timestamp))}, nil
|
||||
}
|
||||
prefixes := make([]string, 0, channels)
|
||||
for i := 0; i < channels; i++ {
|
||||
chanDir := filepath.Join(backupDir, fmt.Sprintf("chan_%d", i))
|
||||
if err := os.MkdirAll(chanDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create channel %d dir: %w", i, err)
|
||||
}
|
||||
prefixes = append(prefixes, filepath.Join(chanDir, fmt.Sprintf("hana_%s_%s", tenantLower, timestamp)))
|
||||
}
|
||||
return prefixes, nil
|
||||
}
|
||||
|
||||
// buildBackupSQL 根据备份类型和级别构建 SAP HANA BACKUP SQL 语句。
|
||||
//
|
||||
// 支持的语法:
|
||||
//
|
||||
// 全量数据备份: BACKUP DATA [FOR <tenant>] USING FILE ('p1' [, 'p2', ...])
|
||||
// 增量数据备份: BACKUP DATA [FOR <tenant>] INCREMENTAL USING FILE ('...')
|
||||
// 差异数据备份: BACKUP DATA [FOR <tenant>] DIFFERENTIAL USING FILE ('...')
|
||||
// 日志备份: BACKUP LOG [FOR <tenant>] USING FILE ('...')
|
||||
func buildBackupSQL(tenantDB string, prefixes []string, backupType, backupLevel string) string {
|
||||
tenantClause := ""
|
||||
if strings.ToUpper(tenantDB) != "SYSTEMDB" {
|
||||
tenantClause = fmt.Sprintf(" FOR %s", tenantDB)
|
||||
}
|
||||
|
||||
// 多路径以 'p1', 'p2', ... 拼接(HANA 多通道并行语法)
|
||||
quoted := make([]string, len(prefixes))
|
||||
for i, p := range prefixes {
|
||||
quoted[i] = fmt.Sprintf("'%s'", p)
|
||||
}
|
||||
pathClause := strings.Join(quoted, ", ")
|
||||
|
||||
if backupType == "log" {
|
||||
// LOG 备份不支持 INCREMENTAL/DIFFERENTIAL 关键字
|
||||
return fmt.Sprintf("BACKUP LOG%s USING FILE (%s)", tenantClause, pathClause)
|
||||
}
|
||||
|
||||
levelClause := ""
|
||||
switch backupLevel {
|
||||
case "incremental":
|
||||
levelClause = " INCREMENTAL"
|
||||
case "differential":
|
||||
levelClause = " DIFFERENTIAL"
|
||||
}
|
||||
return fmt.Sprintf("BACKUP DATA%s%s USING FILE (%s)", tenantClause, levelClause, pathClause)
|
||||
}
|
||||
|
||||
// runHdbsqlWithRetry 执行 hdbsql 命令并在失败时按指数退避重试。
|
||||
// 退避公式:5s × attempt²,并在 ctx 取消时立即返回。
|
||||
func (r *SAPHANARunner) runHdbsqlWithRetry(ctx context.Context, name string, args []string, maxAttempts int, writer LogWriter) error {
|
||||
if maxAttempts < 1 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
backoff := time.Duration(attempt*attempt) * 5 * time.Second
|
||||
writer.WriteLine(fmt.Sprintf("hdbsql 第 %d 次重试(等待 %s)", attempt, backoff))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
}
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
err := r.executor.Run(ctx, name, args, CommandOptions{Stderr: stderrWriter})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = fmt.Errorf("%w: %s", err, strings.TrimSpace(stderrWriter.collected()))
|
||||
writer.WriteLine(fmt.Sprintf("hdbsql 执行失败(第 %d/%d 次): %v", attempt, maxAttempts, lastErr))
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// buildHdbsqlArgs constructs the common hdbsql CLI arguments.
|
||||
func buildHdbsqlArgs(host string, port int, user, password, database, sql string) []string {
|
||||
return []string{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSAPHANARunnerRun_BackupDataCommand(t *testing.T) {
|
||||
@@ -273,6 +274,246 @@ func TestSAPHANARunnerRestore_TenantRecoverCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBackupSQL_FullSystemDB(t *testing.T) {
|
||||
sql := buildBackupSQL("SYSTEMDB", []string{"/tmp/p1"}, "data", "full")
|
||||
if sql != "BACKUP DATA USING FILE ('/tmp/p1')" {
|
||||
t.Fatalf("unexpected SQL: %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBackupSQL_IncrementalTenant(t *testing.T) {
|
||||
sql := buildBackupSQL("HDB", []string{"/tmp/p1"}, "data", "incremental")
|
||||
expected := "BACKUP DATA FOR HDB INCREMENTAL USING FILE ('/tmp/p1')"
|
||||
if sql != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBackupSQL_DifferentialTenant(t *testing.T) {
|
||||
sql := buildBackupSQL("HDB", []string{"/tmp/p1"}, "data", "differential")
|
||||
expected := "BACKUP DATA FOR HDB DIFFERENTIAL USING FILE ('/tmp/p1')"
|
||||
if sql != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBackupSQL_LogBackup(t *testing.T) {
|
||||
sql := buildBackupSQL("HDB", []string{"/tmp/log"}, "log", "full")
|
||||
expected := "BACKUP LOG FOR HDB USING FILE ('/tmp/log')"
|
||||
if sql != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBackupSQL_ParallelChannels(t *testing.T) {
|
||||
sql := buildBackupSQL("SYSTEMDB", []string{"/tmp/c0/p", "/tmp/c1/p", "/tmp/c2/p"}, "data", "full")
|
||||
expected := "BACKUP DATA USING FILE ('/tmp/c0/p', '/tmp/c1/p', '/tmp/c2/p')"
|
||||
if sql != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, sql)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBackupLevel(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "full",
|
||||
"FULL": "full",
|
||||
"incremental": "incremental",
|
||||
"DIFFERENTIAL": "differential",
|
||||
"unknown": "full",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := normalizeBackupLevel(in); got != want {
|
||||
t.Errorf("normalizeBackupLevel(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBackupType(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "data",
|
||||
"DATA": "data",
|
||||
"log": "log",
|
||||
"LOG": "log",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := normalizeBackupType(in); got != want {
|
||||
t.Errorf("normalizeBackupType(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_IncrementalBackup(t *testing.T) {
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
startIdx := strings.Index(capturedSQL, "('") + 2
|
||||
endIdx := strings.Index(capturedSQL, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := capturedSQL[startIdx:endIdx]
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("incremental data"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-incremental",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1",
|
||||
Port: 30015,
|
||||
User: "SYSTEM",
|
||||
Password: "secret",
|
||||
Names: []string{"HDB"},
|
||||
BackupLevel: "incremental",
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
if !strings.Contains(capturedSQL, "INCREMENTAL USING FILE") {
|
||||
t.Fatalf("expected INCREMENTAL in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_LogBackup(t *testing.T) {
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
startIdx := strings.Index(capturedSQL, "('") + 2
|
||||
endIdx := strings.Index(capturedSQL, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := capturedSQL[startIdx:endIdx]
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_logbackup_0_1", []byte("log data"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-log",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
Names: []string{"HDB"},
|
||||
BackupType: "log",
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
if !strings.Contains(capturedSQL, "BACKUP LOG FOR HDB USING FILE") {
|
||||
t.Fatalf("expected log backup SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_ParallelChannels(t *testing.T) {
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
// 模拟为每个通道生成备份文件
|
||||
parts := strings.Split(capturedSQL, "',")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if idx := strings.Index(p, "'"); idx >= 0 {
|
||||
prefix := p[idx+1:]
|
||||
prefix = strings.TrimSuffix(prefix, "')")
|
||||
prefix = strings.TrimSuffix(prefix, "'")
|
||||
if prefix != "" {
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("data"), 0o644)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-parallel",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
Names: []string{"SYSTEMDB"},
|
||||
BackupChannels: 3,
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
// 应该包含 3 个路径
|
||||
if strings.Count(capturedSQL, "'") != 6 { // 3 路径 × 2 引号
|
||||
t.Fatalf("expected 3 channels (6 quotes), got SQL: %s", capturedSQL)
|
||||
}
|
||||
if !strings.Contains(capturedSQL, "chan_0") || !strings.Contains(capturedSQL, "chan_2") {
|
||||
t.Fatalf("expected channel directories in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_RetryOnFailure(t *testing.T) {
|
||||
attempts := 0
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
attempts++
|
||||
if attempts < 2 {
|
||||
return errors.New("transient failure")
|
||||
}
|
||||
// 第二次成功,写入备份文件
|
||||
sql := args[len(args)-1]
|
||||
startIdx := strings.Index(sql, "('") + 2
|
||||
endIdx := strings.Index(sql, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := sql[startIdx:endIdx]
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("data"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// 使用极短的重试周期版本(这里通过 fake context 机制无法快进时间,所以直接验证 attempts)
|
||||
// 设置 MaxRetries=2 以加快测试,不会真实等待 5s
|
||||
runner := NewSAPHANARunner(executor)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := runner.Run(ctx, TaskSpec{
|
||||
Name: "hana-retry",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
Names: []string{"SYSTEMDB"},
|
||||
MaxRetries: 2,
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error after retry: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
if attempts != 2 {
|
||||
t.Fatalf("expected 2 attempts, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHanaInstanceNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
port int
|
||||
|
||||
@@ -12,6 +12,12 @@ type DatabaseSpec struct {
|
||||
Password string
|
||||
Names []string
|
||||
Path string
|
||||
// SAP HANA 特有字段(其他类型忽略)
|
||||
InstanceNumber string // 实例编号(从端口推断或手动指定)
|
||||
BackupLevel string // "full"(默认) / "incremental" / "differential"
|
||||
BackupType string // "data"(默认) / "log"
|
||||
BackupChannels int // 并行通道数(默认 1)
|
||||
MaxRetries int // 最大重试次数(默认 3)
|
||||
}
|
||||
|
||||
type TaskSpec struct {
|
||||
|
||||
Reference in New Issue
Block a user