mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-06 16:06:45 +08:00
refactor: simplify architecture and harden lifecycle
Remove obsolete implementations, centralize background task ownership and terminal-state recovery, consolidate frontend routing and log streaming, and enforce project-wide verification in CI.
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
//go:build ignore
|
||||
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type CommandExecutor interface {
|
||||
LookPath(file string) (string, error)
|
||||
Run(ctx context.Context, name string, args []string, env map[string]string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error
|
||||
}
|
||||
|
||||
type OSCommandExecutor struct{}
|
||||
|
||||
func NewOSCommandExecutor() *OSCommandExecutor {
|
||||
return &OSCommandExecutor{}
|
||||
}
|
||||
|
||||
func (e *OSCommandExecutor) LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
|
||||
func (e *OSCommandExecutor) Run(ctx context.Context, name string, args []string, env map[string]string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
|
||||
command := exec.CommandContext(ctx, name, args...)
|
||||
command.Stdin = stdin
|
||||
command.Stdout = stdout
|
||||
command.Stderr = stderr
|
||||
command.Env = os.Environ()
|
||||
for key, value := range env {
|
||||
command.Env = append(command.Env, key+"="+value)
|
||||
}
|
||||
return command.Run()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type capturingLogWriter struct {
|
||||
lines []string
|
||||
}
|
||||
|
||||
func (w *capturingLogWriter) WriteLine(message string) {
|
||||
w.lines = append(w.lines, message)
|
||||
}
|
||||
|
||||
func TestLogLineWriterHandlesFragmentedWrites(t *testing.T) {
|
||||
log := &capturingLogWriter{}
|
||||
w := newLogLineWriter(log, "tool")
|
||||
|
||||
assertWrite := func(chunk string) {
|
||||
t.Helper()
|
||||
n, err := w.Write([]byte(chunk))
|
||||
if err != nil || n != len(chunk) {
|
||||
t.Fatalf("Write(%q) = (%d, %v), want (%d, nil)", chunk, n, err, len(chunk))
|
||||
}
|
||||
}
|
||||
assertWrite("fir")
|
||||
if len(log.lines) != 0 || string(w.pending) != "fir" {
|
||||
t.Fatalf("first fragment logged or buffered incorrectly: lines=%#v pending=%q", log.lines, w.pending)
|
||||
}
|
||||
assertWrite("st\nsec")
|
||||
if !reflect.DeepEqual(log.lines, []string{"[tool] first"}) || string(w.pending) != "sec" {
|
||||
t.Fatalf("second fragment handled incorrectly: lines=%#v pending=%q", log.lines, w.pending)
|
||||
}
|
||||
assertWrite("ond\n")
|
||||
if !reflect.DeepEqual(log.lines, []string{"[tool] first", "[tool] second"}) || len(w.pending) != 0 {
|
||||
t.Fatalf("completed fragments handled incorrectly: lines=%#v pending=%q", log.lines, w.pending)
|
||||
}
|
||||
if got := w.collected(); got != "first\nsecond" {
|
||||
t.Fatalf("collected() = %q, want complete raw output", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLineWriterEmitsMultipleCompleteLinesOnce(t *testing.T) {
|
||||
log := &capturingLogWriter{}
|
||||
w := newLogLineWriter(log, "tool")
|
||||
|
||||
n, err := w.Write([]byte("one\ntwo\n\n three \r\n"))
|
||||
if err != nil || n != len("one\ntwo\n\n three \r\n") {
|
||||
t.Fatalf("Write() = (%d, %v)", n, err)
|
||||
}
|
||||
want := []string{"[tool] one", "[tool] two", "[tool] three"}
|
||||
if !reflect.DeepEqual(log.lines, want) {
|
||||
t.Fatalf("lines = %#v, want %#v", log.lines, want)
|
||||
}
|
||||
if len(w.pending) != 0 {
|
||||
t.Fatalf("complete input left pending bytes: %q", w.pending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogLineWriterFlushIsIdempotentAndPreservesCollection(t *testing.T) {
|
||||
log := &capturingLogWriter{}
|
||||
w := newLogLineWriter(log, "tool")
|
||||
_, _ = w.Write([]byte("complete\n tail "))
|
||||
|
||||
w.Flush()
|
||||
w.Flush()
|
||||
want := []string{"[tool] complete", "[tool] tail"}
|
||||
if !reflect.DeepEqual(log.lines, want) {
|
||||
t.Fatalf("lines after repeated Flush = %#v, want %#v", log.lines, want)
|
||||
}
|
||||
if len(w.pending) != 0 {
|
||||
t.Fatalf("Flush left pending bytes: %q", w.pending)
|
||||
}
|
||||
if got := w.collected(); got != "complete\n tail" {
|
||||
t.Fatalf("collected() = %q, want complete stderr independent of Flush", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLRunnerFlushesEachCommandTail(t *testing.T) {
|
||||
executor := &fakeCommandExecutor{runFunc: func(_ string, args []string, options CommandOptions) error {
|
||||
name := args[len(args)-1]
|
||||
_, _ = io.WriteString(options.Stdout, name)
|
||||
_, _ = io.WriteString(options.Stderr, "warning "+name)
|
||||
return nil
|
||||
}}
|
||||
log := &capturingLogWriter{}
|
||||
runner := NewPostgreSQLRunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "pg-log-lines",
|
||||
TempDir: t.TempDir(),
|
||||
Database: DatabaseSpec{
|
||||
Host: "127.0.0.1", Port: 5432, User: "postgres", Names: []string{"app", "audit"},
|
||||
},
|
||||
}, log)
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(result.TempDir) })
|
||||
|
||||
for _, expected := range []string{"[pg_dump] warning app", "[pg_dump] warning audit"} {
|
||||
count := 0
|
||||
for _, line := range log.lines {
|
||||
if line == expected {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("line %q occurred %d times in %#v", expected, count, log.lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMongoDBRunnerFlushesUnterminatedStderr(t *testing.T) {
|
||||
executor := &fakeCommandExecutor{runFunc: func(_ string, _ []string, options CommandOptions) error {
|
||||
_, _ = io.WriteString(options.Stdout, "archive")
|
||||
_, _ = io.WriteString(options.Stderr, "tail warning")
|
||||
return nil
|
||||
}}
|
||||
log := &capturingLogWriter{}
|
||||
runner := NewMongoDBRunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "mongo-log-tail",
|
||||
Database: DatabaseSpec{
|
||||
Host: "127.0.0.1", Port: 27017, Names: []string{"app"},
|
||||
},
|
||||
}, log)
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(result.TempDir) })
|
||||
|
||||
count := 0
|
||||
for _, line := range log.lines {
|
||||
if line == "[mongodump] tail warning" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("unterminated stderr line occurred %d times in %#v", count, log.lines)
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,10 @@ func (r *MongoDBRunner) Run(ctx context.Context, task TaskSpec, writer LogWriter
|
||||
writer.WriteLine(fmt.Sprintf("连接到 MongoDB: %s:%d", task.Database.Host, task.Database.Port))
|
||||
stderrWriter := newLogLineWriter(writer, "mongodump")
|
||||
writer.WriteLine("开始执行 mongodump")
|
||||
if err := r.executor.Run(ctx, "mongodump", args, CommandOptions{Stdout: file, Stderr: stderrWriter}); err != nil {
|
||||
return nil, fmt.Errorf("run mongodump: %w: %s", err, stderrWriter.collected())
|
||||
runErr := r.executor.Run(ctx, "mongodump", args, CommandOptions{Stdout: file, Stderr: stderrWriter})
|
||||
stderrWriter.Flush()
|
||||
if runErr != nil {
|
||||
return nil, fmt.Errorf("run mongodump: %w: %s", runErr, stderrWriter.collected())
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -70,8 +69,10 @@ func (r *MySQLRunner) Run(ctx context.Context, task TaskSpec, writer LogWriter)
|
||||
|
||||
stderrWriter := newLogLineWriter(writer, "mysqldump")
|
||||
writer.WriteLine("开始执行 mysqldump")
|
||||
if err := r.executor.Run(ctx, "mysqldump", args, CommandOptions{Stdout: file, Stderr: stderrWriter, Env: mysqlEnv(task.Database.Password)}); err != nil {
|
||||
return nil, fmt.Errorf("run mysqldump: %w: %s", err, stderrWriter.collected())
|
||||
runErr := r.executor.Run(ctx, "mysqldump", args, CommandOptions{Stdout: file, Stderr: stderrWriter, Env: mysqlEnv(task.Database.Password)})
|
||||
stderrWriter.Flush()
|
||||
if runErr != nil {
|
||||
return nil, fmt.Errorf("run mysqldump: %w: %s", runErr, stderrWriter.collected())
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
@@ -109,9 +110,10 @@ func mysqlEnv(password string) []string {
|
||||
|
||||
// logLineWriter streams each line of output to a LogWriter in real-time.
|
||||
type logLineWriter struct {
|
||||
writer LogWriter
|
||||
prefix string
|
||||
buf bytes.Buffer
|
||||
writer LogWriter
|
||||
prefix string
|
||||
pending []byte
|
||||
output []byte
|
||||
}
|
||||
|
||||
func newLogLineWriter(w LogWriter, prefix string) *logLineWriter {
|
||||
@@ -119,28 +121,43 @@ func newLogLineWriter(w LogWriter, prefix string) *logLineWriter {
|
||||
}
|
||||
|
||||
func (w *logLineWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
w.buf.Write(p)
|
||||
scanner := bufio.NewScanner(strings.NewReader(w.buf.String()))
|
||||
var remaining string
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" {
|
||||
w.writer.WriteLine(fmt.Sprintf("[%s] %s", w.prefix, line))
|
||||
w.output = append(w.output, p...)
|
||||
w.pending = append(w.pending, p...)
|
||||
consumed := 0
|
||||
for {
|
||||
newline := bytes.IndexByte(w.pending[consumed:], '\n')
|
||||
if newline < 0 {
|
||||
break
|
||||
}
|
||||
end := consumed + newline
|
||||
w.emit(w.pending[consumed:end])
|
||||
consumed = end + 1
|
||||
}
|
||||
// Keep any partial last line (no newline yet)
|
||||
lastNl := bytes.LastIndexByte(p, '\n')
|
||||
if lastNl >= 0 {
|
||||
remaining = w.buf.String()[w.buf.Len()-(len(p)-lastNl-1):]
|
||||
w.buf.Reset()
|
||||
w.buf.WriteString(remaining)
|
||||
if consumed > 0 {
|
||||
copy(w.pending, w.pending[consumed:])
|
||||
w.pending = w.pending[:len(w.pending)-consumed]
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Flush emits the final unterminated line. It is safe to call more than once.
|
||||
func (w *logLineWriter) Flush() {
|
||||
if len(w.pending) == 0 {
|
||||
return
|
||||
}
|
||||
w.emit(w.pending)
|
||||
w.pending = w.pending[:0]
|
||||
}
|
||||
|
||||
func (w *logLineWriter) emit(raw []byte) {
|
||||
line := strings.TrimSpace(string(raw))
|
||||
if line != "" {
|
||||
w.writer.WriteLine(fmt.Sprintf("[%s] %s", w.prefix, line))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *logLineWriter) collected() string {
|
||||
return strings.TrimSpace(w.buf.String())
|
||||
return strings.TrimSpace(string(w.output))
|
||||
}
|
||||
|
||||
func formatFileSize(size int64) string {
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
//go:build ignore
|
||||
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PostgreSQLRunner struct {
|
||||
executor CommandExecutor
|
||||
}
|
||||
|
||||
func NewPostgreSQLRunner(executor CommandExecutor) *PostgreSQLRunner {
|
||||
if executor == nil {
|
||||
executor = NewOSCommandExecutor()
|
||||
}
|
||||
return &PostgreSQLRunner{executor: executor}
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) Type() string {
|
||||
return "postgresql"
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) Run(ctx context.Context, spec TaskSpec, logger LogSink) (*Result, error) {
|
||||
if _, err := r.executor.LookPath("pg_dump"); err != nil {
|
||||
return nil, fmt.Errorf("pg_dump is required: %w", err)
|
||||
}
|
||||
databases := splitDatabaseNames(spec.DBName)
|
||||
if len(databases) == 0 {
|
||||
return nil, fmt.Errorf("postgresql database name is required")
|
||||
}
|
||||
tempDir, err := CreateTaskTempDir(spec.TaskName, spec.StartedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(databases) == 1 {
|
||||
return r.dumpSingleDatabase(ctx, spec, databases[0], tempDir, logger)
|
||||
}
|
||||
multiDumpDir := filepath.Join(tempDir, "postgres-dumps")
|
||||
if err := os.MkdirAll(multiDumpDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create postgres multi dump directory: %w", err)
|
||||
}
|
||||
for _, databaseName := range databases {
|
||||
if _, err := r.dumpDatabaseToFile(ctx, spec, databaseName, filepath.Join(multiDumpDir, sanitizeDumpName(databaseName)+".sql"), logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fileName := BuildArtifactName(spec.TaskName, spec.StartedAt, "tar.gz")
|
||||
artifactPath := filepath.Join(tempDir, fileName)
|
||||
size, err := CreateTarGz(ctx, multiDumpDir, nil, artifactPath, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{ArtifactPath: artifactPath, FileName: fileName, Size: size, StorageKey: BuildStorageKey("postgresql", spec.StartedAt, fileName)}, nil
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) Restore(ctx context.Context, spec TaskSpec, artifactPath string, logger LogSink) error {
|
||||
if _, err := r.executor.LookPath("psql"); err != nil {
|
||||
return fmt.Errorf("psql is required: %w", err)
|
||||
}
|
||||
databases := splitDatabaseNames(spec.DBName)
|
||||
if len(databases) == 0 {
|
||||
return fmt.Errorf("postgresql database name is required")
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(artifactPath), ".tar.gz") {
|
||||
restoreDir, err := CreateTaskTempDir(spec.TaskName+"-restore", spec.StartedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ExtractTarGz(ctx, artifactPath, restoreDir, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, databaseName := range databases {
|
||||
filePath := filepath.Join(restoreDir, filepath.Base(restoreDir), sanitizeDumpName(databaseName)+".sql")
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
fallback := filepath.Join(restoreDir, "postgres-dumps", sanitizeDumpName(databaseName)+".sql")
|
||||
filePath = fallback
|
||||
}
|
||||
if err := r.restoreDatabaseFromFile(ctx, spec, databaseName, filePath, logger); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return r.restoreDatabaseFromFile(ctx, spec, databases[0], artifactPath, logger)
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) dumpSingleDatabase(ctx context.Context, spec TaskSpec, databaseName string, tempDir string, logger LogSink) (*Result, error) {
|
||||
fileName := BuildArtifactName(spec.TaskName, spec.StartedAt, "sql")
|
||||
artifactPath := filepath.Join(tempDir, fileName)
|
||||
size, err := r.dumpDatabaseToFile(ctx, spec, databaseName, artifactPath, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{ArtifactPath: artifactPath, FileName: fileName, Size: size, StorageKey: BuildStorageKey("postgresql", spec.StartedAt, fileName)}, nil
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) dumpDatabaseToFile(ctx context.Context, spec TaskSpec, databaseName string, artifactPath string, logger LogSink) (int64, error) {
|
||||
output, err := os.Create(filepath.Clean(artifactPath))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create postgres dump file: %w", err)
|
||||
}
|
||||
defer output.Close()
|
||||
stderr := &bytes.Buffer{}
|
||||
args := []string{"-h", spec.DBHost, "-p", fmt.Sprintf("%d", spec.DBPort), "-U", spec.DBUser, "-d", databaseName, "--no-owner", "--no-privileges"}
|
||||
if logger != nil {
|
||||
logger.Infof("开始执行 pg_dump:%s", databaseName)
|
||||
}
|
||||
if err := r.executor.Run(ctx, "pg_dump", args, postgresEnv(spec.DBPassword), nil, output, stderr); err != nil {
|
||||
return 0, fmt.Errorf("run pg_dump: %w: %s", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
info, err := output.Stat()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("stat postgres dump file: %w", err)
|
||||
}
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
func (r *PostgreSQLRunner) restoreDatabaseFromFile(ctx context.Context, spec TaskSpec, databaseName string, artifactPath string, logger LogSink) error {
|
||||
input, err := os.Open(filepath.Clean(artifactPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open postgres restore file: %w", err)
|
||||
}
|
||||
defer input.Close()
|
||||
stderr := &bytes.Buffer{}
|
||||
args := []string{"-h", spec.DBHost, "-p", fmt.Sprintf("%d", spec.DBPort), "-U", spec.DBUser, "-d", databaseName}
|
||||
if logger != nil {
|
||||
logger.Infof("开始执行 psql 恢复:%s", databaseName)
|
||||
}
|
||||
if err := r.executor.Run(ctx, "psql", args, postgresEnv(spec.DBPassword), input, nil, stderr); err != nil {
|
||||
return fmt.Errorf("run psql restore: %w: %s", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postgresEnv(password string) map[string]string {
|
||||
if strings.TrimSpace(password) == "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]string{"PGPASSWORD": password}
|
||||
}
|
||||
|
||||
func splitDatabaseNames(value string) []string {
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sanitizeDumpName(value string) string {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(value))
|
||||
trimmed = strings.ReplaceAll(trimmed, " ", "-")
|
||||
trimmed = strings.ReplaceAll(trimmed, "/", "-")
|
||||
trimmed = strings.ReplaceAll(trimmed, "\\", "-")
|
||||
trimmed = strings.Trim(trimmed, "-._")
|
||||
if trimmed == "" {
|
||||
return "database"
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
@@ -43,12 +43,14 @@ func (r *PostgreSQLRunner) Run(ctx context.Context, task TaskSpec, writer LogWri
|
||||
}
|
||||
writer.WriteLine(fmt.Sprintf("连接到 PostgreSQL: %s:%d", task.Database.Host, task.Database.Port))
|
||||
writer.WriteLine(fmt.Sprintf("备份数据库: %s", strings.Join(dbNames, ", ")))
|
||||
stderrWriter := newLogLineWriter(writer, "pg_dump")
|
||||
for index, name := range dbNames {
|
||||
args := []string{"--clean", "--if-exists", "--create", "--format=plain", "-h", task.Database.Host, "-p", strconv.Itoa(task.Database.Port), "-U", task.Database.User, "--dbname", name}
|
||||
writer.WriteLine(fmt.Sprintf("开始导出数据库 [%d/%d]: %s", index+1, len(dbNames), name))
|
||||
if err := r.executor.Run(ctx, "pg_dump", args, CommandOptions{Stdout: file, Stderr: stderrWriter, Env: append(os.Environ(), "PGPASSWORD="+task.Database.Password)}); err != nil {
|
||||
return nil, fmt.Errorf("run pg_dump for %s: %w", name, err)
|
||||
stderrWriter := newLogLineWriter(writer, "pg_dump")
|
||||
runErr := r.executor.Run(ctx, "pg_dump", args, CommandOptions{Stdout: file, Stderr: stderrWriter, Env: append(os.Environ(), "PGPASSWORD="+task.Database.Password)})
|
||||
stderrWriter.Flush()
|
||||
if runErr != nil {
|
||||
return nil, fmt.Errorf("run pg_dump for %s: %w", name, runErr)
|
||||
}
|
||||
writer.WriteLine(fmt.Sprintf("数据库 %s 导出完成", name))
|
||||
if index < len(dbNames)-1 {
|
||||
|
||||
@@ -20,6 +20,18 @@ func NewRegistry(runners ...BackupRunner) *Registry {
|
||||
return registry
|
||||
}
|
||||
|
||||
// NewDefaultRegistry returns the runner set shared by Master and Agent.
|
||||
func NewDefaultRegistry() *Registry {
|
||||
return NewRegistry(
|
||||
NewFileRunner(),
|
||||
NewSQLiteRunner(),
|
||||
NewMySQLRunner(nil),
|
||||
NewPostgreSQLRunner(nil),
|
||||
NewSAPHANARunner(nil),
|
||||
NewMongoDBRunner(nil),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Registry) Register(runner BackupRunner) {
|
||||
if runner == nil {
|
||||
return
|
||||
|
||||
@@ -304,6 +304,7 @@ func (r *SAPHANARunner) runHdbsqlWithRetry(ctx context.Context, name string, arg
|
||||
}
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
err := r.executor.Run(ctx, name, args, CommandOptions{Stderr: stderrWriter})
|
||||
stderrWriter.Flush()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user