fix: Support encrypted backup of databases (#10963)

This commit is contained in:
ssongliu 2025-11-14 22:12:55 +08:00 committed by GitHub
parent 1afe84ed02
commit a6062897d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 105 additions and 86 deletions

View file

@ -102,7 +102,7 @@ func backupDatabaseWithTask(parentTask *task.Task, resourceKey, tmpDir, name str
}
parentTask.LogStart(task.GetTaskName(db.Name, task.TaskBackup, task.TaskScopeDatabase))
databaseHelper := DatabaseHelper{Database: db.MysqlName, DBType: resourceKey, Name: db.Name}
if err := handleMysqlBackup(databaseHelper, parentTask, 0, tmpDir, fmt.Sprintf("%s.sql.gz", name), ""); err != nil {
if err := handleMysqlBackup(databaseHelper, parentTask, 0, tmpDir, fmt.Sprintf("%s.sql.gz", name), "", ""); err != nil {
return err
}
parentTask.LogSuccess(task.GetTaskName(db.Name, task.TaskBackup, task.TaskScopeDatabase))
@ -113,7 +113,7 @@ func backupDatabaseWithTask(parentTask *task.Task, resourceKey, tmpDir, name str
}
parentTask.LogStart(task.GetTaskName(db.Name, task.TaskBackup, task.TaskScopeDatabase))
databaseHelper := DatabaseHelper{Database: db.PostgresqlName, DBType: resourceKey, Name: db.Name}
if err := handlePostgresqlBackup(databaseHelper, parentTask, 0, tmpDir, fmt.Sprintf("%s.sql.gz", name), ""); err != nil {
if err := handlePostgresqlBackup(databaseHelper, parentTask, 0, tmpDir, fmt.Sprintf("%s.sql.gz", name), "", ""); err != nil {
return err
}
parentTask.LogSuccess(task.GetTaskName(db.Name, task.TaskBackup, task.TaskScopeDatabase))
@ -260,7 +260,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
Name: newDB.MysqlName,
DetailName: newDB.Name,
File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, install.Name),
}, parentTask, true, ""); err != nil {
}, parentTask, true); err != nil {
t.LogFailedWithErr(taskName, err)
return err
}

View file

