Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions agent/app/dto/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
111 changes: 103 additions & 8 deletions agent/app/service/backup_mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"os"
"os/exec"
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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]*$`),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/interface/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ export namespace Backup {
file: string;
secret: string;
taskID: string;
dropAllCollections?: boolean;
}
}
9 changes: 9 additions & 0 deletions frontend/src/components/backup/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/terminal/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ const message = {
redisCliHelper: '「Redis-Cli」サービスは検出されません。最初にサービスを有効にします。',
redisQuickCmd: 'Redis Quickコマンド',
recoverHelper: 'これにより、[{0}]でデータが上書きされます。続けたいですか?',
mongodbRecoverDropAllCollections: '復元前に現在のデータベースを空にする',
mongodbRecoverDropAllCollectionsHelper:
'既定では既存のコレクションのみを上書きし、新しく追加されたコレクションは保持します。有効にすると、現在のデータベースを空にしてからバックアップ内容で復元します。',
submitIt: 'データを上書きします',
baseConf: '基本',
allConf: '全て',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,9 @@ const message = {
redisCliHelper: '"redis-cli" 서비스가 감지되지 않았습니다. 서비스를 먼저 활성화하십시오.',
redisQuickCmd: 'Redis 빠른 명령',
recoverHelper: '이 작업은 데이터를 [{0}]으로 덮어씁니다. 계속하시겠습니까?',
mongodbRecoverDropAllCollections: '복원 전에 현재 데이터베이스 비우기',
mongodbRecoverDropAllCollectionsHelper:
'기본적으로는 기존 컬렉션만 덮어쓰고 새로 추가된 컬렉션은 유지합니다. 활성화하면 현재 데이터베이스를 비운 뒤 백업 내용으로 복원합니다.',
submitIt: '데이터 덮어쓰기',
baseConf: '기본 설정',
allConf: '모든 설정',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ const message = {
redisCliHelper: 'Сервис "redis-cli" не обнаружен. Сначала включите сервис.',
redisQuickCmd: 'Быстрые команды Redis',
recoverHelper: 'Это перезапишет данные с [{0}]. Хотите продолжить?',
mongodbRecoverDropAllCollections: 'Очистить текущую базу данных перед восстановлением',
mongodbRecoverDropAllCollectionsHelper:
'По умолчанию перезаписываются только существующие коллекции, а новые коллекции сохраняются. При включении текущая база данных очищается, а затем восстанавливается из резервной копии.',
submitIt: 'Перезаписать данные',
baseConf: 'Базовая',
allConf: 'Все',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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ü',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,9 @@ const message = {
redisCliHelper: '未偵測到 redis-cli 服務,請先啟用服務',
redisQuickCmd: 'Redis 快速指令',
recoverHelper: '即將使用 [{0}] 對資料進行覆蓋,是否繼續?',
mongodbRecoverDropAllCollections: '復原前清空目前資料庫',
mongodbRecoverDropAllCollectionsHelper:
'預設僅覆蓋既有集合,保留新增集合;啟用後將清空目前資料庫,並依備份內容復原。',
submitIt: '覆蓋資料',
baseConf: '基本設定',
allConf: '全部設定',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ const message = {
redisQuickCmd: 'Redis 快速命令',

recoverHelper: '即将使用 [{0}] 对数据进行覆盖,是否继续?',
mongodbRecoverDropAllCollections: '恢复前清空当前数据库',
mongodbRecoverDropAllCollectionsHelper:
'默认仅覆盖已有集合,保留新增集合;开启后将清空当前数据库,并按备份内容恢复。',
submitIt: '覆盖数据',

baseConf: '基础配置',
Expand Down
Loading