1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 计划任务增加 pg 备份 (#3520)

This commit is contained in:
ssongliu 2024-01-07 22:52:41 +08:00 committed by GitHub
parent 9713b32ba8
commit 6130d4e026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 362 additions and 82 deletions

View File

@ -112,6 +112,27 @@ func (b *BaseApi) ListDatabase(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags Database
// @Summary List databases
// @Description 获取数据库列表
// @Success 200 {array} dto.DatabaseItem
// @Security ApiKeyAuth
// @Router /databases/db/item/:type [get]
func (b *BaseApi) LoadDatabaseItems(c *gin.Context) {
dbType, err := helper.GetStrParamByKey(c, "type")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
list, err := databaseService.LoadItems(dbType)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags Database // @Tags Database
// @Summary Get databases // @Summary Get databases
// @Description 获取远程数据库 // @Description 获取远程数据库
@ -233,4 +254,4 @@ func (b *BaseApi) LoadDatabaseFile(c *gin.Context) {
return return
} }
helper.SuccessWithData(c, content) helper.SuccessWithData(c, content)
} }

View File

@ -17,6 +17,7 @@ type CronjobCreate struct {
AppID string `json:"appID"` AppID string `json:"appID"`
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
DBType string `json:"dbType"`
DBName string `json:"dbName"` DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`
@ -40,6 +41,7 @@ type CronjobUpdate struct {
AppID string `json:"appID"` AppID string `json:"appID"`
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
DBType string `json:"dbType"`
DBName string `json:"dbName"` DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`
@ -84,6 +86,7 @@ type CronjobInfo struct {
AppID string `json:"appID"` AppID string `json:"appID"`
Website string `json:"website"` Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"` ExclusionRules string `json:"exclusionRules"`
DBType string `json:"dbType"`
DBName string `json:"dbName"` DBName string `json:"dbName"`
URL string `json:"url"` URL string `json:"url"`
SourceDir string `json:"sourceDir"` SourceDir string `json:"sourceDir"`

View File

@ -263,6 +263,13 @@ type DatabaseOption struct {
Address string `json:"address"` Address string `json:"address"`
} }
type DatabaseItem struct {
ID uint `json:"id"`
From string `json:"from"`
Database string `json:"database"`
Name string `json:"name"`
}
type DatabaseCreate struct { type DatabaseCreate struct {
Name string `json:"name" validate:"required,max=256"` Name string `json:"name" validate:"required,max=256"`
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`

View File

@ -19,6 +19,7 @@ type Cronjob struct {
Script string `gorm:"longtext" json:"script"` Script string `gorm:"longtext" json:"script"`
Website string `gorm:"type:varchar(64)" json:"website"` Website string `gorm:"type:varchar(64)" json:"website"`
AppID string `gorm:"type:varchar(64)" json:"appID"` AppID string `gorm:"type:varchar(64)" json:"appID"`
DBType string `gorm:"type:varchar(64)" json:"dbType"`
DBName string `gorm:"type:varchar(64)" json:"dbName"` DBName string `gorm:"type:varchar(64)" json:"dbName"`
URL string `gorm:"type:varchar(256)" json:"url"` URL string `gorm:"type:varchar(256)" json:"url"`
SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"` SourceDir string `gorm:"type:varchar(256)" json:"sourceDir"`

View File

@ -794,7 +794,7 @@ func updateInstallInfoInDB(appKey, appName, param string, isRestart bool, value
envKey := "" envKey := ""
switch param { switch param {
case "password": case "password":
if appKey == "mysql" || appKey == "mariadb" { if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" {
envKey = "PANEL_DB_ROOT_PASSWORD=" envKey = "PANEL_DB_ROOT_PASSWORD="
} else { } else {
envKey = "PANEL_REDIS_ROOT_PASSWORD=" envKey = "PANEL_REDIS_ROOT_PASSWORD="

View File

@ -264,6 +264,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
upMap["app_id"] = req.AppID upMap["app_id"] = req.AppID
upMap["website"] = req.Website upMap["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules upMap["exclusion_rules"] = req.ExclusionRules
upMap["db_type"] = req.DBType
upMap["db_name"] = req.DBName upMap["db_name"] = req.DBName
upMap["url"] = req.URL upMap["url"] = req.URL
upMap["source_dir"] = req.SourceDir upMap["source_dir"] = req.SourceDir

View File

@ -3,8 +3,6 @@ package service
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/i18n"
"os" "os"
"path" "path"
"strconv" "strconv"
@ -12,6 +10,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
@ -273,19 +274,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
return paths, err return paths, err
} }
var dbs []model.DatabaseMysql dbs := loadDbsForJob(cronjob)
if cronjob.DBName == "all" {
dbs, err = mysqlRepo.List()
if err != nil {
return paths, err
}
} else {
itemID, _ := (strconv.Atoi(cronjob.DBName))
dbs, err = mysqlRepo.List(commonRepo.WithByID(uint(itemID)))
if err != nil {
return paths, err
}
}
var client cloud_storage.CloudStorageClient var client cloud_storage.CloudStorageClient
if backup.Type != "LOCAL" { if backup.Type != "LOCAL" {
@ -297,17 +286,21 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
for _, dbInfo := range dbs { for _, dbInfo := range dbs {
var record model.BackupRecord var record model.BackupRecord
record.Type = dbInfo.DBType
database, _ := databaseRepo.Get(commonRepo.WithByName(dbInfo.MysqlName))
record.Type = database.Type
record.Source = "LOCAL" record.Source = "LOCAL"
record.BackupType = backup.Type record.BackupType = backup.Type
record.Name = dbInfo.MysqlName record.Name = dbInfo.Database
backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", database.Type, record.Name, dbInfo.Name)) backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format("20060102150405")) record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format("20060102150405"))
if err = handleMysqlBackup(dbInfo.MysqlName, dbInfo.Name, backupDir, record.FileName); err != nil { if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
return paths, err if err = handleMysqlBackup(dbInfo.Database, dbInfo.Name, backupDir, record.FileName); err != nil {
return paths, err
}
} else {
if err = handlePostgresqlBackup(dbInfo.Database, dbInfo.Name, backupDir, record.FileName); err != nil {
return paths, err
}
} }
record.DetailName = dbInfo.Name record.DetailName = dbInfo.Name
@ -717,3 +710,52 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
func hasBackup(cronjobType string) bool { func hasBackup(cronjobType string) bool {
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log"
} }
type databaseHelper struct {
DBType string
Database string
Name string
}
func loadDbsForJob(cronjob model.Cronjob) []databaseHelper {
var dbs []databaseHelper
if cronjob.DBName == "all" {
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
mysqlItems, _ := mysqlRepo.List()
for _, mysql := range mysqlItems {
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: mysql.MysqlName,
Name: mysql.Name,
})
}
} else {
pgItems, _ := postgresqlRepo.List()
for _, pg := range pgItems {
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: pg.PostgresqlName,
Name: pg.Name,
})
}
}
return dbs
}
itemID, _ := (strconv.Atoi(cronjob.DBName))
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
mysqlItem, _ := mysqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: mysqlItem.MysqlName,
Name: mysqlItem.Name,
})
} else {
pgItem, _ := postgresqlRepo.Get(commonRepo.WithByID(uint(itemID)))
dbs = append(dbs, databaseHelper{
DBType: cronjob.DBType,
Database: pgItem.PostgresqlName,
Name: pgItem.Name,
})
}
return dbs
}

View File

@ -31,6 +31,7 @@ type IDatabaseService interface {
DeleteCheck(id uint) ([]string, error) DeleteCheck(id uint) ([]string, error)
Delete(req dto.DatabaseDelete) error Delete(req dto.DatabaseDelete) error
List(dbType string) ([]dto.DatabaseOption, error) List(dbType string) ([]dto.DatabaseOption, error)
LoadItems(dbType string) ([]dto.DatabaseItem, error)
} }
func NewIDatabaseService() IDatabaseService { func NewIDatabaseService() IDatabaseService {
@ -80,6 +81,35 @@ func (u *DatabaseService) List(dbType string) ([]dto.DatabaseOption, error) {
return datas, err return datas, err
} }
func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType))
var datas []dto.DatabaseItem
for _, db := range dbs {
if dbType == "postgresql" {
items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name))
for _, item := range items {
var dItem dto.DatabaseItem
if err := copier.Copy(&dItem, &item); err != nil {
continue
}
dItem.Database = db.Name
datas = append(datas, dItem)
}
} else {
items, _ := mysqlRepo.List(mysqlRepo.WithByMysqlName(db.Name))
for _, item := range items {
var dItem dto.DatabaseItem
if err := copier.Copy(&dItem, &item); err != nil {
continue
}
dItem.Database = db.Name
datas = append(datas, dItem)
}
}
}
return datas, err
}
func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool { func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
switch req.Type { switch req.Type {
case constant.AppPostgresql: case constant.AppPostgresql:

View File

@ -335,7 +335,7 @@ func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error {
} }
global.LOG.Infof("start to update postgresql password used by app %s-%s", appModel.Key, appInstall.Name) global.LOG.Infof("start to update postgresql password used by app %s-%s", appModel.Key, appInstall.Name)
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "password", true, req.Value); err != nil { if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", true, req.Value); err != nil {
return err return err
} }
} }