@ -48,7 +48,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
}
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID); err != nil {
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
@ -56,7 +56,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
}
func (u *BackupService) MysqlRecover(req dto.CommonRecover) error {
return handleMysqlRecover(req, nil, false, req.TaskID)
return handleMysqlRecover(req, nil, false)
}
func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
@ -66,13 +66,13 @@ func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
}
req.File = recoveFile
if err := handleMysqlRecover(req, nil, false, req.TaskID); err != nil {
if err := handleMysqlRecover(req, nil, false); err != nil {
return err
}
return nil
}
func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint, targetDir, fileName, taskID string) error {
func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint, targetDir, fileName, taskID, secret string) error {
var (
err error
backupTask *task.Task
@ -90,7 +90,7 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
}
}
itemHandler := func() error { return doMysqlBackup(db, targetDir, fileName) }
itemHandler := func() error { return doMysqlBackup(db, targetDir, fileName, secret) }
if parentTask != nil {
return itemHandler()
}
@ -105,7 +105,7 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
return nil
}
func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool, taskID string) error {
func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool) error {
var (
err error
itemTask *task.Task
@ -117,7 +117,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
}
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
if parentTask == nil {
itemTask, err = task.NewTaskWithOps(itemName, task.TaskRecover, task.TaskScopeBackup, taskID, dbInfo.ID)
itemTask, err = task.NewTaskWithOps(itemName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, dbInfo.ID)
if err != nil {
return err
}
@ -170,6 +170,15 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
}
}()
}
if len(req.Secret) != 0 {
err = files.OpensslDecrypt(req.File, req.Secret)
if err != nil {
return err
}
req.File = path.Join(path.Dir(req.File), "tmp_"+path.Base(req.File))
defer os.Remove(req.File)
t.LogWithStatus(i18n.GetMsgByKey("Decrypt"), err)
}
if err := cli.Recover(client.RecoverInfo{
Name: req.DetailName,
Type: req.Type,
@ -194,7 +203,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
return nil
}
func doMysqlBackup(db DatabaseHelper, targetDir, fileName string) error {
func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error {
dbInfo, err := mysqlRepo.Get(repo.WithByName(db.Name), mysqlRepo.WithByMysqlName(db.Database))
if err != nil {
return err
@ -211,7 +220,13 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName string) error {
TargetDir: targetDir,
FileName: fileName,
}
return cli.Backup(backupInfo)
if err := cli.Backup(backupInfo); err != nil {
return err
}
if len(secret) != 0 {
return files.OpensslEncrypt(path.Join(targetDir, fileName), secret)
}
return nil
}
func loadSqlFile(file string) (string, error) {

View file

@ -6,21 +6,18 @@ import (
"path"
"time"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/common"
pgclient "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/postgresql/client"
pgclient "github.com/1Panel-dev/1Panel/agent/utils/postgresql/client"
)
func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
@ -46,7 +43,7 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
}
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID); err != nil {
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
return err
}
return nil
@ -71,7 +68,7 @@ func (u *BackupService) PostgresqlRecoverByUpload(req dto.CommonRecover) error {
return nil
}
func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint, targetDir, fileName, taskID string) error {
func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint, targetDir, fileName, taskID, secret string) error {
var (
err error
backupTask *task.Task
@ -85,7 +82,7 @@ func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID u
}
}
itemHandler := func() error { return doPostgresqlgBackup(db, targetDir, fileName) }
itemHandler := func() error { return doPostgresqlgBackup(db, targetDir, fileName, secret) }
if parentTask != nil {
return itemHandler()
}
@ -160,6 +157,15 @@ func handlePostgresqlRecover(req dto.CommonRecover, parentTask *task.Task, isRol
}
}()
}
if len(req.Secret) != 0 {
err = files.OpensslDecrypt(req.File, req.Secret)
if err != nil {
return err
}
req.File = path.Join(path.Dir(req.File), "tmp_"+path.Base(req.File))
defer os.Remove(req.File)
t.LogWithStatus(i18n.GetMsgByKey("Decrypt"), err)
}
if err := cli.Recover(client.RecoverInfo{
Name: req.DetailName,
SourceFile: req.File,
@ -183,7 +189,7 @@ func handlePostgresqlRecover(req dto.CommonRecover, parentTask *task.Task, isRol
return nil
}
func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName string) error {
func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName, secret string) error {
cli, err := LoadPostgresqlClientByFrom(db.Database)
if err != nil {
return err
@ -196,5 +202,11 @@ func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName string) error {
Timeout: 300,
}
return cli.Backup(backupInfo)
if err := cli.Backup(backupInfo); err != nil {
return err
}
if len(secret) != 0 {
return files.OpensslEncrypt(path.Join(targetDir, fileName), secret)
}
return nil
}

View file

@ -363,7 +363,7 @@ func recoverWebsiteDatabase(t *task.Task, dbID uint, dbType, tmpPath, websiteKey
Name: db.MysqlName,
DetailName: db.Name,
File: fmt.Sprintf("%s/%s.sql.gz", tmpPath, websiteKey),
}, t, true, ""); err != nil {
}, t, true); err != nil {
t.LogFailedWithErr(taskName, err)
return err
}

View file

@ -167,7 +167,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
record.FileName = simplifiedFileName(fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)))
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
if err := doMysqlBackup(dbInfo, backupDir, record.FileName); err != nil {
if err := doMysqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
@ -178,7 +178,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
}
}
} else {
if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName); err != nil {
if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName, cronjob.Secret); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err

View file

@ -36,6 +36,7 @@ Failed: 'Failed'
SystemRestart: 'Task interrupted due to system restart'
ErrGroupIsDefault: 'Default group, cannot be deleted'
ErrGroupIsInWebsiteUse: 'The group is being used by another website and cannot be deleted.'
Decrypt: "Decrypt"
#backup
Localhost: 'Local Machine'

View file

