Files
BackupX/server/internal/backup/mongodb_runner.go
Wu Qing f584a0802a feat(backup): 新增 MongoDB 备份与恢复支持 (#87)
通过 mongodump/mongorestore --archive 流式管线接入 MongoDB 数据源,与现有数据库运行器架构一致;注册到 Master 与 Agent,含任务校验、默认端口与前端表单/恢复确认。5 个单测覆盖参数构造、全库、空产物与缺工具分支。
2026-05-27 18:35:10 +08:00

120 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package backup
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// MongoDBRunner 通过 mongodump/mongorestore 备份与恢复 MongoDB 数据库。
// 采用 --archive 流式模式dump 写 stdout、restore 读 stdin与 MySQLRunner
// 的 mysqldump/mysql 管线保持一致;产物为未压缩的 mongo archive由备份管线统一压缩/加密。
type MongoDBRunner struct {
executor CommandExecutor
}
func NewMongoDBRunner(executor CommandExecutor) *MongoDBRunner {
if executor == nil {
executor = NewOSCommandExecutor()
}
return &MongoDBRunner{executor: executor}
}
func (r *MongoDBRunner) Type() string {
return "mongodb"
}
func (r *MongoDBRunner) Run(ctx context.Context, task TaskSpec, writer LogWriter) (*RunResult, error) {
if _, err := r.executor.LookPath("mongodump"); err != nil {
return nil, fmt.Errorf("未找到 mongodump 命令 (请确保服务器已安装 mongodb-database-tools)")
}
startedAt := task.StartedAt
if startedAt.IsZero() {
startedAt = time.Now().UTC()
}
tempDir, err := CreateTaskTempDir(task.Name, startedAt)
if err != nil {
return nil, err
}
fileName := BuildArtifactName(task.Name, startedAt, "archive")
artifactPath := filepath.Join(tempDir, fileName)
file, err := os.Create(artifactPath)
if err != nil {
return nil, fmt.Errorf("create mongodump archive file: %w", err)
}
defer file.Close()
args := mongoConnArgs(task.Database)
dbNames := normalizeDatabaseNames(task.Database.Names)
if len(dbNames) == 1 {
args = append(args, "--db", dbNames[0])
writer.WriteLine(fmt.Sprintf("备份数据库: %s", dbNames[0]))
} else {
writer.WriteLine("备份全部数据库")
}
args = append(args, "--archive") // 归档流式写入 stdout
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())
}
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("stat mongodump archive: %w", err)
}
if info.Size() == 0 {
return nil, fmt.Errorf("mongodump 产物为空,请检查数据库连接与权限")
}
writer.WriteLine(fmt.Sprintf("MongoDB 导出完成(文件大小: %s", formatFileSize(info.Size())))
return &RunResult{ArtifactPath: artifactPath, FileName: fileName, TempDir: tempDir, Size: info.Size(), StorageKey: BuildStorageKey("mongodb", startedAt, fileName)}, nil
}
func (r *MongoDBRunner) Restore(ctx context.Context, task TaskSpec, artifactPath string, writer LogWriter) error {
if _, err := r.executor.LookPath("mongorestore"); err != nil {
return fmt.Errorf("未找到 mongorestore 命令 (请确保服务器已安装 mongodb-database-tools)")
}
input, err := os.Open(filepath.Clean(artifactPath))
if err != nil {
return fmt.Errorf("open mongodb restore archive: %w", err)
}
defer input.Close()
args := mongoConnArgs(task.Database)
// --drop恢复前删除同名集合保证恢复后与归档一致与 mysql 恢复的整库覆盖语义对齐)。
args = append(args, "--drop", "--archive")
stderr := &bytes.Buffer{}
writer.WriteLine("开始执行 mongorestore")
if err := r.executor.Run(ctx, "mongorestore", args, CommandOptions{Stdin: input, Stderr: stderr}); err != nil {
return fmt.Errorf("run mongorestore: %w: %s", err, strings.TrimSpace(stderr.String()))
}
writer.WriteLine("MongoDB 恢复完成")
return nil
}
// mongoConnArgs 构造 mongodump/mongorestore 的连接与认证参数。
// 注意mongodb-database-tools 无类似 MYSQL_PWD 的密码环境变量,密码只能经 --password 传入;
// 认证库默认 admin绝大多数部署的管理账号所在库
func mongoConnArgs(db DatabaseSpec) []string {
args := make([]string, 0, 8)
if strings.TrimSpace(db.Host) != "" {
args = append(args, "--host", db.Host)
}
if db.Port > 0 {
args = append(args, "--port", strconv.Itoa(db.Port))
}
if strings.TrimSpace(db.User) != "" {
args = append(args, "--username", db.User, "--authenticationDatabase", "admin")
if strings.TrimSpace(db.Password) != "" {
args = append(args, "--password", db.Password)
}
}
return args
}