View File

@ -19,7 +19,6 @@ func Init() {
migrations.AddTableImageRepo, migrations.AddTableImageRepo,
migrations.AddTableWebsite, migrations.AddTableWebsite,
migrations.AddTableDatabaseMysql, migrations.AddTableDatabaseMysql,
migrations.AddTableDatabasePostgresql,
migrations.AddTableSnap, migrations.AddTableSnap,
migrations.AddDefaultGroup, migrations.AddDefaultGroup,
migrations.AddTableRuntime, migrations.AddTableRuntime,
@ -64,6 +63,7 @@ func Init() {
migrations.UpdateWebsiteBackupRecord, migrations.UpdateWebsiteBackupRecord,
migrations.AddTablePHPExtensions, migrations.AddTablePHPExtensions,
migrations.AddTableDatabasePostgresql,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -214,12 +214,6 @@ var AddTableDatabaseMysql = &gormigrate.Migration{
return tx.AutoMigrate(&model.DatabaseMysql{}) return tx.AutoMigrate(&model.DatabaseMysql{})
}, },
} }
var AddTableDatabasePostgresql = &gormigrate.Migration{
ID: "20231224-add-table-database_postgresql",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.DatabasePostgresql{})
},
}
var AddTableWebsite = &gormigrate.Migration{ var AddTableWebsite = &gormigrate.Migration{
ID: "20201009-add-table-website", ID: "20201009-add-table-website",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {

View File

@ -116,3 +116,33 @@ var AddTablePHPExtensions = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddTableDatabasePostgresql = &gormigrate.Migration{
ID: "20231225-add-table-database_postgresql",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.DatabasePostgresql{}); err != nil {
return err
}
if err := tx.AutoMigrate(&model.Cronjob{}); err != nil {
return err
}
var jobs []model.Cronjob
if err := tx.Where("type == ?", "database").Find(&jobs).Error; err != nil {
return err
}
for _, job := range jobs {
var db model.DatabaseMysql
if err := tx.Where("id == ?", job.DBName).First(&db).Error; err != nil {
return err
}
var database model.Database
if err := tx.Where("name == ?", db.MysqlName).First(&database).Error; err != nil {
return err
}
if err := tx.Model(&model.Cronjob{}).Where("id = ?", job.ID).Update("db_type", database.Type).Error; err != nil {
return err
}
}
return nil
},
}

View File

@ -48,6 +48,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/db", baseApi.CreateDatabase) cmdRouter.POST("/db", baseApi.CreateDatabase)
cmdRouter.GET("/db/:name", baseApi.GetDatabase) cmdRouter.GET("/db/:name", baseApi.GetDatabase)
cmdRouter.GET("/db/list/:type", baseApi.ListDatabase) cmdRouter.GET("/db/list/:type", baseApi.ListDatabase)
cmdRouter.GET("/db/item/:type", baseApi.LoadDatabaseItems)
cmdRouter.POST("/db/update", baseApi.UpdateDatabase) cmdRouter.POST("/db/update", baseApi.UpdateDatabase)
cmdRouter.POST("/db/search", baseApi.SearchDatabase) cmdRouter.POST("/db/search", baseApi.SearchDatabase)
cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase) cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase)