@ -36,6 +36,7 @@ Failed: 'Error'
SystemRestart: 'La tarea fue interrumpida debido a un reinicio del sistema'
ErrGroupIsDefault: 'Grupo predeterminado, no se puede eliminar'
ErrGroupIsInWebsiteUse: 'El grupo está siendo usado por otro sitio web y no se puede eliminar.'
Decrypt: "Descifrar"
#backup
ErrBackupInUsed: 'La cuenta de respaldo está siendo utilizada en una tarea programada y no se puede eliminar.'

View file

@ -35,6 +35,7 @@ Failed: '失敗'
SystemRestart: 'システムの再起動によりタスクが中断されました'
ErrGroupIsDefault: 'デフォルト グループ、削除できません'
ErrGroupIsInWebsiteUse: 'グループは別の Web サイトで使用されているため、削除できません。'
Decrypt: "復号化"
#backup
Localhost: 'ローカルマシン'

View file

@ -36,6 +36,7 @@ Failed: '실패했습니다'
SystemRestart: '시스템 재시작으로 인해 작업이 중단되었습니다'
ErrGroupIsDefault: '기본 그룹, 삭제할 수 없습니다'
ErrGroupIsInWebsiteUse: '그룹이 다른 웹사이트에서 사용 중이므로 삭제할 수 없습니다.'
Decrypt: "복호화"
#지원
Localhost: '로컬 머신'

View file

@ -39,6 +39,7 @@ Failed: 'Gagal'
SystemRestart: 'Tugas terganggu kerana sistem mula semula'
ErrGroupIsDefault: 'Kumpulan lalai, tidak boleh dipadamkan'
ErrGroupIsInWebsiteUse: 'Kumpulan sedang digunakan oleh tapak web lain dan tidak boleh dipadamkan.'
Decrypt: "Dekripsi"
#sandaran
Localhost: 'Mesin Tempatan'

View file

@ -39,6 +39,7 @@ Failed: 'Falhou'
SystemRestart: 'Tarefa interrompida devido à reinicialização do sistema'
ErrGroupIsDefault: 'Grupo padrão, não pode ser excluído'
ErrGroupIsInWebsiteUse: 'O grupo está sendo usado por outro site e não pode ser excluído.'
Decrypt: "Descriptografar"
#backup
Localhost: 'Máquina Local'

View file

@ -39,6 +39,7 @@ Failed: 'Не удалось'
SystemRestart: 'Задача прервана из-за перезапуска системы'
ErrGroupIsDefault: 'Группа по умолчанию, не может быть удалена'
ErrGroupIsInWebsiteUse: 'Группа используется другим веб-сайтом и не может быть удалена.'
Decrypt: "Расшифровать"
#резервное копирование
Localhost: 'Локальная машина'

View file

@ -39,6 +39,7 @@ Failed: 'Başarısız'
SystemRestart: 'Sistem yeniden başlatması nedeniyle görev kesildi'
ErrGroupIsDefault: 'Varsayılan grup, silinemez'
ErrGroupIsInWebsiteUse: 'Grup başka bir web sitesi tarafından kullanılıyor ve silinemez.'
Decrypt: "Şifre Çöz"
#backup
Localhost: 'Yerel Makine'

View file

@ -36,6 +36,7 @@ Failed: '失敗'
SystemRestart: '系統重新啟動導致任務中斷'
ErrGroupIsDefault: '預設分組,無法刪除'
ErrGroupIsInWebsiteUse: '分組正在被其他網站使用,無法刪除'
Decrypt: "解密"
#backup
Localhost: '本機'

View file

@ -36,6 +36,7 @@ Failed: "失败"
SystemRestart: "系统重启导致任务中断"
ErrGroupIsDefault: "默认分组,无法删除"
ErrGroupIsInWebsiteUse: "分组正在被其他网站使用,无法删除"
Decrypt: "解密"
#backup
Localhost: '本机'

View file

