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)
}
// @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
// @Summary Get databases
// @Description 获取远程数据库
@ -233,4 +254,4 @@ func (b *BaseApi) LoadDatabaseFile(c *gin.Context) {
return
}
helper.SuccessWithData(c, content)
}
}

View file

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

View file

@ -263,6 +263,13 @@ type DatabaseOption struct {
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 {
Name string `json:"name" validate:"required,max=256"`
Type string `json:"type" validate:"required"`

View file

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

View file

@ -794,7 +794,7 @@ func updateInstallInfoInDB(appKey, appName, param string, isRestart bool, value
envKey := ""
switch param {
case "password":
if appKey == "mysql" || appKey == "mariadb" {
if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" {
envKey = "PANEL_DB_ROOT_PASSWORD="
} else {
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["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules
upMap["db_type"] = req.DBType
upMap["db_name"] = req.DBName
upMap["url"] = req.URL
upMap["source_dir"] = req.SourceDir

View file

@ -3,8 +3,6 @@ package service
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/i18n"
"os"
"path"
"strconv"
@ -12,6 +10,9 @@ import (
"sync"
"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/model"
"github.com/1Panel-dev/1Panel/backend/constant"
@ -273,19 +274,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
return paths, err
}
var dbs []model.DatabaseMysql
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
}
}
dbs := loadDbsForJob(cronjob)
var client cloud_storage.CloudStorageClient
if backup.Type != "LOCAL" {
@ -297,17 +286,21 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, backup model.Back
for _, dbInfo := range dbs {
var record model.BackupRecord
database, _ := databaseRepo.Get(commonRepo.WithByName(dbInfo.MysqlName))
record.Type = database.Type
record.Type = dbInfo.DBType
record.Source = "LOCAL"
record.BackupType = backup.Type
record.Name = dbInfo.MysqlName
backupDir := path.Join(localDir, fmt.Sprintf("database/%s/%s/%s", database.Type, record.Name, dbInfo.Name))
record.Name = dbInfo.Database
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"))
if err = handleMysqlBackup(dbInfo.MysqlName, dbInfo.Name, backupDir, record.FileName); err != nil {
return paths, err
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
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
@ -717,3 +710,52 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
func hasBackup(cronjobType string) bool {
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)
Delete(req dto.DatabaseDelete) error
List(dbType string) ([]dto.DatabaseOption, error)
LoadItems(dbType string) ([]dto.DatabaseItem, error)
}
func NewIDatabaseService() IDatabaseService {
@ -80,6 +81,35 @@ func (u *DatabaseService) List(dbType string) ([]dto.DatabaseOption, error) {
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 {
switch req.Type {
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)
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
}
}

View file

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

View file

@ -214,12 +214,6 @@ var AddTableDatabaseMysql = &gormigrate.Migration{
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{
ID: "20201009-add-table-website",
Migrate: func(tx *gorm.DB) error {

View file

@ -116,3 +116,33 @@ var AddTablePHPExtensions = &gormigrate.Migration{
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.GET("/db/:name", baseApi.GetDatabase)
cmdRouter.GET("/db/list/:type", baseApi.ListDatabase)
cmdRouter.GET("/db/item/:type", baseApi.LoadDatabaseItems)
cmdRouter.POST("/db/update", baseApi.UpdateDatabase)
cmdRouter.POST("/db/search", baseApi.SearchDatabase)
cmdRouter.POST("/db/del/check", baseApi.DeleteCheckDatabase)

View file

@ -15,7 +15,6 @@ import (
type PostgresqlClient interface {
Create(info client.CreateInfo) error
Delete(info client.DeleteInfo) error
ReloadConf() error
ChangePassword(info client.PasswordChangeInfo) error
Backup(info client.BackupInfo) error
@ -26,7 +25,7 @@ type PostgresqlClient interface {
func NewPostgresqlClient(conn client.DBInfo) (PostgresqlClient, error) {
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
}

View file

@ -30,7 +30,7 @@ func NewLocal(command []string, containerName, username, password, database stri
}
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 strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrDatabaseIsExist)
@ -39,7 +39,7 @@ func (r *Local) Create(info CreateInfo) error {
}
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
}
@ -47,7 +47,7 @@ func (r *Local) Create(info CreateInfo) 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 strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrUserIsExist)
@ -61,7 +61,7 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
}
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 withDeleteDB {
_ = r.Delete(DeleteInfo{
@ -77,12 +77,12 @@ func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
func (r *Local) Delete(info DeleteInfo) error {
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 {
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 strings.Contains(strings.ToLower(err.Error()), "depend on it") {
return buserr.WithDetail(constant.ErrInUsed, info.Username, nil)
@ -147,10 +147,6 @@ func (r *Local) Recover(info RecoverInfo) error {
return nil
}
func (r *Local) ReloadConf() error {
return nil
}
func (r *Local) SyncDB() ([]SyncDBInfo, error) {
var datas []SyncDBInfo
lines, err := r.ExecSQLForRows("SELECT datname FROM pg_database", 300)
@ -158,10 +154,11 @@ func (r *Local) SyncDB() ([]SyncDBInfo, error) {
return datas, err
}
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
}
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
}

View file

@ -34,7 +34,7 @@ func NewRemote(db Remote) *Remote {
return &db
}
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 strings.Contains(strings.ToLower(err.Error()), "already exists") {
return buserr.New(constant.ErrDatabaseIsExist)
@ -62,7 +62,7 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
}
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 withDeleteDB {
_ = r.Delete(DeleteInfo{
@ -79,12 +79,12 @@ func (r *Remote) CreateUser(info CreateInfo, withDeleteDB bool) error {
func (r *Remote) Delete(info DeleteInfo) error {
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 {
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 strings.Contains(strings.ToLower(err.Error()), "depend on it") {
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 {
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 {
fileOp := files.NewFileOp()
@ -150,7 +147,7 @@ func (r *Remote) Recover(info RecoverInfo) error {
}()
}
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))
pipe, _ := recoverCommand.StdoutPipe()
stderrPipe, _ := recoverCommand.StderrPipe()
@ -191,7 +188,7 @@ func (r *Remote) SyncDB() ([]SyncDBInfo, error) {
if err := rows.Scan(&dbName); err != nil {
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
}
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
// This file was generated by swaggo/swag
// Code generated by swaggo/swag. DO NOT EDIT.
package docs
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": {
"get": {
"security": [
@ -14889,6 +14914,9 @@ const docTemplate = `{
"dbName": {
"type": "string"
},
"dbType": {
"type": "string"
},
"exclusionRules": {
"type": "string"
},
@ -14974,6 +15002,9 @@ const docTemplate = `{
"dbName": {
"type": "string"
},
"dbType": {
"type": "string"
},
"exclusionRules": {
"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": {
"type": "object",
"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": {
"get": {
"security": [
@ -14882,6 +14907,9 @@
"dbName": {
"type": "string"
},
"dbType": {
"type": "string"
},
"exclusionRules": {
"type": "string"
},
@ -14967,6 +14995,9 @@
"dbName": {
"type": "string"
},
"dbType": {
"type": "string"
},
"exclusionRules": {
"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": {
"type": "object",
"properties": {

View file

@ -586,6 +586,8 @@ definitions:
type: integer
dbName:
type: string
dbType:
type: string
exclusionRules:
type: string
hour:
@ -644,6 +646,8 @@ definitions:
type: integer
dbName:
type: string
dbType:
type: string
exclusionRules:
type: string
hour:
@ -928,6 +932,17 @@ definitions:
version:
type: string
type: object
dto.DatabaseItem:
properties:
database:
type: string
from:
type: string
id:
type: integer
name:
type: string
type: object
dto.DatabaseOption:
properties:
address:
@ -7695,6 +7710,21 @@ paths:
formatEN: delete database [names]
formatZH: 删除远程数据库 [names]
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:
get:
description: 获取远程数据库列表

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,12 @@
:close-on-click-modal="false"
>
<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-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" />
<span class="input-help">