View File

@ -15,7 +15,6 @@ import (
type PostgresqlClient interface { type PostgresqlClient interface {
Create(info client.CreateInfo) error Create(info client.CreateInfo) error
Delete(info client.DeleteInfo) error Delete(info client.DeleteInfo) error
ReloadConf() error
ChangePassword(info client.PasswordChangeInfo) error ChangePassword(info client.PasswordChangeInfo) error
Backup(info client.BackupInfo) error Backup(info client.BackupInfo) error
@ -26,7 +25,7 @@ type PostgresqlClient interface {
func NewPostgresqlClient(conn client.DBInfo) (PostgresqlClient, error) { func NewPostgresqlClient(conn client.DBInfo) (PostgresqlClient, error) {
if conn.From == "local" { if conn.From == "local" {
connArgs := []string{"exec", conn.Address, "psql", "-U", conn.Username, "-c"} connArgs := []string{"exec", conn.Address, "psql", "-t", "-U", conn.Username, "-c"}
return client.NewLocal(connArgs, conn.Address, conn.Username, conn.Password, conn.Database), nil return client.NewLocal(connArgs, conn.Address, conn.Username, conn.Password, conn.Database), nil
} }

View File

@ -30,7 +30,7 @@ func NewLocal(command []string, containerName, username, password, database stri
} }
func (r *Local) Create(info CreateInfo) error { func (r *Local) Create(info CreateInfo) error {
createSql := fmt.Sprintf("CREATE DATABASE %s", info.Name) createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name)
if err := r.ExecSQL(createSql, info.Timeout); err != nil { if err := r.ExecSQL(createSql, info.Timeout); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "already exists") { if strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrDatabaseIsExist) return buserr.New(constant.ErrDatabaseIsExist)
@ -39,7 +39,7 @@ func (r *Local) Create(info CreateInfo) error {
} }
if err := r.CreateUser(info, true); err != nil { if err := r.CreateUser(info, true); err != nil {
_ = r.ExecSQL(fmt.Sprintf("DROP DATABASE %s", info.Name), info.Timeout) _ = r.ExecSQL(fmt.Sprintf("DROP DATABASE \"%s\"", info.Name), info.Timeout)
return err return err
} }
@ -47,7 +47,7 @@ func (r *Local) Create(info CreateInfo) error {
} }
func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error { func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Username) createSql := fmt.Sprintf("CREATE USER \"%s\" WITH PASSWORD '%s'", info.Username, info.Password)
if err := r.ExecSQL(createSql, info.Timeout); err != nil { if err := r.ExecSQL(createSql, info.Timeout); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "already exists") { if strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrUserIsExist) return buserr.New(constant.ErrUserIsExist)
@ -61,7 +61,7 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
} }
return err return err
} }
grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", info.Name, info.Username) grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username)
if err := r.ExecSQL(grantStr, info.Timeout); err != nil { if err := r.ExecSQL(grantStr, info.Timeout); err != nil {
if withDeleteDB { if withDeleteDB {
_ = r.Delete(DeleteInfo{ _ = r.Delete(DeleteInfo{
@ -77,12 +77,12 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
func (r *Local) Delete(info DeleteInfo) error { func (r *Local) Delete(info DeleteInfo) error {
if len(info.Name) != 0 { if len(info.Name) != 0 {
dropSql := fmt.Sprintf("DROP DATABASE %s", info.Name) dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
return err return err
} }
} }
dropSql := fmt.Sprintf("DROP ROLE %s", info.Username) dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username)
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
if strings.Contains(strings.ToLower(err.Error()), "depend on it") { if strings.Contains(strings.ToLower(err.Error()), "depend on it") {
return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) return buserr.WithDetail(constant.ErrInUsed, info.Username, nil)
@ -147,10 +147,6 @@ func (r *Local) Recover(info RecoverInfo) error {
return nil return nil
} }
func (r *Local) ReloadConf() error {
return nil
}
func (r *Local) SyncDB() ([]SyncDBInfo, error) { func (r *Local) SyncDB() ([]SyncDBInfo, error) {
var datas []SyncDBInfo var datas []SyncDBInfo
lines, err := r.ExecSQLForRows("SELECT datname FROM pg_database", 300) lines, err := r.ExecSQLForRows("SELECT datname FROM pg_database", 300)
@ -158,10 +154,11 @@ func (r *Local) SyncDB() ([]SyncDBInfo, error) {
return datas, err return datas, err
} }
for _, line := range lines { for _, line := range lines {
if line == "postgres" || line == "template1" || line == "template0" || line == r.Username { itemLine := strings.TrimLeft(line, " ")
if len(itemLine) == 0 || itemLine == "postgres" || itemLine == "template1" || itemLine == "template0" || itemLine == r.Username {
continue continue
} }
datas = append(datas, SyncDBInfo{Name: line, From: "local", PostgresqlName: r.Database}) datas = append(datas, SyncDBInfo{Name: itemLine, From: "local", PostgresqlName: r.Database})
} }
return datas, nil return datas, nil
} }

View File

@ -34,7 +34,7 @@ func NewRemote(db Remote) *Remote {
return &db return &db
} }
func (r *Remote) Create(info CreateInfo) error { func (r *Remote) Create(info CreateInfo) error {
createSql := fmt.Sprintf("CREATE DATABASE %s", info.Name) createSql := fmt.Sprintf("CREATE DATABASE \"%s\"", info.Name)
if err := r.ExecSQL(createSql, info.Timeout); err != nil { if err := r.ExecSQL(createSql, info.Timeout); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "already exists") { if strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrDatabaseIsExist) return buserr.New(constant.ErrDatabaseIsExist)
@ -62,7 +62,7 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
} }
return err return err
} }
grantSql := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", info.Name, info.Username) grantSql := fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE \"%s\" TO \"%s\"", info.Name, info.Username)
if err := r.ExecSQL(grantSql, info.Timeout); err != nil { if err := r.ExecSQL(grantSql, info.Timeout); err != nil {
if withDeleteDB { if withDeleteDB {
_ = r.Delete(DeleteInfo{ _ = r.Delete(DeleteInfo{
@ -79,12 +79,12 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
func (r *Remote) Delete(info DeleteInfo) error { func (r *Remote) Delete(info DeleteInfo) error {
if len(info.Name) != 0 { if len(info.Name) != 0 {
dropSql := fmt.Sprintf("DROP DATABASE %s", info.Name) dropSql := fmt.Sprintf("DROP DATABASE \"%s\"", info.Name)
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
return err return err
} }
} }
dropSql := fmt.Sprintf("DROP ROLE %s", info.Username) dropSql := fmt.Sprintf("DROP USER \"%s\"", info.Username)
if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete { if err := r.ExecSQL(dropSql, info.Timeout); err != nil && !info.ForceDelete {
if strings.Contains(strings.ToLower(err.Error()), "depend on it") { if strings.Contains(strings.ToLower(err.Error()), "depend on it") {
return buserr.WithDetail(constant.ErrInUsed, info.Username, nil) return buserr.WithDetail(constant.ErrInUsed, info.Username, nil)
@ -97,9 +97,6 @@ func (r *Remote) Delete(info DeleteInfo) error {
func (r *Remote) ChangePassword(info PasswordChangeInfo) error { func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH ENCRYPTED PASSWORD '%s'", info.Username, info.Password), info.Timeout) return r.ExecSQL(fmt.Sprintf("ALTER USER \"%s\" WITH ENCRYPTED PASSWORD '%s'", info.Username, info.Password), info.Timeout)
} }
func (r *Remote) ReloadConf() error {
return r.ExecSQL("SELECT pg_reload_conf()", 5)
}
func (r *Remote) Backup(info BackupInfo) error { func (r *Remote) Backup(info BackupInfo) error {
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
@ -150,7 +147,7 @@ func (r *Remote) Recover(info RecoverInfo) error {
}() }()
} }
recoverCommand := exec.Command("bash", "-c", recoverCommand := exec.Command("bash", "-c",
fmt.Sprintf("docker run --rm --net=host -i postgres:alpine /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 postgres:16.1-alpine /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",
r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName)) r.Password, r.Address, r.Port, r.User, info.Name, info.Username, fileName))
pipe, _ := recoverCommand.StdoutPipe() pipe, _ := recoverCommand.StdoutPipe()
stderrPipe, _ := recoverCommand.StderrPipe() stderrPipe, _ := recoverCommand.StderrPipe()
@ -191,7 +188,7 @@ func (r *Remote) SyncDB() ([]SyncDBInfo, error) {
if err := rows.Scan(&dbName); err != nil { if err := rows.Scan(&dbName); err != nil {
continue continue
} }
if dbName == "postgres" || dbName == "template1" || dbName == "template0" || dbName == r.User { if len(dbName) == 0 || dbName == "postgres" || dbName == "template1" || dbName == "template0" || dbName == r.User {
continue continue
} }
datas = append(datas, SyncDBInfo{Name: dbName, From: r.From, PostgresqlName: r.Database}) datas = append(datas, SyncDBInfo{Name: dbName, From: r.From, PostgresqlName: r.Database})

View File

@ -1,5 +1,5 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT // Code generated by swaggo/swag. DO NOT EDIT.
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@ -4296,6 +4296,31 @@ const docTemplate = `{
} }
} }
}, },
"/databases/db/item/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取数据库列表",
"tags": [
"Database"
],
"summary": "List databases",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.DatabaseItem"
}
}
}
}
}
},
"/databases/db/list/:type": { "/databases/db/list/:type": {
"get": { "get": {
"security": [ "security": [
@ -14889,6 +14914,9 @@ const docTemplate = `{
"dbName": { "dbName": {
"type": "string" "type": "string"
}, },
"dbType": {
"type": "string"
},
"exclusionRules": { "exclusionRules": {
"type": "string" "type": "string"
}, },
@ -14974,6 +15002,9 @@ const docTemplate = `{
"dbName": { "dbName": {
"type": "string" "type": "string"
}, },
"dbType": {
"type": "string"
},
"exclusionRules": { "exclusionRules": {
"type": "string" "type": "string"
}, },
@ -15393,6 +15424,23 @@ const docTemplate = `{
} }
} }
}, },
"dto.DatabaseItem": {
"type": "object",
"properties": {
"database": {
"type": "string"
},
"from": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"dto.DatabaseOption": { "dto.DatabaseOption": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -4289,6 +4289,31 @@
} }
} }
}, },
"/databases/db/item/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取数据库列表",
"tags": [
"Database"
],
"summary": "List databases",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.DatabaseItem"
}
}
}
}
}
},
"/databases/db/list/:type": { "/databases/db/list/:type": {
"get": { "get": {
"security": [ "security": [
@ -14882,6 +14907,9 @@
"dbName": { "dbName": {
"type": "string" "type": "string"
}, },
"dbType": {
"type": "string"
},
"exclusionRules": { "exclusionRules": {
"type": "string" "type": "string"
}, },
@ -14967,6 +14995,9 @@
"dbName": { "dbName": {
"type": "string" "type": "string"
}, },
"dbType": {
"type": "string"
},
"exclusionRules": { "exclusionRules": {
"type": "string" "type": "string"
}, },
@ -15386,6 +15417,23 @@
} }
} }
}, },
"dto.DatabaseItem": {
"type": "object",
"properties": {
"database": {
"type": "string"
},
"from": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"dto.DatabaseOption": { "dto.DatabaseOption": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -586,6 +586,8 @@ definitions:
type: integer type: integer
dbName: dbName:
type: string type: string
dbType:
type: string
exclusionRules: exclusionRules:
type: string type: string
hour: hour:
@ -644,6 +646,8 @@ definitions:
type: integer type: integer
dbName: dbName:
type: string type: string
dbType:
type: string
exclusionRules: exclusionRules:
type: string type: string
hour: hour:
@ -928,6 +932,17 @@ definitions:
version: version:
type: string type: string
type: object type: object
dto.DatabaseItem:
properties:
database:
type: string
from:
type: string
id:
type: integer
name:
type: string
type: object
dto.DatabaseOption: dto.DatabaseOption:
properties: properties:
address: address:
@ -7695,6 +7710,21 @@ paths:
formatEN: delete database [names] formatEN: delete database [names]
formatZH: 删除远程数据库 [names] formatZH: 删除远程数据库 [names]
paramKeys: [] paramKeys: []
/databases/db/item/:type:
get:
description: 获取数据库列表
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.DatabaseItem'
type: array
security:
- ApiKeyAuth: []
summary: List databases
tags:
- Database
/databases/db/list/:type: /databases/db/list/:type:
get: get:
description: 获取远程数据库列表 description: 获取远程数据库列表

View File

@ -18,6 +18,7 @@ export namespace Cronjob {
appID: string; appID: string;
website: string; website: string;
exclusionRules: string; exclusionRules: string;
dbType: string;
dbName: string; dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;
@ -40,6 +41,7 @@ export namespace Cronjob {
script: string; script: string;
website: string; website: string;
exclusionRules: string; exclusionRules: string;
dbType: string;
dbName: string; dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;
@ -59,6 +61,7 @@ export namespace Cronjob {
script: string; script: string;
website: string; website: string;
exclusionRules: string; exclusionRules: string;
dbType: string;
dbName: string; dbName: string;
url: string; url: string;
sourceDir: string; sourceDir: string;

View File

@ -141,12 +141,10 @@ export namespace Database {
File: string; File: string;
Position: number; Position: number;
} }
export interface MysqlOption { export interface PgLoadDB {
id: number;
from: string; from: string;
type: string; type: string;
database: string; database: string;
name: string;
} }
export interface PgLoadDB { export interface PgLoadDB {
from: string; from: string;
@ -316,6 +314,12 @@ export namespace Database {
version: string; version: string;
address: string; address: string;
} }
export interface DbItem {
id: number;
from: string;
database: string;
name: string;
}
export interface DatabaseCreate { export interface DatabaseCreate {
name: string; name: string;
version: string; version: string;

View File

@ -101,9 +101,6 @@ export const loadMysqlStatus = (type: string, database: string) => {
export const loadRemoteAccess = (type: string, database: string) => { export const loadRemoteAccess = (type: string, database: string) => {
return http.post<boolean>(`/databases/remote`, { type: type, name: database }); return http.post<boolean>(`/databases/remote`, { type: type, name: database });
}; };
export const loadDBOptions = () => {
return http.get<Array<Database.MysqlOption>>(`/databases/options`);
};
// redis // redis
export const loadRedisStatus = () => { export const loadRedisStatus = () => {
@ -141,6 +138,9 @@ export const searchDatabases = (params: Database.SearchDatabasePage) => {
export const listDatabases = (type: string) => { export const listDatabases = (type: string) => {
return http.get<Array<Database.DatabaseOption>>(`/databases/db/list/${type}`); return http.get<Array<Database.DatabaseOption>>(`/databases/db/list/${type}`);
}; };
export const listDbItems = (type: string) => {
return http.get<Array<Database.DbItem>>(`/databases/db/item/${type}`);
};
export const checkDatabase = (params: Database.DatabaseCreate) => { export const checkDatabase = (params: Database.DatabaseCreate) => {
let request = deepCopy(params) as Database.DatabaseCreate; let request = deepCopy(params) as Database.DatabaseCreate;
if (request.ssl) { if (request.ssl) {

View File

@ -136,7 +136,11 @@
prop="website" prop="website"
> >
<el-select class="selectClass" v-model="dialogData.rowData!.website"> <el-select class="selectClass" v-model="dialogData.rowData!.website">
<el-option :label="$t('commons.table.all')" value="all" /> <el-option
:disabled="websiteOptions.length === 0"
:label="$t('commons.table.all')"
value="all"
/>
<el-option v-for="item in websiteOptions" :key="item" :value="item" :label="item" /> <el-option v-for="item in websiteOptions" :key="item" :value="item" :label="item" />
</el-select> </el-select>
<span class="input-help" v-if="dialogData.rowData!.type === 'cutWebsiteLog'"> <span class="input-help" v-if="dialogData.rowData!.type === 'cutWebsiteLog'">
@ -147,7 +151,11 @@
<div v-if="dialogData.rowData!.type === 'app'"> <div v-if="dialogData.rowData!.type === 'app'">
<el-form-item :label="$t('cronjob.app')" prop="appID"> <el-form-item :label="$t('cronjob.app')" prop="appID">
<el-select class="selectClass" clearable v-model="dialogData.rowData!.appID"> <el-select class="selectClass" clearable v-model="dialogData.rowData!.appID">
<el-option :label="$t('commons.table.all')" value="all" /> <el-option
:disabled="appOptions.length === 0"
:label="$t('commons.table.all')"
value="all"
/>
<div v-for="item in appOptions" :key="item.id"> <div v-for="item in appOptions" :key="item.id">
<el-option :value="item.id + ''" :label="item.name"> <el-option :value="item.id + ''" :label="item.name">
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
@ -161,11 +169,22 @@
</div> </div>
<div v-if="dialogData.rowData!.type === 'database'"> <div v-if="dialogData.rowData!.type === 'database'">
<el-form-item :label="$t('cronjob.database')">
<el-radio-group v-model="dialogData.rowData!.dbType" @change="loadDatabases">
<el-radio label="mysql">MySQL</el-radio>
<el-radio label="mariadb">Mariadb</el-radio>
<el-radio label="postgresql">PostgreSQL</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('cronjob.database')" prop="dbName"> <el-form-item :label="$t('cronjob.database')" prop="dbName">
<el-select class="selectClass" clearable v-model="dialogData.rowData!.dbName"> <el-select class="selectClass" clearable v-model="dialogData.rowData!.dbName">
<el-option :label="$t('commons.table.all')" value="all" />
<el-option <el-option
v-for="item in mysqlInfo.dbs" :disabled="dbInfo.dbs.length === 0"
:label="$t('commons.table.all')"
value="all"
/>
<el-option
v-for="item in dbInfo.dbs"
:key="item.id" :key="item.id"
:value="item.id + ''" :value="item.id + ''"
:label="item.name" :label="item.name"
@ -174,9 +193,6 @@
<el-tag class="tagClass"> <el-tag class="tagClass">
{{ item.from === 'local' ? $t('database.local') : $t('database.remote') }} {{ item.from === 'local' ? $t('database.local') : $t('database.remote') }}
</el-tag> </el-tag>
<el-tag class="tagClass">
{{ item.type === 'mysql' ? 'MySQL' : 'MariaDB' }}
</el-tag>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -277,7 +293,7 @@ import i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import { Cronjob } from '@/api/interface/cronjob'; import { Cronjob } from '@/api/interface/cronjob';
import { addCronjob, editCronjob } from '@/api/modules/cronjob'; import { addCronjob, editCronjob } from '@/api/modules/cronjob';
import { loadDBOptions } from '@/api/modules/database'; import { listDbItems } from '@/api/modules/database';
import { GetWebsiteOptions } from '@/api/modules/website'; import { GetWebsiteOptions } from '@/api/modules/website';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
@ -297,10 +313,12 @@ const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({ const dialogData = ref<DialogProps>({
title: '', title: '',
}); });
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
dialogData.value = params; dialogData.value = params;
if (dialogData.value.title === 'create') { if (dialogData.value.title === 'create') {
changeType(); changeType();
dialogData.value.rowData.dbType = 'mysql';
} }
title.value = i18n.global.t('cronjob.' + dialogData.value.title); title.value = i18n.global.t('cronjob.' + dialogData.value.title);
if (dialogData.value?.rowData?.exclusionRules) { if (dialogData.value?.rowData?.exclusionRules) {
@ -310,11 +328,11 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value.rowData.inContainer = true; dialogData.value.rowData.inContainer = true;
} }
drawerVisible.value = true; drawerVisible.value = true;
checkMysqlInstalled();
loadBackups(); loadBackups();
loadAppInstalls(); loadAppInstalls();
loadWebsites(); loadWebsites();
loadContainers(); loadContainers();
loadDatabases();
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
@ -333,11 +351,11 @@ const websiteOptions = ref();
const backupOptions = ref(); const backupOptions = ref();
const appOptions = ref(); const appOptions = ref();
const mysqlInfo = reactive({ const dbInfo = reactive({
isExist: false, isExist: false,
name: '', name: '',
version: '', version: '',
dbs: [] as Array<Database.MysqlOption>, dbs: [] as Array<Database.DbItem>,
}); });
const verifySpec = (rule: any, value: any, callback: any) => { const verifySpec = (rule: any, value: any, callback: any) => {
@ -459,6 +477,11 @@ const hasHour = () => {
); );
}; };
const loadDatabases = async () => {
const data = await listDbItems(dialogData.value.rowData.dbType);
dbInfo.dbs = data.data || [];
};
const changeType = () => { const changeType = () => {
switch (dialogData.value.rowData!.type) { switch (dialogData.value.rowData!.type) {
case 'shell': case 'shell':
@ -545,11 +568,6 @@ const loadContainers = async () => {
containerOptions.value = res.data || []; containerOptions.value = res.data || [];
}; };
const checkMysqlInstalled = async () => {
const data = await loadDBOptions();
mysqlInfo.dbs = data.data || [];
};
function isBackup() { function isBackup() {
return ( return (
dialogData.value.rowData!.type === 'app' || dialogData.value.rowData!.type === 'app' ||

View File

@ -406,7 +406,7 @@ import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { MsgError, MsgInfo, MsgSuccess } from '@/utils/message'; import { MsgError, MsgInfo, MsgSuccess } from '@/utils/message';
import { loadDBOptions } from '@/api/modules/database'; import { listDbItems } from '@/api/modules/database';
import { ListAppInstalled } from '@/api/modules/app'; import { ListAppInstalled } from '@/api/modules/app';
const loading = ref(); const loading = ref();
@ -440,7 +440,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
recordShow.value = true; recordShow.value = true;
dialogData.value = params; dialogData.value = params;
if (dialogData.value.rowData.type === 'database') { if (dialogData.value.rowData.type === 'database') {
const data = await loadDBOptions(); const data = await listDbItems('mysql,mariadb,postgresql');
let itemDBs = data.data || []; let itemDBs = data.data || [];
for (const item of itemDBs) { for (const item of itemDBs) {
if (item.id == dialogData.value.rowData.dbName) { if (item.id == dialogData.value.rowData.dbName) {

View File

@ -8,7 +8,7 @@
<el-col :span="22"> <el-col :span="22">
<el-form-item :label="$t('database.containerConn')"> <el-form-item :label="$t('database.containerConn')">
<el-tag> <el-tag>
{{ form.serviceName + form.port }} {{ form.serviceName + ':' + form.port }}
</el-tag> </el-tag>
<CopyButton :content="form.serviceName + ':' + form.port" type="icon" /> <CopyButton :content="form.serviceName + ':' + form.port" type="icon" />
<span class="input-help"> <span class="input-help">

View File

@ -6,6 +6,12 @@
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<el-form ref="deleteForm" v-loading="loading" @submit.prevent> <el-form ref="deleteForm" v-loading="loading" @submit.prevent>
<el-form-item>
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('app.forceDelete')" />
<span class="input-help">
{{ $t('app.forceDeleteHelper') }}
</span>
</el-form-item>
<el-form-item> <el-form-item>
<el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" /> <el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" />
<span class="input-help"> <span class="input-help">