@ -957,3 +957,23 @@ func CopyCustomAppFile(srcPath, dstPath string) error {
}
return nil
}
func OpensslEncrypt(filePath, secret string) error {
tmpName := path.Join(path.Dir(filePath), "tmp_"+path.Base(filePath))
if err := cmd.RunDefaultBashCf("MY_PASS='%s' openssl enc -aes-256-cbc -salt -pass env:MY_PASS -in %s -out %s", secret, filePath, tmpName); err != nil {
_ = os.Remove(tmpName)
return err
}
return os.Rename(tmpName, filePath)
}
func OpensslDecrypt(filePath, secret string) error {
tmpName := path.Join(path.Dir(filePath), "tmp_"+path.Base(filePath))
if err := cmd.RunDefaultBashCf("MY_PASS='%s' openssl enc -aes-256-cbc -d -salt -pass env:MY_PASS -in %s -out %s", secret, filePath, tmpName); err != nil {
if strings.Contains(err.Error(), "bad decrypt") || strings.Contains(err.Error(), "bad magic number") {
return buserr.New("ErrBadDecrypt")
}
return err
}
return nil
}

View file

@ -107,7 +107,7 @@
{{ $t('commons.msg.' + (isBackup ? 'backupHelper' : 'recoverHelper'), [name + '( ' + detailName + ' )']) }}
</el-alert>
<el-form class="mt-5" ref="backupForm" @submit.prevent label-position="top" v-loading="loading">
<el-form-item :label="$t('setting.compressPassword')" v-if="type === 'app' || type === 'website'">
<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" :label="$t('commons.table.description')">
@ -281,7 +281,7 @@ function selectable(row) {
return row.status !== 'Waiting';
}
const backup = async (close: boolean) => {
const backup = async () => {
const taskID = newUUID();
let params = {
type: type.value,
@ -292,23 +292,18 @@ const backup = async (close: boolean) => {
description: description.value,
};
loading.value = true;
try {
await handleBackup(params);
await handleBackup(params)
.then(() => {
loading.value = false;
if (close) {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
} else {
openTaskLog(taskID);
}
handleBackupClose();
} catch (error) {
})
.catch(() => {
loading.value = false;
}
});
};
const recover = async (close: boolean, row?: any) => {
const recover = async (row?: any) => {
const taskID = newUUID();
let params = {
downloadAccountID: row.downloadAccountID,
@ -324,14 +319,8 @@ const recover = async (close: boolean, row?: any) => {
await handleRecover(params)
.then(() => {
loading.value = false;
handleBackupClose();
if (close) {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
} else {
openTaskLog(taskID);
}
handleBackupClose();
})
.catch(() => {
loading.value = false;
@ -340,6 +329,7 @@ const recover = async (close: boolean, row?: any) => {
const onBackup = async () => {
description.value = '';
secret.value = '';
isBackup.value = true;
open.value = true;
};
@ -347,28 +337,15 @@ const onBackup = async () => {
const onRecover = async (row: Backup.RecordInfo) => {
secret.value = '';
isBackup.value = false;
if (type.value !== 'app' && type.value !== 'website') {
ElMessageBox.confirm(
i18n.global.t('commons.msg.recoverHelper', [name.value + '( ' + detailName.value + ' )']),
i18n.global.t('commons.button.recover'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
).then(async () => {
recover(true, row);
});
return;
}
recordInfo.value = row;
open.value = true;
};
const onSubmit = () => {
if (isBackup.value) {
backup(false);
backup();
} else {
recover(false, recordInfo.value);
recover(recordInfo.value);
}
};

View file

@ -88,13 +88,10 @@
v-model="recoverDialog"
:title="$t('commons.button.recover') + ' - ' + name"
@close="handleRecoverClose"
size="small"
>
<el-form ref="backupForm" label-position="left" v-loading="loading">
<el-form-item
:label="$t('setting.compressPassword')"
class="mt-5"
v-if="type === 'app' || type === 'website'"
>
<el-form ref="backupForm" @submit.prevent label-position="top" v-loading="loading">
<el-form-item :label="$t('setting.compressPassword')">
<el-input v-model="secret" :placeholder="$t('setting.backupRecoverMessage')" />
</el-form-item>
</el-form>
@ -282,19 +279,7 @@ const onHandleRecover = async () => {
const onRecover = async (row: File.File) => {
currentRow.value = row;
if (type.value !== 'app' && type.value !== 'website') {
ElMessageBox.confirm(
i18n.global.t('commons.msg.recoverHelper', [row.name]),
i18n.global.t('commons.button.recover'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
},
).then(async () => {
onHandleRecover();
});
return;
}
secret.value = '';
recoverDialog.value = true;
};