mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 15:06:37 +08:00
feat: 优化 PostgreSQL 远程数据库备份逻辑 (#3538)
This commit is contained in:
parent
9a905750af
commit
47524dc49b
10 changed files with 67 additions and 23 deletions
|
@ -116,6 +116,7 @@ var (
|
||||||
ErrInUsed = "ErrInUsed"
|
ErrInUsed = "ErrInUsed"
|
||||||
ErrObjectInUsed = "ErrObjectInUsed"
|
ErrObjectInUsed = "ErrObjectInUsed"
|
||||||
ErrPortRules = "ErrPortRules"
|
ErrPortRules = "ErrPortRules"
|
||||||
|
ErrPgImagePull = "ErrPgImagePull"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runtime
|
// runtime
|
||||||
|
|
|
@ -125,6 +125,7 @@ ErrTypeOfRedis: "The recovery file type does not match the current persistence m
|
||||||
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
|
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
|
||||||
ErrObjectInUsed: "This object is in use and cannot be deleted"
|
ErrObjectInUsed: "This object is in use and cannot be deleted"
|
||||||
ErrPortRules: "The number of ports does not match, please re-enter!"
|
ErrPortRules: "The number of ports does not match, please re-enter!"
|
||||||
|
ErrPgImagePull: "Image pull timeout. Please configure image acceleration or manually pull the postgres:16.0-alpine image and try again"
|
||||||
|
|
||||||
#runtime
|
#runtime
|
||||||
ErrDirNotFound: "The build folder does not exist! Please check file integrity!"
|
ErrDirNotFound: "The build folder does not exist! Please check file integrity!"
|
||||||
|
|
|
@ -126,6 +126,7 @@ ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符,請修改後
|
||||||
ErrInUsed: "{{ .detail }} 正被使用,無法刪除"
|
ErrInUsed: "{{ .detail }} 正被使用,無法刪除"
|
||||||
ErrObjectInUsed: "該對象正被使用,無法刪除"
|
ErrObjectInUsed: "該對象正被使用,無法刪除"
|
||||||
ErrPortRules: "端口數目不匹配,請重新輸入!"
|
ErrPortRules: "端口數目不匹配,請重新輸入!"
|
||||||
|
ErrPgImagePull: "鏡像拉取超時,請配置鏡像加速或手動拉取 postgres:16.0-alpine 鏡像後重試"
|
||||||
|
|
||||||
#runtime
|
#runtime
|
||||||
ErrDirNotFound: "build 文件夾不存在!請檢查文件完整性!"
|
ErrDirNotFound: "build 文件夾不存在!請檢查文件完整性!"
|
||||||
|
|
|
@ -125,6 +125,7 @@ ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后
|
||||||
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
|
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
|
||||||
ErrObjectInUsed: "该对象正被使用,无法删除"
|
ErrObjectInUsed: "该对象正被使用,无法删除"
|
||||||
ErrPortRules: "端口数目不匹配,请重新输入!"
|
ErrPortRules: "端口数目不匹配,请重新输入!"
|
||||||
|
ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 postgres:16.0-alpine 镜像后重试"
|
||||||
|
|
||||||
#runtime
|
#runtime
|
||||||
ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!"
|
ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!"
|
||||||
|
|
|
@ -122,6 +122,10 @@ func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Remote) Backup(info BackupInfo) error {
|
func (r *Remote) Backup(info BackupInfo) error {
|
||||||
|
imageTag, err := loadImageTag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(info.TargetDir) {
|
if !fileOp.Stat(info.TargetDir) {
|
||||||
if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil {
|
||||||
|
@ -129,7 +133,6 @@ func (r *Remote) Backup(info BackupInfo) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz")
|
fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz")
|
||||||
imageTag := loadImageTag()
|
|
||||||
backupCommand := exec.Command("bash", "-c",
|
backupCommand := exec.Command("bash", "-c",
|
||||||
fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_dump -h %s -p %d --no-owner -Fc -U %s %s' > %s",
|
fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_dump -h %s -p %d --no-owner -Fc -U %s %s' > %s",
|
||||||
imageTag, r.Password, r.Address, r.Port, r.User, info.Name, fileNameItem))
|
imageTag, r.Password, r.Address, r.Port, r.User, info.Name, fileNameItem))
|
||||||
|
@ -156,6 +159,10 @@ func (r *Remote) Backup(info BackupInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Remote) Recover(info RecoverInfo) error {
|
func (r *Remote) Recover(info RecoverInfo) error {
|
||||||
|
imageTag, err := loadImageTag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fileName := info.SourceFile
|
fileName := info.SourceFile
|
||||||
if strings.HasSuffix(info.SourceFile, ".sql.gz") {
|
if strings.HasSuffix(info.SourceFile, ".sql.gz") {
|
||||||
fileName = strings.TrimSuffix(info.SourceFile, ".gz")
|
fileName = strings.TrimSuffix(info.SourceFile, ".gz")
|
||||||
|
@ -169,7 +176,6 @@ func (r *Remote) Recover(info RecoverInfo) error {
|
||||||
_, _ = gzipCmd.CombinedOutput()
|
_, _ = gzipCmd.CombinedOutput()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
imageTag := loadImageTag()
|
|
||||||
recoverCommand := exec.Command("bash", "-c",
|
recoverCommand := exec.Command("bash", "-c",
|
||||||
fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_restore -h %s -p %d --verbose --clean --no-privileges --no-owner -Fc -U %s -d %s --role=%s' < %s",
|
fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'PGPASSWORD=%s pg_restore -h %s -p %d --verbose --clean --no-privileges --no-owner -Fc -U %s -d %s --role=%s' < %s",
|
||||||
imageTag, r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName))
|
imageTag, r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName))
|
||||||
|
@ -241,36 +247,64 @@ func (r *Remote) ExecSQL(command string, timeout uint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadImageTag() string {
|
func loadImageTag() (string, error) {
|
||||||
var (
|
var (
|
||||||
app model.App
|
app model.App
|
||||||
appDetails []model.AppDetail
|
appDetails []model.AppDetail
|
||||||
itemTag = "postgres:16.1-alpine"
|
versions []string
|
||||||
)
|
)
|
||||||
if err := global.DB.Where("key = ?", "postgresql").First(&app).Error; err != nil {
|
if err := global.DB.Where("key = ?", "postgresql").First(&app).Error; err != nil {
|
||||||
return itemTag
|
versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"}
|
||||||
}
|
} else {
|
||||||
if err := global.DB.Where("app_id = ?", app.ID).Find(&appDetails).Error; err != nil {
|
if err := global.DB.Where("app_id = ?", app.ID).Find(&appDetails).Error; err != nil {
|
||||||
return itemTag
|
versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"}
|
||||||
|
} else {
|
||||||
|
for _, item := range appDetails {
|
||||||
|
versions = append(versions, "postgres:"+item.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return itemTag
|
return "", err
|
||||||
}
|
}
|
||||||
images, err := client.ImageList(context.Background(), types.ImageListOptions{})
|
images, err := client.ImageList(context.Background(), types.ImageListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return itemTag
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range appDetails {
|
itemTag := ""
|
||||||
|
for _, item := range versions {
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
for _, tag := range image.RepoTags {
|
for _, tag := range image.RepoTags {
|
||||||
if tag == "postgres:"+item.Version {
|
if tag == item {
|
||||||
return tag
|
itemTag = tag
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(itemTag) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(itemTag) != 0 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return itemTag
|
if len(itemTag) != 0 {
|
||||||
|
return itemTag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTag = "postgres:16.1-alpine"
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := client.ImagePull(ctx, itemTag, types.ImagePullOptions{}); err != nil {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return itemTag, buserr.New(constant.ErrPgImagePull)
|
||||||
|
}
|
||||||
|
global.LOG.Errorf("image %s pull failed, err: %v", itemTag, err)
|
||||||
|
return itemTag, fmt.Errorf("image %s pull failed, err: %v", itemTag, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemTag, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ const message = {
|
||||||
commonName: 'Support English, Chinese, numbers, .-, and _ length 1-128',
|
commonName: 'Support English, Chinese, numbers, .-, and _ length 1-128',
|
||||||
userName: 'Support English, Chinese, numbers and _ length 3-30',
|
userName: 'Support English, Chinese, numbers and _ length 3-30',
|
||||||
simpleName: 'Supports non-underscore starting, English, numbers, _, length 1-30',
|
simpleName: 'Supports non-underscore starting, English, numbers, _, length 1-30',
|
||||||
dbName: 'Support English, Chinese, numbers, .-, and _ length 1-64',
|
dbName: 'Supports non-special character starting, including English, Chinese, numbers, .-_, with a length of 1-64',
|
||||||
imageName: 'Support English, numbers, :/.-_, length 1-150',
|
imageName: 'Support English, numbers, :/.-_, length 1-150',
|
||||||
volumeName: 'Support English, numbers, .-_, length 2-30',
|
volumeName: 'Support English, numbers, .-_, length 2-30',
|
||||||
complexityPassword:
|
complexityPassword:
|
||||||
|
|
|
@ -160,7 +160,7 @@ const message = {
|
||||||
commonName: '支持英文、中文、數字、.-和_,長度1-128',
|
commonName: '支持英文、中文、數字、.-和_,長度1-128',
|
||||||
userName: '支持英文、中文、數字和_,長度3-30',
|
userName: '支持英文、中文、數字和_,長度3-30',
|
||||||
simpleName: '支持非底線開頭,英文、數字、_,長度1-30',
|
simpleName: '支持非底線開頭,英文、數字、_,長度1-30',
|
||||||
dbName: '支持英文、中文、數字、.-_,長度1-64',
|
dbName: '支持非特殊字符開頭,英文、中文、數字、.-_,長度1-64',
|
||||||
imageName: '支持英文、數字、:/.-_,長度1-150',
|
imageName: '支持英文、數字、:/.-_,長度1-150',
|
||||||
volumeName: '支持英文、數字、.-和_,長度2-30',
|
volumeName: '支持英文、數字、.-和_,長度2-30',
|
||||||
complexityPassword: '請輸入長度為 8-30 位,並包含字母、數字、至少兩種特殊字符的密碼組合',
|
complexityPassword: '請輸入長度為 8-30 位,並包含字母、數字、至少兩種特殊字符的密碼組合',
|
||||||
|
|
|
@ -160,7 +160,7 @@ const message = {
|
||||||
commonName: '支持英文、中文、数字、.-和_,长度1-128',
|
commonName: '支持英文、中文、数字、.-和_,长度1-128',
|
||||||
userName: '支持英文、中文、数字和_,长度3-30',
|
userName: '支持英文、中文、数字和_,长度3-30',
|
||||||
simpleName: '支持非下划线开头,英文、数字、_,长度3-30',
|
simpleName: '支持非下划线开头,英文、数字、_,长度3-30',
|
||||||
dbName: '支持英文、中文、数字、.-_,长度1-64',
|
dbName: '支持非特殊字符开头,英文、中文、数字、.-_,长度1-64',
|
||||||
imageName: '支持英文、数字、:/.-_,长度1-150',
|
imageName: '支持英文、数字、:/.-_,长度1-150',
|
||||||
volumeName: '支持英文、数字、.-和_,长度2-30',
|
volumeName: '支持英文、数字、.-和_,长度2-30',
|
||||||
complexityPassword: '请输入长度为 8-30 位且包含字母、数字、特殊字符至少两项的密码组合',
|
complexityPassword: '请输入长度为 8-30 位且包含字母、数字、特殊字符至少两项的密码组合',
|
||||||
|
|
|
@ -507,6 +507,9 @@ const onChangePassword = async (row: Database.PostgresqlDBInfo) => {
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
label: i18n.global.t('database.changePassword'),
|
label: i18n.global.t('database.changePassword'),
|
||||||
|
disabled: (row: Database.PostgresqlDBInfo) => {
|
||||||
|
return !row.username;
|
||||||
|
},
|
||||||
click: (row: Database.PostgresqlDBInfo) => {
|
click: (row: Database.PostgresqlDBInfo) => {
|
||||||
onChangePassword(row);
|
onChangePassword(row);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
<DrawerHeader :header="title" :resource="changeForm.postgresqlName" :back="handleClose" />
|
<DrawerHeader :header="title" :resource="changeForm.postgresqlName" :back="handleClose" />
|
||||||
</template>
|
</template>
|
||||||
<el-form v-loading="loading" ref="changeFormRef" :model="changeForm" label-position="top">
|
<el-form v-loading="loading" ref="changeFormRef" :rules="rules" :model="changeForm" label-position="top">
|
||||||
<el-row type="flex" justify="center">
|
<el-row type="flex" justify="center">
|
||||||
<el-col :span="22">
|
<el-col :span="22">
|
||||||
<div v-if="changeForm.operation === 'password'">
|
<div v-if="changeForm.operation === 'password'">
|
||||||
<el-form-item :label="$t('commons.login.username')" prop="userName">
|
<el-form-item :label="$t('commons.login.username')" prop="username">
|
||||||
<el-input disabled v-model="changeForm.userName"></el-input>
|
<el-input disabled v-model="changeForm.username"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
|
@ -45,6 +45,7 @@ import { ElForm } from 'element-plus';
|
||||||
import { deleteCheckPostgresqlDB, updatePostgresqlPassword } from '@/api/modules/database';
|
import { deleteCheckPostgresqlDB, updatePostgresqlPassword } from '@/api/modules/database';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
|
||||||
const loading = ref();
|
const loading = ref();
|
||||||
const changeVisible = ref(false);
|
const changeVisible = ref(false);
|
||||||
|
@ -57,12 +58,15 @@ const changeForm = reactive({
|
||||||
type: '',
|
type: '',
|
||||||
database: '',
|
database: '',
|
||||||
postgresqlName: '',
|
postgresqlName: '',
|
||||||
userName: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
operation: '',
|
operation: '',
|
||||||
value: '',
|
value: '',
|
||||||
});
|
});
|
||||||
const confirmDialogRef = ref();
|
const confirmDialogRef = ref();
|
||||||
|
const rules = reactive({
|
||||||
|
password: [Rules.paramComplexity],
|
||||||
|
});
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -79,13 +83,12 @@ interface DialogProps {
|
||||||
}
|
}
|
||||||
const acceptParams = (params: DialogProps): void => {
|
const acceptParams = (params: DialogProps): void => {
|
||||||
title.value = i18n.global.t('database.changePassword');
|
title.value = i18n.global.t('database.changePassword');
|
||||||
|
|
||||||
changeForm.id = params.id;
|
changeForm.id = params.id;
|
||||||
changeForm.from = params.from;
|
changeForm.from = params.from;
|
||||||
changeForm.type = params.type;
|
changeForm.type = params.type;
|
||||||
changeForm.database = params.database;
|
changeForm.database = params.database;
|
||||||
changeForm.postgresqlName = params.postgresqlName;
|
changeForm.postgresqlName = params.postgresqlName;
|
||||||
changeForm.userName = params.username;
|
changeForm.username = params.username;
|
||||||
changeForm.password = params.password;
|
changeForm.password = params.password;
|
||||||
changeForm.operation = params.operation;
|
changeForm.operation = params.operation;
|
||||||
changeForm.value = params.value;
|
changeForm.value = params.value;
|
||||||
|
|
Loading…
Add table
Reference in a new issue