mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-06 00:00:14 +00:00
fix: mongodb restore fixes (#12496)
* fix mongodb restore flow * chore cleanup i18n and terminal style
This commit is contained in:
+10
-9
@@ -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 {
|
||||
|
||||
@@ -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]*$`),
|
||||
|
||||
@@ -103,5 +103,6 @@ export namespace Backup {
|
||||
file: string;
|
||||
secret: string;
|
||||
taskID: string;
|
||||
dropAllCollections?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -645,6 +645,9 @@ const message = {
|
||||
redisCliHelper: '「Redis-Cli」サービスは検出されません。最初にサービスを有効にします。',
|
||||
redisQuickCmd: 'Redis Quickコマンド',
|
||||
recoverHelper: 'これにより、[{0}]でデータが上書きされます。続けたいですか?',
|
||||
mongodbRecoverDropAllCollections: '復元前に現在のデータベースを空にする',
|
||||
mongodbRecoverDropAllCollectionsHelper:
|
||||
'既定では既存のコレクションのみを上書きし、新しく追加されたコレクションは保持します。有効にすると、現在のデータベースを空にしてからバックアップ内容で復元します。',
|
||||
submitIt: 'データを上書きします',
|
||||
baseConf: '基本',
|
||||
allConf: '全て',
|
||||
|
||||
@@ -637,6 +637,9 @@ const message = {
|
||||
redisCliHelper: '"redis-cli" 서비스가 감지되지 않았습니다. 서비스를 먼저 활성화하십시오.',
|
||||
redisQuickCmd: 'Redis 빠른 명령',
|
||||
recoverHelper: '이 작업은 데이터를 [{0}]으로 덮어씁니다. 계속하시겠습니까?',
|
||||
mongodbRecoverDropAllCollections: '복원 전에 현재 데이터베이스 비우기',
|
||||
mongodbRecoverDropAllCollectionsHelper:
|
||||
'기본적으로는 기존 컬렉션만 덮어쓰고 새로 추가된 컬렉션은 유지합니다. 활성화하면 현재 데이터베이스를 비운 뒤 백업 내용으로 복원합니다.',
|
||||
submitIt: '데이터 덮어쓰기',
|
||||
baseConf: '기본 설정',
|
||||
allConf: '모든 설정',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -641,6 +641,9 @@ const message = {
|
||||
redisCliHelper: 'Сервис "redis-cli" не обнаружен. Сначала включите сервис.',
|
||||
redisQuickCmd: 'Быстрые команды Redis',
|
||||
recoverHelper: 'Это перезапишет данные с [{0}]. Хотите продолжить?',
|
||||
mongodbRecoverDropAllCollections: 'Очистить текущую базу данных перед восстановлением',
|
||||
mongodbRecoverDropAllCollectionsHelper:
|
||||
'По умолчанию перезаписываются только существующие коллекции, а новые коллекции сохраняются. При включении текущая база данных очищается, а затем восстанавливается из резервной копии.',
|
||||
submitIt: 'Перезаписать данные',
|
||||
baseConf: 'Базовая',
|
||||
allConf: 'Все',
|
||||
|
||||
@@ -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ü',
|
||||
|
||||
@@ -615,6 +615,9 @@ const message = {
|
||||
redisCliHelper: '未偵測到 redis-cli 服務,請先啟用服務',
|
||||
redisQuickCmd: 'Redis 快速指令',
|
||||
recoverHelper: '即將使用 [{0}] 對資料進行覆蓋,是否繼續?',
|
||||
mongodbRecoverDropAllCollections: '復原前清空目前資料庫',
|
||||
mongodbRecoverDropAllCollectionsHelper:
|
||||
'預設僅覆蓋既有集合,保留新增集合;啟用後將清空目前資料庫,並依備份內容復原。',
|
||||
submitIt: '覆蓋資料',
|
||||
baseConf: '基本設定',
|
||||
allConf: '全部設定',
|
||||
|
||||
@@ -611,6 +611,9 @@ const message = {
|
||||
redisQuickCmd: 'Redis 快速命令',
|
||||
|
||||
recoverHelper: '即将使用 [{0}] 对数据进行覆盖,是否继续?',
|
||||
mongodbRecoverDropAllCollections: '恢复前清空当前数据库',
|
||||
mongodbRecoverDropAllCollectionsHelper:
|
||||
'默认仅覆盖已有集合,保留新增集合;开启后将清空当前数据库,并按备份内容恢复。',
|
||||
submitIt: '覆盖数据',
|
||||
|
||||
baseConf: '基础配置',
|
||||
|
||||
Reference in New Issue
Block a user