fix: mongodb restore fixes (#12496)

* fix mongodb restore flow

* chore cleanup i18n and terminal style
This commit is contained in:
王贺
2026-04-15 15:26:30 +08:00
committed by GitHub
parent 6a28359ac2
commit 7f5bb58c72
15 changed files with 154 additions and 20 deletions
+10 -9
View File
@@ -77,15 +77,16 @@ type CommonBackup struct {
Description string `json:"description"`
}
type CommonRecover struct {
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
File string `json:"file"`
Secret string `json:"secret"`
TaskID string `json:"taskID"`
BackupRecordID uint `json:"backupRecordID"`
Timeout int `json:"timeout"`
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
File string `json:"file"`
Secret string `json:"secret"`
DropAllCollections bool `json:"dropAllCollections"`
TaskID string `json:"taskID"`
BackupRecordID uint `json:"backupRecordID"`
Timeout int `json:"timeout"`
}
type RecordSearch struct {
+103 -8
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"os"
"os/exec"
@@ -27,6 +28,8 @@ import (
dockerClient "github.com/docker/docker/client"
)
const opensslSaltedHeader = "Salted__"
func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
timeNow := time.Now().Format(constant.DateTimeSlimLayout)
itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName)
@@ -123,15 +126,11 @@ func handleMongodbRecover(req dto.CommonRecover, parentTask *task.Task, isRollba
return buserr.WithName("ErrFileNotFound", req.File)
}
restoreFile := req.File
if len(req.Secret) != 0 {
if err := files.OpensslDecrypt(req.File, req.Secret); err != nil {
return err
}
restoreFile = path.Join(path.Dir(req.File), "tmp_"+path.Base(req.File))
defer os.Remove(restoreFile)
t.LogWithStatus(i18n.GetMsgByKey("Decrypt"), nil)
restoreFile, cleanup, err := prepareMongodbBackupFileForRestore(req.File, req.Secret, t)
if err != nil {
return err
}
defer cleanup()
isOk := false
if !isRollback {
@@ -155,6 +154,12 @@ func handleMongodbRecover(req dto.CommonRecover, parentTask *task.Task, isRollba
}()
}
if req.DropAllCollections {
if err := clearMongodbDatabase(req.Name, req.Type, req.DetailName, t); err != nil {
return err
}
}
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, restoreFile, t); err != nil {
global.LOG.Errorf("recover mongodb db %s from %s failed, err: %v", req.DetailName, restoreFile, err)
return err
@@ -276,6 +281,93 @@ func doMongodbRestore(database, dbType, dbName, sourceFile string, taskItem *tas
return nil
}
func prepareMongodbBackupFileForRestore(filePath, secret string, taskItem *task.Task) (string, func(), error) {
isEncrypted, err := isOpenSSLEncryptedMongodbBackup(filePath)
if err != nil {
return "", nil, err
}
if !isEncrypted {
return filePath, func() {}, nil
}
if secret == "" {
return "", nil, buserr.New("ErrBadDecrypt")
}
if err := files.OpensslDecrypt(filePath, secret); err != nil {
return "", nil, err
}
restoreFile := path.Join(path.Dir(filePath), "tmp_"+path.Base(filePath))
taskItem.LogWithStatus(i18n.GetMsgByKey("Decrypt"), nil)
return restoreFile, func() { _ = os.Remove(restoreFile) }, nil
}
func isOpenSSLEncryptedMongodbBackup(filePath string) (bool, error) {
file, err := os.Open(filePath)
if err != nil {
return false, err
}
defer file.Close()
header := make([]byte, len(opensslSaltedHeader))
n, err := io.ReadFull(file, header)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return false, nil
}
return false, err
}
return n == len(opensslSaltedHeader) && string(header) == opensslSaltedHeader, nil
}
func clearMongodbDatabase(database, dbType, dbName string, taskItem *task.Task) error {
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
if err == nil && dbItem.From == constant.AppResourceRemote {
return clearRemoteMongodbDatabase(database, dbName, taskItem)
}
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
if err != nil {
return err
}
if appInfo.ContainerName == "" {
return fmt.Errorf("mongodb container not found for database %s", database)
}
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear local mongodb database %s before restore", dbName))
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
return mongodbCmdMgr(taskItem).Run(
"docker",
"exec",
appInfo.ContainerName,
"mongosh",
uri,
"--quiet",
"--eval",
fmt.Sprintf(`db.getSiblingDB(%q).dropDatabase()`, dbName),
)
}
func clearRemoteMongodbDatabase(database, dbName string, taskItem *task.Task) error {
info, err := loadRemoteMongodbConnection(database)
if err != nil {
return err
}
client, ctx, cancel, err := newRemoteMongodbClient(info)
if err != nil {
return err
}
defer cancel()
defer client.Disconnect(context.Background())
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear remote mongodb database %s before restore", dbName))
if err := client.Database(dbName).Drop(ctx); err != nil {
return fmt.Errorf("drop mongodb database %s failed, err: %v", dbName, err)
}
return nil
}
func buildMongodbDumpURI(username, password, dbName string) string {
return (&url.URL{
Scheme: "mongodb",
@@ -444,6 +536,9 @@ func loadMongodbBackupDBName(sourceFile, defaultDB string) string {
if strings.HasSuffix(baseName, ".gz") {
baseName = strings.TrimSuffix(baseName, ".gz")
}
// Encrypted backups are restored from a decrypted temp file like tmp_<original>.
// Strip the temp prefix before deriving the original database name.
baseName = strings.TrimPrefix(baseName, "tmp_")
patterns := []*regexp.Regexp{
regexp.MustCompile(`^1panel_mongodb_(.+)_\d{14}[A-Za-z0-9]*$`),
regexp.MustCompile(`^db_(.+)_\d{14}[A-Za-z0-9]*$`),
+1
View File
@@ -103,5 +103,6 @@ export namespace Backup {
file: string;
secret: string;
taskID: string;
dropAllCollections?: boolean;
}
}
+9
View File
@@ -123,6 +123,12 @@
<el-form-item :label="$t('setting.compressPassword')">
<el-input v-model="secret" :placeholder="$t('setting.backupRecoverMessage')" />
</el-form-item>
<el-form-item v-if="!isBackup && type === 'mongodb'">
<el-checkbox v-model="dropAllCollections">
{{ $t('database.mongodbRecoverDropAllCollections') }}
</el-checkbox>
<span class="input-help">{{ $t('database.mongodbRecoverDropAllCollectionsHelper') }}</span>
</el-form-item>
<el-form-item v-if="type === 'mysql' || type === 'mysql-cluster'" :label="$t('cronjob.backupArgs')">
<el-select v-model="args" filterable allow-create multiple>
<el-option v-for="item in mysqlArgs" :key="item.arg" :value="item.arg" :label="item.arg">
@@ -228,6 +234,7 @@ const timeoutItem = ref(30);
const timeoutUnit = ref('m');
const node = ref();
const stopBefore = ref(false);
const dropAllCollections = ref(false);
const open = ref();
const isBackup = ref();
@@ -366,6 +373,7 @@ const recover = async (row?: any) => {
taskID: taskID,
backupRecordID: row.id,
timeout: timeoutItem.value === -1 ? -1 : transferTimeToSecond(timeoutItem.value + timeoutUnit.value),
dropAllCollections: type.value === 'mongodb' ? dropAllCollections.value : false,
};
loading.value = true;
await handleRecover(params, node.value)
@@ -390,6 +398,7 @@ const onBackup = async () => {
const onRecover = async (row: Backup.RecordInfo) => {
secret.value = '';
dropAllCollections.value = false;
isBackup.value = false;
recordInfo.value = row;
open.value = true;
+1 -3
View File
@@ -555,9 +555,7 @@ onBeforeUnmount(() => {
.ai-notice-fade-enter-active,
.ai-notice-fade-leave-active {
transition:
opacity 180ms ease,
transform 180ms ease;
transition: opacity 180ms ease, transform 180ms ease;
}
.ai-mask-fade-enter-active,
+3
View File
@@ -643,6 +643,9 @@ const message = {
redisCliHelper: '"redis-cli" not detected. Enable the service first.',
redisQuickCmd: 'Redis quick commands',
recoverHelper: 'This will overwrite data with [{0}]. Continue?',
mongodbRecoverDropAllCollections: 'Clear current database before restore',
mongodbRecoverDropAllCollectionsHelper:
'By default, only existing collections are overwritten and newly added collections are kept. When enabled, the current database is cleared and then restored from the backup.',
submitIt: 'Overwrite the data',
baseConf: 'Basic',
allConf: 'All',
+3
View File
@@ -649,6 +649,9 @@ const message = {
redisCliHelper: 'No se detectó el servicio "redis-cli". Primero habilite el servicio.',
redisQuickCmd: 'Comandos rápidos de Redis',
recoverHelper: 'Esto sobrescribirá los datos con [{0}]. ¿Desea continuar?',
mongodbRecoverDropAllCollections: 'Vaciar la base de datos actual antes de restaurar',
mongodbRecoverDropAllCollectionsHelper:
'De forma predeterminada, solo se sobrescriben las colecciones existentes y se conservan las colecciones nuevas. Al activarlo, se vacía la base de datos actual y luego se restaura desde la copia de seguridad.',
submitIt: 'Sobrescribir los datos',
baseConf: 'Básico',
allConf: 'Todos',
+3
View File
@@ -645,6 +645,9 @@ const message = {
redisCliHelper: '「Redis-Cli」サービスは検出されません。最初にサービスを有効にします。',
redisQuickCmd: 'Redis Quickコマンド',
recoverHelper: 'これにより、[{0}]でデータが上書きされます。続けたいですか?',
mongodbRecoverDropAllCollections: '復元前に現在のデータベースを空にする',
mongodbRecoverDropAllCollectionsHelper:
'既定では既存のコレクションのみを上書きし、新しく追加されたコレクションは保持します。有効にすると、現在のデータベースを空にしてからバックアップ内容で復元します。',
submitIt: 'データを上書きします',
baseConf: '基本',
allConf: '全て',
+3
View File
@@ -637,6 +637,9 @@ const message = {
redisCliHelper: '"redis-cli" 서비스가 감지되지 않았습니다. 서비스를 먼저 활성화하십시오.',
redisQuickCmd: 'Redis 빠른 명령',
recoverHelper: '이 작업은 데이터를 [{0}]으로 덮어씁니다. 계속하시겠습니까?',
mongodbRecoverDropAllCollections: '복원 전에 현재 데이터베이스 비우기',
mongodbRecoverDropAllCollectionsHelper:
'기본적으로는 기존 컬렉션만 덮어쓰고 새로 추가된 컬렉션은 유지합니다. 활성화하면 현재 데이터베이스를 비운 뒤 백업 내용으로 복원합니다.',
submitIt: '데이터 덮어쓰기',
baseConf: '기본 설정',
allConf: '모든 설정',
+3
View File
@@ -652,6 +652,9 @@ const message = {
redisCliHelper: '"redis-cli" perkhidmatan tidak dikesan. Aktifkan perkhidmatan terlebih dahulu.',
redisQuickCmd: 'Arahan pantas Redis',
recoverHelper: 'Ini akan menimpa data dengan [{0}]. Adakah anda mahu meneruskan?',
mongodbRecoverDropAllCollections: 'Kosongkan pangkalan data semasa sebelum pemulihan',
mongodbRecoverDropAllCollectionsHelper:
'Secara lalai, hanya koleksi sedia ada akan ditulis ganti dan koleksi baharu akan dikekalkan. Apabila diaktifkan, pangkalan data semasa akan dikosongkan dan kemudian dipulihkan daripada sandaran.',
submitIt: 'Tindih data',
baseConf: 'Asas',
allConf: 'Semua',
+3
View File
@@ -646,6 +646,9 @@ const message = {
redisCliHelper: '"redis-cli" não foi detectado. Habilite o serviço primeiro.',
redisQuickCmd: 'Comandos rápidos Redis',
recoverHelper: 'Isso sobrescreverá os dados com [{0}]. Deseja continuar?',
mongodbRecoverDropAllCollections: 'Limpar o banco de dados atual antes da restauração',
mongodbRecoverDropAllCollectionsHelper:
'Por padrão, apenas as coleções existentes são sobrescritas e as coleções novas são mantidas. Quando ativado, o banco de dados atual é limpo e depois restaurado a partir do backup.',
submitIt: 'Sobrescrever os dados',
baseConf: 'Básico',
allConf: 'Todos',
+3
View File
@@ -641,6 +641,9 @@ const message = {
redisCliHelper: 'Сервис "redis-cli" не обнаружен. Сначала включите сервис.',
redisQuickCmd: 'Быстрые команды Redis',
recoverHelper: 'Это перезапишет данные с [{0}]. Хотите продолжить?',
mongodbRecoverDropAllCollections: 'Очистить текущую базу данных перед восстановлением',
mongodbRecoverDropAllCollectionsHelper:
'По умолчанию перезаписываются только существующие коллекции, а новые коллекции сохраняются. При включении текущая база данных очищается, а затем восстанавливается из резервной копии.',
submitIt: 'Перезаписать данные',
baseConf: 'Базовая',
allConf: 'Все',
+3
View File
@@ -647,6 +647,9 @@ const message = {
redisCliHelper: '"redis-cli" servisi algılanmadı. Önce servisi etkinleştirin.',
redisQuickCmd: 'Redis hızlı komutları',
recoverHelper: 'Bu işlem verileri [{0}] ile üzerine yazacak. Devam etmek istiyor musunuz?',
mongodbRecoverDropAllCollections: 'Geri yüklemeden önce mevcut veritabanını temizle',
mongodbRecoverDropAllCollectionsHelper:
'Varsayılan olarak yalnızca mevcut koleksiyonların üzerine yazılır ve yeni eklenen koleksiyonlar korunur. Etkinleştirildiğinde mevcut veritabanı temizlenir ve ardından yedekten geri yüklenir.',
submitIt: 'Verilerin üzerine yaz',
baseConf: 'Temel',
allConf: 'Tümü',
+3
View File
@@ -615,6 +615,9 @@ const message = {
redisCliHelper: '未偵測到 redis-cli 服務,請先啟用服務',
redisQuickCmd: 'Redis 快速指令',
recoverHelper: '即將使用 [{0}] 對資料進行覆蓋,是否繼續?',
mongodbRecoverDropAllCollections: '復原前清空目前資料庫',
mongodbRecoverDropAllCollectionsHelper:
'預設僅覆蓋既有集合,保留新增集合;啟用後將清空目前資料庫,並依備份內容復原。',
submitIt: '覆蓋資料',
baseConf: '基本設定',
allConf: '全部設定',
+3
View File
@@ -611,6 +611,9 @@ const message = {
redisQuickCmd: 'Redis 快速命令',
recoverHelper: '即将使用 [{0}] 对数据进行覆盖,是否继续?',
mongodbRecoverDropAllCollections: '恢复前清空当前数据库',
mongodbRecoverDropAllCollectionsHelper:
'默认仅覆盖已有集合,保留新增集合;开启后将清空当前数据库,并按备份内容恢复。',
submitIt: '覆盖数据',
baseConf: '基础配置',