mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
feat: support mongodb management (#12486)
#### What this PR does / why we need it? Refs https://github.com/1Panel-dev/1Panel/issues/7759 #### Summary of your change #### Please indicate you've done the following: - [ ] Made sure tests are passing and test coverage is added if needed. - [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/). - [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
This commit is contained in:
@@ -415,6 +415,11 @@ func (b *BaseApi) Backup(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -475,6 +480,11 @@ func (b *BaseApi) Recover(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -530,6 +540,11 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
263
agent/app/api/v2/database_mongodb.go
Normal file
263
agent/app/api/v2/database_mongodb.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Create mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mongodb 数据库 [name]","formatEN":"create mongodb database [name]"}
|
||||
func (b *BaseApi) CreateMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
|
||||
if _, err := mongodbService.Create(context.Background(), req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Page mongodb databases
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/search [post]
|
||||
func (b *BaseApi) SearchMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := mongodbService.SearchWithPage(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Update mongodb database description
|
||||
// @Accept json
|
||||
// @Param request body dto.UpdateDescription true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/description [post]
|
||||
// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"mongodb 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mongodb database [name] is modified => [description]"}
|
||||
func (b *BaseApi) UpdateMongodbDescription(c *gin.Context) {
|
||||
var req dto.UpdateDescription
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.UpdateDescription(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb database from remote
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbLoadDB true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/load [post]
|
||||
func (b *BaseApi) LoadMongodbFromRemote(c *gin.Context) {
|
||||
var req dto.MongodbLoadDB
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.LoadFromRemote(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Bind mongodb database user info
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbBind true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/bind [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mongodb 数据库 [database] [name] 用户 [username]","formatEN":"bind mongodb database [database] [name] user [username]"}
|
||||
func (b *BaseApi) BindMongodbUser(c *gin.Context) {
|
||||
var req dto.MongodbBind
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
|
||||
if err := mongodbService.BindUser(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb database password
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPassword true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/password [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] [name] 密码","formatEN":"update mongodb database [database] [name] password"}
|
||||
func (b *BaseApi) ChangeMongodbPassword(c *gin.Context) {
|
||||
var req dto.MongodbPassword
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangePassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivilegesLoad true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges [post]
|
||||
func (b *BaseApi) LoadMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivilegesLoad
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := mongodbService.LoadPrivileges(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, permission)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivileges true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges/change [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] 用户 [username] 权限","formatEN":"update mongodb database [database] user [username] privileges"}
|
||||
func (b *BaseApi) ChangeMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivileges
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangePrivileges(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Check before delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDeleteCheck true "request"
|
||||
// @Success 200 {array} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del/check [post]
|
||||
func (b *BaseApi) DeleteCheckMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDeleteCheck
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apps, err := mongodbService.DeleteCheck(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, apps)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"删除 mongodb 数据库 [name]","formatEN":"delete mongodb database [name]"}
|
||||
func (b *BaseApi) DeleteMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tx, ctx := helper.GetTxAndContext()
|
||||
if err := mongodbService.Delete(ctx, req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ var (
|
||||
dbCommonService = service.NewIDBCommonService()
|
||||
mysqlService = service.NewIMysqlService()
|
||||
postgresqlService = service.NewIPostgresqlService()
|
||||
mongodbService = service.NewIMongodbService()
|
||||
databaseService = service.NewIDatabaseService()
|
||||
redisService = service.NewIRedisService()
|
||||
|
||||
|
||||
@@ -213,19 +213,29 @@ func loadContainerInitCmd(c *gin.Context) ([]string, error) {
|
||||
func loadDatabaseInitCmd(c *gin.Context) ([]string, error) {
|
||||
database := c.Query("database")
|
||||
databaseType := c.Query("databaseType")
|
||||
if len(database) == 0 || len(databaseType) == 0 {
|
||||
if len(databaseType) == 0 {
|
||||
return nil, fmt.Errorf("error param of database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
databaseConn, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Type: databaseType, Name: database})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
if len(databaseConn.ContainerName) == 0 {
|
||||
return nil, fmt.Errorf("no such database container for database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
commands := []string{"exec", "-it", databaseConn.ContainerName}
|
||||
switch databaseType {
|
||||
case "mysql", "mysql-cluster":
|
||||
commands = append(commands, []string{"mysql", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mariadb":
|
||||
commands = append(commands, []string{"mariadb", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mongodb":
|
||||
commands = append(commands, []string{
|
||||
"mongosh",
|
||||
"--username", databaseConn.Username,
|
||||
"--password", databaseConn.Password,
|
||||
"--authenticationDatabase", "admin",
|
||||
}...)
|
||||
case "postgresql", "postgresql-cluster":
|
||||
commands = []string{"exec", "-e", fmt.Sprintf("PGPASSWORD=%s", databaseConn.Password), "-it", databaseConn.ContainerName, "psql", "-t", "-U", databaseConn.Username}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ type UploadForRecover struct {
|
||||
}
|
||||
|
||||
type CommonBackup struct {
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
Secret string `json:"secret"`
|
||||
@@ -78,7 +78,7 @@ type CommonBackup struct {
|
||||
}
|
||||
type CommonRecover struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
|
||||
@@ -137,6 +137,83 @@ type MysqlStatus struct {
|
||||
Position string `json:"Position"`
|
||||
}
|
||||
|
||||
// mongodb
|
||||
type MongodbDBSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
type MongodbDBInfo struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
MongodbName string `json:"mongodbName"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbDBCreate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbLoadDB struct {
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDeleteCheck struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
DeleteBackup bool `json:"deleteBackup"`
|
||||
}
|
||||
|
||||
type MongodbBind struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPassword struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPrivileges struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
}
|
||||
|
||||
type MongodbPrivilegesLoad struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type MysqlVariables struct {
|
||||
BinlogCacheSize string `json:"binlog_cache_size"`
|
||||
InnodbBufferPoolSize string `json:"innodb_buffer_pool_size"`
|
||||
|
||||
12
agent/app/model/database_mongodb.go
Normal file
12
agent/app/model/database_mongodb.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type DatabaseMongodb struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
From string `json:"from" gorm:"not null;default:local"`
|
||||
MongodbName string `json:"mongodbName" gorm:"not null"`
|
||||
Username string `json:"username" gorm:"not null"`
|
||||
Password string `json:"password" gorm:"not null"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
113
agent/app/repo/database_mongodb.go
Normal file
113
agent/app/repo/database_mongodb.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MongodbRepo struct{}
|
||||
|
||||
type IMongodbRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseMongodb, error)
|
||||
WithByMongodbName(mongodbName string) DBOption
|
||||
List(opts ...DBOption) ([]model.DatabaseMongodb, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMongodb, error)
|
||||
Create(ctx context.Context, mongodb *model.DatabaseMongodb) error
|
||||
Delete(ctx context.Context, opts ...DBOption) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
DeleteLocal(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIMongodbRepo() IMongodbRepo {
|
||||
return &MongodbRepo{}
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Get(opts ...DBOption) (model.DatabaseMongodb, error) {
|
||||
var mongodb model.DatabaseMongodb
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.First(&mongodb).Error; err != nil {
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
pass, err := encrypt.StringDecrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) List(opts ...DBOption) ([]model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.Find(&mongodbs).Error; err != nil {
|
||||
return mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
if err := db.Limit(size).Offset(size * (page - 1)).Find(&mongodbs).Error; err != nil {
|
||||
return count, mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return count, mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Create(ctx context.Context, mongodb *model.DatabaseMongodb) error {
|
||||
pass, err := encrypt.StringEncrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return getTx(ctx).Create(mongodb).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
||||
return getTx(ctx, opts...).Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) DeleteLocal(ctx context.Context) error {
|
||||
return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Update(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.DatabaseMongodb{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) WithByMongodbName(mongodbName string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("mongodb_name = ?", mongodbName)
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,13 @@ type IBackupService interface {
|
||||
|
||||
MysqlBackup(db dto.CommonBackup) error
|
||||
PostgresqlBackup(db dto.CommonBackup) error
|
||||
MongodbBackup(db dto.CommonBackup) error
|
||||
MysqlRecover(db dto.CommonRecover) error
|
||||
PostgresqlRecover(db dto.CommonRecover) error
|
||||
MongodbRecover(db dto.CommonRecover) error
|
||||
MysqlRecoverByUpload(req dto.CommonRecover) error
|
||||
PostgresqlRecoverByUpload(req dto.CommonRecover) error
|
||||
MongodbRecoverByUpload(req dto.CommonRecover) error
|
||||
|
||||
RedisBackup(db dto.CommonBackup) error
|
||||
RedisRecover(db dto.CommonRecover) error
|
||||
|
||||
493
agent/app/service/backup_mongodb.go
Normal file
493
agent/app/service/backup_mongodb.go
Normal file
@@ -0,0 +1,493 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
dockerImage "github.com/docker/docker/api/types/image"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
|
||||
timeNow := time.Now().Format(constant.DateTimeSlimLayout)
|
||||
itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName)
|
||||
targetDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||
fileName := fmt.Sprintf("%s_%s.gz", req.DetailName, timeNow+common.RandStrAndNum(5))
|
||||
|
||||
record := &model.BackupRecord{
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
DetailName: req.DetailName,
|
||||
SourceAccountIDs: "1",
|
||||
DownloadAccountID: 1,
|
||||
FileDir: itemDir,
|
||||
FileName: fileName,
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
|
||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecover(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecoverByUpload(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, targetDir, fileName string) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
backupTask := parentTask
|
||||
if backupTask == nil {
|
||||
backupTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
itemHandler := func(t *task.Task) error {
|
||||
return doMongodbBackup(req.Name, req.Type, req.DetailName, targetDir, fileName, req.Secret, t)
|
||||
}
|
||||
if parentTask != nil {
|
||||
return itemHandler(parentTask)
|
||||
}
|
||||
|
||||
backupTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup),
|
||||
func(t *task.Task) error { return itemHandler(t) },
|
||||
nil,
|
||||
0,
|
||||
3*time.Hour,
|
||||
)
|
||||
go func() {
|
||||
if err := backupTask.Execute(); err != nil {
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMongodbRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
recoverTask := parentTask
|
||||
if recoverTask == nil {
|
||||
recoverTask, err = task.NewTaskWithOps(itemName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
recoverDatabase := func(t *task.Task) error {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(req.File) {
|
||||
return buserr.WithName("ErrFileNotFound", req.File)
|
||||
}
|
||||
|
||||
restoreFile := req.File
|
||||
if len(req.Secret) != 0 {
|
||||
if err := files.OpensslDecrypt(req.File, req.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
restoreFile = path.Join(path.Dir(req.File), "tmp_"+path.Base(req.File))
|
||||
defer os.Remove(restoreFile)
|
||||
t.LogWithStatus(i18n.GetMsgByKey("Decrypt"), nil)
|
||||
}
|
||||
|
||||
isOk := false
|
||||
if !isRollback {
|
||||
rollbackFile := path.Join(
|
||||
global.Dir.TmpDir,
|
||||
fmt.Sprintf("database/%s/%s_%s.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)),
|
||||
)
|
||||
if err := doMongodbBackup(req.Name, req.Type, req.DetailName, path.Dir(rollbackFile), path.Base(rollbackFile), "", t); err != nil {
|
||||
return fmt.Errorf("backup mongodb db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||
}
|
||||
defer func() {
|
||||
if !isOk {
|
||||
global.LOG.Info("recover failed, start to rollback now")
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, rollbackFile, t); err != nil {
|
||||
global.LOG.Errorf("rollback mongodb db %s from %s failed, err: %v", req.DetailName, rollbackFile, err)
|
||||
} else {
|
||||
global.LOG.Infof("rollback mongodb db %s from %s successful", req.DetailName, rollbackFile)
|
||||
}
|
||||
}
|
||||
_ = os.RemoveAll(rollbackFile)
|
||||
}()
|
||||
}
|
||||
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, restoreFile, t); err != nil {
|
||||
global.LOG.Errorf("recover mongodb db %s from %s failed, err: %v", req.DetailName, restoreFile, err)
|
||||
return err
|
||||
}
|
||||
isOk = true
|
||||
return nil
|
||||
}
|
||||
if parentTask != nil {
|
||||
return recoverDatabase(parentTask)
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
switch req.Timeout {
|
||||
case -1:
|
||||
timeout = 0
|
||||
case 0:
|
||||
timeout = 3 * time.Hour
|
||||
default:
|
||||
timeout = time.Duration(req.Timeout) * time.Second
|
||||
}
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskRecover"), recoverDatabase, nil, 0, timeout)
|
||||
go func() {
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbBackup(database, dbType, dbName, targetDir, fileName, secret string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
if err := doRemoteMongodbBackup(database, dbName, targetDir, fileName, taskItem); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(path.Join(targetDir, fileName), secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := path.Join(targetDir, fileName)
|
||||
containerFile := path.Join("/tmp", fileName)
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
uri := buildMongodbDumpURI(appInfo.UserName, appInfo.Password, dbName)
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongodump",
|
||||
"--uri="+uri,
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdMgr.Run("docker", "cp", fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile), targetFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(targetFile, secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbRestore(database, dbType, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
return doRemoteMongodbRestore(database, dbName, sourceFile, taskItem)
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
|
||||
containerFile := path.Join("/tmp", fmt.Sprintf("1panel-mongodb-restore-%s.gz", common.RandStrAndNum(8)))
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run("docker", "cp", sourceFile, fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongorestore",
|
||||
"--uri="+uri,
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
"--drop",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMongodbDumpURI(username, password, dbName string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/" + dbName,
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func buildMongodbRestoreURI(username, password string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/",
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func mongodbCmdMgr(taskItem *task.Task) *cmd.CommandHelper {
|
||||
if taskItem == nil {
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3 * time.Hour))
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3*time.Hour), cmd.WithTask(*taskItem))
|
||||
}
|
||||
|
||||
func doRemoteMongodbBackup(database, dbName, targetDir, fileName string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "backup", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start backup", imageTag))
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(path.Join(targetDir, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, constant.DirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file %s failed, err: %v", path.Join(targetDir, fileName), err)
|
||||
}
|
||||
defer func() { _ = targetFile.Close() }()
|
||||
|
||||
backupCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongodump",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--db="+dbName,
|
||||
"--archive",
|
||||
"--gzip",
|
||||
)
|
||||
backupCmd.Stdout = targetFile
|
||||
var stderr bytes.Buffer
|
||||
backupCmd.Stderr = &stderr
|
||||
if err := backupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle backup mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRemoteMongodbRestore(database, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "restore", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start restore", imageTag))
|
||||
|
||||
fi, err := os.Open(sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = fi.Close() }()
|
||||
|
||||
restoreCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongorestore",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive",
|
||||
"--gzip",
|
||||
"--drop",
|
||||
)
|
||||
restoreCmd.Stdin = fi
|
||||
var stderr bytes.Buffer
|
||||
restoreCmd.Stderr = &stderr
|
||||
if err := restoreCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle recover mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureMongodbImage(database string, taskItem *task.Task) (string, error) {
|
||||
imageTag, exists, err := loadMongodbImageTag(database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("check local image %s", imageTag))
|
||||
if exists {
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s exists", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s not found, start docker pull", imageTag))
|
||||
if err := mongodbCmdMgr(taskItem).Run("docker", "pull", imageTag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("docker pull %s finished", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
|
||||
func loadMongodbImageTag(database string) (string, bool, error) {
|
||||
databaseInfo, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
images, err := cli.ImageList(context.Background(), dockerImage.ListOptions{})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
imagePrefix := "mongo:" + loadMongodbImageMajor(databaseInfo.Version)
|
||||
for _, image := range images {
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, imagePrefix) {
|
||||
return tag, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return imagePrefix, false, nil
|
||||
}
|
||||
|
||||
func buildMongodbNamespace(sourceFile, targetDB string) string {
|
||||
sourceDB := loadMongodbBackupDBName(sourceFile, targetDB)
|
||||
return sourceDB + ".*"
|
||||
}
|
||||
|
||||
func loadMongodbBackupDBName(sourceFile, defaultDB string) string {
|
||||
baseName := path.Base(sourceFile)
|
||||
if strings.HasSuffix(baseName, ".gz") {
|
||||
baseName = strings.TrimSuffix(baseName, ".gz")
|
||||
}
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`^1panel_mongodb_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^db_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
matches := pattern.FindStringSubmatch(baseName)
|
||||
if len(matches) == 2 && len(matches[1]) != 0 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
return defaultDB
|
||||
}
|
||||
|
||||
func loadMongodbImageMajor(version string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(version, "6"):
|
||||
return "6"
|
||||
case strings.HasPrefix(version, "7"):
|
||||
return "7"
|
||||
default:
|
||||
return "8"
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbImage(taskItem *task.Task, action, database, dbName, imageTag string, info mongodbConnectionInfo) {
|
||||
message := fmt.Sprintf(
|
||||
"use local docker image %s to %s remote mongodb %s/%s via %s:%d",
|
||||
imageTag,
|
||||
action,
|
||||
database,
|
||||
dbName,
|
||||
info.Address,
|
||||
info.Port,
|
||||
)
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbStep(taskItem *task.Task, message string) {
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
@@ -287,6 +287,15 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(db.Name), repo.WithByName(db.DetailName))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName))
|
||||
|
||||
@@ -165,7 +165,20 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||
|
||||
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)))
|
||||
switch dbInfo.DBType {
|
||||
case constant.AppMongodb:
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf(
|
||||
"db_%s_%s.gz",
|
||||
dbInfo.Name,
|
||||
startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5),
|
||||
))
|
||||
default:
|
||||
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, cronjob.Secret); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -177,6 +190,17 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
if err := doMongodbBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName, cronjob.Secret, task); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
retry++
|
||||
return err
|
||||
} else {
|
||||
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
|
||||
cleanAccountMap(accountMap)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := doPostgresqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret, taskItem); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -404,6 +428,18 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
databaseService := NewIDatabaseService()
|
||||
mongodbItems, _ := databaseService.LoadItems(cronjob.DBType)
|
||||
for _, mongodb := range mongodbItems {
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodb.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodb.Database,
|
||||
Name: mongodb.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
pgItems, _ := postgresqlRepo.List()
|
||||
for _, pg := range pgItems {
|
||||
@@ -430,6 +466,15 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Name: mysqlItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
mongodbItem, _ := mongodbRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodbItem.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodbItem.MongodbName,
|
||||
Name: mongodbItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else {
|
||||
pgItem, _ := postgresqlRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
|
||||
@@ -90,7 +90,7 @@ 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 == constant.AppPostgresql || dbType == constant.AppPostgresqlCluster {
|
||||
if db.Type == constant.AppPostgresql || db.Type == constant.AppPostgresqlCluster {
|
||||
items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name))
|
||||
for _, item := range items {
|
||||
var dItem dto.DatabaseItem
|
||||
@@ -100,6 +100,16 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
dItem.Database = db.Name
|
||||
datas = append(datas, dItem)
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
items, _ := mongodbRepo.List(mongodbRepo.WithByMongodbName(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 {
|
||||
@@ -138,6 +148,13 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
})
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, connectErr := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if connectErr == nil {
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
}
|
||||
err = connectErr
|
||||
case "mysql", "mariadb":
|
||||
_, err = mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -195,6 +212,13 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -265,6 +289,10 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
|
||||
if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
if err := mongodbRepo.Delete(context.Background(), mongodbRepo.WithByMongodbName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := postgresqlRepo.Delete(context.Background(), postgresqlRepo.WithByPostgresqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
@@ -297,6 +325,24 @@ func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(dto.DatabaseCreate{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
|
||||
913
agent/app/service/database_mongodb.go
Normal file
913
agent/app/service/database_mongodb.go
Normal file
@@ -0,0 +1,913 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/jinzhu/copier"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type MongodbService struct{}
|
||||
|
||||
type IMongodbService interface {
|
||||
SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error)
|
||||
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
|
||||
LoadFromRemote(req dto.MongodbLoadDB) error
|
||||
UpdateDescription(req dto.UpdateDescription) error
|
||||
BindUser(req dto.MongodbBind) error
|
||||
ChangePassword(req dto.MongodbPassword) error
|
||||
LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error)
|
||||
ChangePrivileges(req dto.MongodbPrivileges) error
|
||||
DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error)
|
||||
Delete(ctx context.Context, req dto.MongodbDBDelete) error
|
||||
}
|
||||
|
||||
func NewIMongodbService() IMongodbService {
|
||||
return &MongodbService{}
|
||||
}
|
||||
|
||||
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) {
|
||||
total, mongodbs, err := mongodbRepo.Page(
|
||||
search.Page,
|
||||
search.PageSize,
|
||||
mongodbRepo.WithByMongodbName(search.Database),
|
||||
repo.WithByLikeName(search.Info),
|
||||
repo.WithOrderRuleBy(search.OrderBy, search.Order),
|
||||
)
|
||||
var dtoMongodbs []dto.MongodbDBInfo
|
||||
for _, mongodb := range mongodbs {
|
||||
var item dto.MongodbDBInfo
|
||||
if err := copier.Copy(&item, &mongodb); err != nil {
|
||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
dtoMongodbs = append(dtoMongodbs, item)
|
||||
}
|
||||
return total, dtoMongodbs, err
|
||||
}
|
||||
|
||||
func (u *MongodbService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return mongodbRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
||||
func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
mongodb, _ := mongodbRepo.Get(repo.WithByName(req.Name), mongodbRepo.WithByMongodbName(req.Database), repo.WithByFrom(req.From))
|
||||
if mongodb.ID != 0 {
|
||||
return nil, buserr.New("ErrRecordExist")
|
||||
}
|
||||
|
||||
var createItem model.DatabaseMongodb
|
||||
if err := copier.Copy(&createItem, &req); err != nil {
|
||||
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
createItem.MongodbName = req.Database
|
||||
|
||||
if err := runMongodbCreate(req.From, req.Database, req.Name, req.Username, req.Password, req.Permission); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
global.LOG.Infof("create mongodb database %s successful", req.Name)
|
||||
if err := mongodbRepo.Create(ctx, &createItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &createItem, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) error {
|
||||
databases, err := mongodbRepo.List(mongodbRepo.WithByMongodbName(req.Database))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datas, err := loadMongodbDatabases(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
global.LOG.Infof("sync mongodb databases from %s:%s, found %d items", req.From, req.Database, len(datas))
|
||||
|
||||
deleteList := append([]model.DatabaseMongodb(nil), databases...)
|
||||
for _, data := range datas {
|
||||
hasOld := false
|
||||
for i := 0; i < len(databases); i++ {
|
||||
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MongodbName, req.Database) {
|
||||
hasOld = true
|
||||
updateMap := map[string]interface{}{"is_delete": false}
|
||||
if len(data.Username) != 0 {
|
||||
updateMap["username"] = data.Username
|
||||
}
|
||||
_ = mongodbRepo.Update(databases[i].ID, updateMap)
|
||||
for j := 0; j < len(deleteList); j++ {
|
||||
if deleteList[j].ID == databases[i].ID {
|
||||
deleteList = append(deleteList[:j], deleteList[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOld {
|
||||
createItem := model.DatabaseMongodb{
|
||||
Name: data.Name,
|
||||
From: req.From,
|
||||
MongodbName: req.Database,
|
||||
Username: data.Username,
|
||||
Password: "",
|
||||
Description: "",
|
||||
}
|
||||
if err := mongodbRepo.Create(context.Background(), &createItem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, delItem := range deleteList {
|
||||
_ = mongodbRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) BindUser(req dto.MongodbBind) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindMongodbUser(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"password": pass,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbItem.Username == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := updateMongodbPassword(dbItem.From, dbItem.MongodbName, dbItem.Name, dbItem.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{"password": pass})
|
||||
}
|
||||
|
||||
func (u *MongodbService) DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error) {
|
||||
var res []dto.DBResource
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if db.From == "local" {
|
||||
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error) {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return loadMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username)
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePrivileges(req dto.MongodbPrivileges) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Username) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Permission)
|
||||
}
|
||||
|
||||
func (u *MongodbService) Delete(ctx context.Context, req dto.MongodbDBDelete) error {
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runMongodbDelete(db.From, req.Database, db.Name); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.DeleteBackup {
|
||||
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(uploadDir); err == nil {
|
||||
_ = os.RemoveAll(uploadDir)
|
||||
}
|
||||
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(backupDir); err == nil {
|
||||
_ = os.RemoveAll(backupDir)
|
||||
}
|
||||
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(req.Type), repo.WithByName(req.Database), repo.WithByDetailName(db.Name))
|
||||
global.LOG.Infof("delete mongodb database %s-%s backups successful", req.Database, db.Name)
|
||||
}
|
||||
|
||||
_ = mongodbRepo.Delete(ctx, repo.WithByID(db.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMongodbCreate(from, database, dbName, username, password, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbCreate(database, dbName, username, password, permission)
|
||||
}
|
||||
script, err := buildMongodbCreateScript(dbName, username, password, permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func runMongodbDelete(from, database, dbName string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbDelete(database, dbName)
|
||||
}
|
||||
script, err := buildMongodbDeleteScript(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func bindMongodbUser(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return bindRemoteMongodbUser(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbBindUserScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func updateMongodbPassword(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbPasswordScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func runMongodbAdminScript(database, script string) error {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func runMongodbAdminScriptWithStdout(database, script string) (string, error) {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return "", fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().RunWithStdout(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMongodbCreateScript(dbName, username, password, permission string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
targetDb.createCollection("_init");
|
||||
targetDb.createUser({
|
||||
user: userName,
|
||||
pwd: password,
|
||||
roles: [{ role: permission, db: dbName }]
|
||||
});
|
||||
`, dbNameJSON, usernameJSON, passwordJSON, permissionJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbDeleteScript(dbName string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const dropUsersResult = targetDb.runCommand({ dropAllUsersFromDatabase: 1 });
|
||||
if (!dropUsersResult || dropUsersResult.ok !== 1) {
|
||||
throw new Error("failed to drop users from " + dbName);
|
||||
}
|
||||
const dropDatabaseResult = targetDb.runCommand({ dropDatabase: 1 });
|
||||
if (!dropDatabaseResult || dropDatabaseResult.ok !== 1) {
|
||||
throw new Error("failed to drop database " + dbName);
|
||||
}
|
||||
`, dbNameJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbBindUserScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user " + userName);
|
||||
}
|
||||
const roles = [{ role: "readWrite", db: dbName }];
|
||||
if (Array.isArray(userInfo.users) && userInfo.users.length > 0) {
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user " + userName);
|
||||
}
|
||||
} else {
|
||||
const result = targetDb.runCommand({
|
||||
createUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to create mongodb user " + userName);
|
||||
}
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbPasswordScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user password " + userName);
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
type mongodbSyncItem struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func loadMongodbDatabases(req dto.MongodbLoadDB) ([]mongodbSyncItem, error) {
|
||||
if req.From == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbDatabases(req.Database)
|
||||
}
|
||||
return loadLocalMongodbDatabases(req.Database)
|
||||
}
|
||||
|
||||
func loadLocalMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
script := strings.TrimSpace(`
|
||||
const systemDbs = new Set(["admin", "config", "local"]);
|
||||
const result = db.adminCommand({ listDatabases: 1, nameOnly: false });
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to list mongodb databases");
|
||||
}
|
||||
const items = result.databases
|
||||
.filter(item => !systemDbs.has(item.name))
|
||||
.map(item => ({ name: item.name, username: "" }));
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(items));
|
||||
print("__1panel_json_end__");
|
||||
`)
|
||||
stdout, err := runMongodbAdminScriptWithStdout(database, script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0)
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func runRemoteMongodbCreate(database, dbName, username, password, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.CreateCollection(ctx, "_init"); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func runRemoteMongodbDelete(database, dbName string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.RunCommand(ctx, bson.D{{Key: "dropAllUsersFromDatabase", Value: 1}}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{{Key: "dropDatabase", Value: 1}}).Err()
|
||||
}
|
||||
|
||||
func bindRemoteMongodbUser(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct{} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) > 0 {
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func loadRemoteMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
adminDB := client.Database("admin")
|
||||
var listResult struct {
|
||||
Databases []struct {
|
||||
Name string `bson:"name"`
|
||||
} `bson:"databases"`
|
||||
}
|
||||
if err := adminDB.RunCommand(ctx, bson.D{
|
||||
{Key: "listDatabases", Value: 1},
|
||||
{Key: "nameOnly", Value: false},
|
||||
}).Decode(&listResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemDbs := map[string]struct{}{
|
||||
"admin": {},
|
||||
"config": {},
|
||||
"local": {},
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0, len(listResult.Databases))
|
||||
for _, databaseItem := range listResult.Databases {
|
||||
if _, ok := systemDbs[databaseItem.Name]; ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, mongodbSyncItem{
|
||||
Name: databaseItem.Name,
|
||||
Username: "",
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func loadMongodbPrivilege(from, connectionName, dbName, username string) (string, error) {
|
||||
if from == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
return loadLocalMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
|
||||
func updateMongodbPrivilege(from, connectionName, dbName, username, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
return updateLocalMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
|
||||
func loadLocalMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const result = db.getSiblingDB(dbName).runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
const roles = Array.isArray(result.users) && result.users.length > 0 ? result.users[0].roles || [] : [];
|
||||
const permissions = roles.filter(role => role.db === dbName).map(role => role.role);
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(permissions));
|
||||
print("__1panel_json_end__");
|
||||
`, databaseJSON, usernameJSON))
|
||||
stdout, err := runMongodbAdminScriptWithStdout(connectionName, script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var permissions []string
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &permissions); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateLocalMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
if (!Array.isArray(userInfo.users) || userInfo.users.length === 0) {
|
||||
throw new Error("mongodb user not found: " + userName);
|
||||
}
|
||||
const roles = (userInfo.users[0].roles || []).filter(role => role.db !== dbName);
|
||||
roles.push({ role: permission, db: dbName });
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user privileges");
|
||||
}
|
||||
`, databaseJSON, usernameJSON, permissionJSON))
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func loadRemoteMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var result struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
permissions := make([]string, 0)
|
||||
if len(result.Users) > 0 {
|
||||
for _, role := range result.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
permissions = append(permissions, role.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) == 0 {
|
||||
return fmt.Errorf("mongodb user %s not found in database %s", username, dbName)
|
||||
}
|
||||
roles := make(bson.A, 0, len(userInfo.Users[0].Roles)+1)
|
||||
for _, role := range userInfo.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: role.Role}, {Key: "db", Value: role.DB}})
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}})
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "roles", Value: roles},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func normalizeMongodbPrivilege(permissions []string) string {
|
||||
roleMap := map[string]struct{}{
|
||||
"dbOwner": {},
|
||||
"read": {},
|
||||
"readWrite": {},
|
||||
"userAdmin": {},
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if _, ok := roleMap[permission]; ok {
|
||||
return permission
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isSupportedMongodbPrivilege(permission string) bool {
|
||||
switch permission {
|
||||
case "dbOwner", "read", "readWrite", "userAdmin":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractMongodbJSONOutput(stdout string) ([]byte, error) {
|
||||
const (
|
||||
beginMark = "__1panel_json_begin__"
|
||||
endMark = "__1panel_json_end__"
|
||||
)
|
||||
|
||||
output := strings.TrimSpace(stdout)
|
||||
if output == "" {
|
||||
return nil, fmt.Errorf("empty mongodb command output")
|
||||
}
|
||||
|
||||
beginIndex := strings.Index(output, beginMark)
|
||||
endIndex := strings.LastIndex(output, endMark)
|
||||
if beginIndex >= 0 && endIndex > beginIndex {
|
||||
content := strings.TrimSpace(output[beginIndex+len(beginMark) : endIndex])
|
||||
if content != "" {
|
||||
return []byte(content), nil
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
return []byte(line), nil
|
||||
}
|
||||
return nil, fmt.Errorf("mongodb command output does not contain json result")
|
||||
}
|
||||
@@ -21,6 +21,7 @@ var (
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
mongodbRepo = repo.NewIMongodbRepo()
|
||||
databaseRepo = repo.NewIDatabaseRepo()
|
||||
|
||||
imageRepoRepo = repo.NewIImageRepoRepo()
|
||||
|
||||
147
agent/app/service/mongodb_client.go
Normal file
147
agent/app/service/mongodb_client.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
type mongodbConnectionInfo struct {
|
||||
Address string
|
||||
Port uint
|
||||
InitialDB string
|
||||
Username string
|
||||
Password string
|
||||
Timeout uint
|
||||
SSL bool
|
||||
RootCert string
|
||||
ClientKey string
|
||||
ClientCert string
|
||||
SkipVerify bool
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromCreate(req dto.DatabaseCreate) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
InitialDB: req.InitialDB,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromModel(db model.Database) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: db.Address,
|
||||
Port: db.Port,
|
||||
InitialDB: db.InitialDB,
|
||||
Username: db.Username,
|
||||
Password: db.Password,
|
||||
Timeout: db.Timeout,
|
||||
SSL: db.SSL,
|
||||
RootCert: db.RootCert,
|
||||
ClientKey: db.ClientKey,
|
||||
ClientCert: db.ClientCert,
|
||||
SkipVerify: db.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRemoteMongodbConnection(database string) (mongodbConnectionInfo, error) {
|
||||
db, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return mongodbConnectionInfo{}, err
|
||||
}
|
||||
return mongodbConnectionInfoFromModel(db), nil
|
||||
}
|
||||
|
||||
func newRemoteMongodbClient(info mongodbConnectionInfo) (*mongo.Client, context.Context, context.CancelFunc, error) {
|
||||
timeout := time.Duration(info.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
clientOptions := options.Client().ApplyURI(buildRemoteMongodbURI(info)).
|
||||
SetServerSelectionTimeout(timeout).
|
||||
SetConnectTimeout(timeout)
|
||||
if info.SSL {
|
||||
tlsConfig, err := buildRemoteMongodbTLSConfig(info)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
clientOptions.SetTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if err := client.Ping(ctx, readpref.Primary()); err != nil {
|
||||
_ = client.Disconnect(context.Background())
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return client, ctx, cancel, nil
|
||||
}
|
||||
|
||||
func buildRemoteMongodbURI(info mongodbConnectionInfo) string {
|
||||
uri := url.URL{
|
||||
Scheme: "mongodb",
|
||||
Host: net.JoinHostPort(info.Address, strconv.Itoa(int(info.Port))),
|
||||
Path: "/",
|
||||
}
|
||||
uri.User = url.UserPassword(info.Username, info.Password)
|
||||
query := url.Values{}
|
||||
authSource := info.InitialDB
|
||||
if authSource == "" {
|
||||
authSource = "admin"
|
||||
}
|
||||
query.Set("authSource", authSource)
|
||||
query.Set("directConnection", "true")
|
||||
uri.RawQuery = query.Encode()
|
||||
return uri.String()
|
||||
}
|
||||
|
||||
func buildRemoteMongodbTLSConfig(info mongodbConnectionInfo) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: info.SkipVerify,
|
||||
}
|
||||
|
||||
if info.RootCert != "" {
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM([]byte(info.RootCert)) {
|
||||
return nil, fmt.Errorf("load mongodb ca cert failed")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
if info.ClientCert != "" && info.ClientKey != "" {
|
||||
cert, err := tls.X509KeyPair([]byte(info.ClientCert), []byte(info.ClientKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
@@ -43,6 +43,7 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.19.0
|
||||
@@ -50,6 +51,7 @@ require (
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
|
||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
go.mongodb.org/mongo-driver v1.17.6
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
@@ -193,6 +195,7 @@ require (
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||
@@ -220,7 +223,6 @@ require (
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sigstore/sigstore v1.10.4 // indirect
|
||||
github.com/sigstore/sigstore-go v1.1.4 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
@@ -238,9 +240,12 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
|
||||
|
||||
@@ -794,6 +794,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
||||
@@ -1067,14 +1069,19 @@ github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CP
|
||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
||||
@@ -57,6 +57,7 @@ func InitAgentDB() {
|
||||
migrations.UpdateDatabase,
|
||||
migrations.AddGPUMonitor,
|
||||
migrations.UpdateDatabaseMysql,
|
||||
migrations.AddDatabaseMongodb,
|
||||
migrations.InitIptablesStatus,
|
||||
migrations.UpdateWebsite,
|
||||
migrations.AddisIPtoWebsiteSSL,
|
||||
|
||||
@@ -49,6 +49,7 @@ var AddTable = &gormigrate.Migration{
|
||||
&model.Cronjob{},
|
||||
&model.Database{},
|
||||
&model.DatabaseMysql{},
|
||||
&model.DatabaseMongodb{},
|
||||
&model.DatabasePostgresql{},
|
||||
&model.Favorite{},
|
||||
&model.FileShare{},
|
||||
@@ -823,6 +824,13 @@ var UpdateDatabaseMysql = &gormigrate.Migration{
|
||||
},
|
||||
}
|
||||
|
||||
var AddDatabaseMongodb = &gormigrate.Migration{
|
||||
ID: "20260413-add-database-mongodb",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.DatabaseMongodb{})
|
||||
},
|
||||
}
|
||||
|
||||
var InitIptablesStatus = &gormigrate.Migration{
|
||||
ID: "20251201-init-iptables-status",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
@@ -58,5 +58,16 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
cmdRouter.POST("/pg/privileges", baseApi.ChangePostgresqlPrivileges)
|
||||
cmdRouter.POST("/pg/password", baseApi.ChangePostgresqlPassword)
|
||||
cmdRouter.POST("/pg/description", baseApi.UpdatePostgresqlDescription)
|
||||
|
||||
cmdRouter.POST("/mongodb", baseApi.CreateMongodb)
|
||||
cmdRouter.POST("/mongodb/search", baseApi.SearchMongodb)
|
||||
cmdRouter.POST("/mongodb/description", baseApi.UpdateMongodbDescription)
|
||||
cmdRouter.POST("/mongodb/load", baseApi.LoadMongodbFromRemote)
|
||||
cmdRouter.POST("/mongodb/bind", baseApi.BindMongodbUser)
|
||||
cmdRouter.POST("/mongodb/password", baseApi.ChangeMongodbPassword)
|
||||
cmdRouter.POST("/mongodb/privileges", baseApi.LoadMongodbPrivileges)
|
||||
cmdRouter.POST("/mongodb/privileges/change", baseApi.ChangeMongodbPrivileges)
|
||||
cmdRouter.POST("/mongodb/del/check", baseApi.DeleteCheckMongodb)
|
||||
cmdRouter.POST("/mongodb/del", baseApi.DeleteMongodb)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,66 @@ export namespace Database {
|
||||
forceDelete: boolean;
|
||||
deleteBackup: boolean;
|
||||
}
|
||||
export interface MongodbDBInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
name: string;
|
||||
mongodbName: string;
|
||||
from: string;
|
||||
username: string;
|
||||
password: string;
|
||||
isDelete: boolean;
|
||||
description: string;
|
||||
showPassword?: boolean;
|
||||
}
|
||||
export interface MongodbDBCreate {
|
||||
name: string;
|
||||
from: string;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
permission: string;
|
||||
description: string;
|
||||
}
|
||||
export interface MongodbLoadDB {
|
||||
from: string;
|
||||
type: string;
|
||||
database: string;
|
||||
}
|
||||
export interface MongodbDBDeleteCheck {
|
||||
id: number;
|
||||
type: string;
|
||||
database: string;
|
||||
}
|
||||
export interface MongodbDBDelete {
|
||||
id: number;
|
||||
type: string;
|
||||
database: string;
|
||||
forceDelete: boolean;
|
||||
deleteBackup: boolean;
|
||||
}
|
||||
export interface MongodbBind {
|
||||
database: string;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
export interface MongodbPassword {
|
||||
database: string;
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
export interface MongodbPrivileges {
|
||||
database: string;
|
||||
name: string;
|
||||
username: string;
|
||||
permission: string;
|
||||
}
|
||||
export interface MongodbPrivilegesLoad {
|
||||
database: string;
|
||||
name: string;
|
||||
username: string;
|
||||
}
|
||||
export interface MysqlVariables {
|
||||
mysqlName: string;
|
||||
binlog_cache_size: number;
|
||||
@@ -309,6 +369,7 @@ export namespace Database {
|
||||
from: string;
|
||||
address: string;
|
||||
port: number;
|
||||
initialDB: string;
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
@@ -326,6 +387,7 @@ export namespace Database {
|
||||
version: string;
|
||||
address: string;
|
||||
port: number;
|
||||
initialDB: string;
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
|
||||
@@ -89,6 +89,45 @@ export const deleteMysqlDB = (params: Database.MysqlDBDelete) => {
|
||||
return http.post(`/databases/del`, params);
|
||||
};
|
||||
|
||||
// mongodb
|
||||
export const searchMongodbDBs = (params: Database.SearchDBWithPage, node?: string) => {
|
||||
const query = node ? `?operateNode=${node}` : '';
|
||||
return http.post<ResPage<Database.MongodbDBInfo>>(`/databases/mongodb/search${query}`, params);
|
||||
};
|
||||
export const addMongodbDB = (params: Database.MongodbDBCreate) => {
|
||||
let request = deepCopy(params) as Database.MongodbDBCreate;
|
||||
encodeBase64Fields(request, ['password']);
|
||||
return http.post(`/databases/mongodb`, request, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const loadMongodbFromRemote = (params: Database.MongodbLoadDB) => {
|
||||
return http.post(`/databases/mongodb/load`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const bindMongodbUser = (params: Database.MongodbBind) => {
|
||||
let request = deepCopy(params) as Database.MongodbBind;
|
||||
encodeBase64Fields(request, ['password']);
|
||||
return http.post(`/databases/mongodb/bind`, request, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const updateMongodbPassword = (params: Database.MongodbPassword) => {
|
||||
let request = deepCopy(params) as Database.MongodbPassword;
|
||||
encodeBase64Fields(request, ['password']);
|
||||
return http.post(`/databases/mongodb/password`, request, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const updateMongodbDescription = (params: DescriptionUpdate) => {
|
||||
return http.post(`/databases/mongodb/description`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const deleteCheckMongodbDB = (params: Database.MongodbDBDeleteCheck) => {
|
||||
return http.post<Database.DBResource[]>(`/databases/mongodb/del/check`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const deleteMongodbDB = (params: Database.MongodbDBDelete) => {
|
||||
return http.post(`/databases/mongodb/del`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const loadMongodbPrivileges = (params: Database.MongodbPrivilegesLoad) => {
|
||||
return http.post<string>(`/databases/mongodb/privileges`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const changeMongodbPrivileges = (params: Database.MongodbPrivileges) => {
|
||||
return http.post(`/databases/mongodb/privileges/change`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
|
||||
export const loadMysqlVariables = (type: string, database: string) => {
|
||||
return http.post<Database.MysqlVariables>(`/databases/variables`, { type: type, name: database });
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-model="open"
|
||||
:header="$t('menu.terminal')"
|
||||
@close="handleClose"
|
||||
:resource="database"
|
||||
:resource="resourceName"
|
||||
:autoClose="!open"
|
||||
size="large"
|
||||
:fullScreen="true"
|
||||
@@ -22,14 +22,32 @@ const open = ref(false);
|
||||
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
||||
const database = ref();
|
||||
const databaseType = ref();
|
||||
const resourceName = ref();
|
||||
const command = ref('/bin/sh');
|
||||
const user = ref('');
|
||||
const containerID = ref('');
|
||||
const initCmd = ref('');
|
||||
const waitForPrompt = ref('');
|
||||
|
||||
interface DialogProps {
|
||||
databaseType: string;
|
||||
database: string;
|
||||
databaseType?: string;
|
||||
database?: string;
|
||||
resourceName?: string;
|
||||
command?: string;
|
||||
user?: string;
|
||||
containerID?: string;
|
||||
initCmd?: string;
|
||||
waitForPrompt?: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
database.value = params.database;
|
||||
databaseType.value = params.databaseType;
|
||||
database.value = params.database || '';
|
||||
databaseType.value = params.databaseType || '';
|
||||
resourceName.value = params.resourceName || params.database || params.containerID || '';
|
||||
command.value = params.command || '/bin/sh';
|
||||
user.value = params.user || '';
|
||||
containerID.value = params.containerID || '';
|
||||
initCmd.value = params.initCmd || '';
|
||||
waitForPrompt.value = params.waitForPrompt || '';
|
||||
open.value = false;
|
||||
await initTerm();
|
||||
};
|
||||
@@ -37,11 +55,15 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
const initTerm = async () => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
const args = containerID.value
|
||||
? `source=container&containerid=${containerID.value}&user=${user.value}&command=${command.value}`
|
||||
: `source=database&databaseType=${databaseType.value}&database=${database.value}`;
|
||||
terminalRef.value!.acceptParams({
|
||||
endpoint: '/api/v2/containers/exec',
|
||||
args: `source=database&databaseType=${databaseType.value}&database=${database.value}`,
|
||||
args,
|
||||
error: '',
|
||||
initCmd: '',
|
||||
initCmd: initCmd.value,
|
||||
waitForPrompt: waitForPrompt.value,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ const terminalSocket = ref<WebSocket>();
|
||||
const heartbeatTimer = ref<NodeJS.Timer>();
|
||||
const latency = ref(0);
|
||||
const initCmd = ref('');
|
||||
const hideInitCmdEcho = ref(false);
|
||||
const initCmdEchoBuffer = ref('');
|
||||
const waitForPrompt = ref('');
|
||||
const waitForPromptBuffer = ref('');
|
||||
const aiNotice = ref({
|
||||
visible: false,
|
||||
loading: false,
|
||||
@@ -105,6 +109,7 @@ interface WsProps {
|
||||
args: string;
|
||||
error: string;
|
||||
initCmd: string;
|
||||
waitForPrompt?: string;
|
||||
}
|
||||
|
||||
interface TerminalBufferLine {
|
||||
@@ -117,6 +122,8 @@ const acceptParams = (props: WsProps) => {
|
||||
initError(props.error);
|
||||
} else {
|
||||
initCmd.value = props.initCmd || '';
|
||||
waitForPrompt.value = props.waitForPrompt || '';
|
||||
waitForPromptBuffer.value = '';
|
||||
init(props.endpoint, props.args);
|
||||
}
|
||||
});
|
||||
@@ -258,10 +265,46 @@ const initWebSocket = (endpoint_: string, args: string = '') => {
|
||||
const runRealTerminal = () => {
|
||||
webSocketReady.value = true;
|
||||
if (initCmd.value !== '') {
|
||||
hideInitCmdEcho.value = true;
|
||||
initCmdEchoBuffer.value = '';
|
||||
sendMsg(initCmd.value);
|
||||
}
|
||||
};
|
||||
|
||||
const stripInitCmdEchoLine = (message: string) => {
|
||||
if (!hideInitCmdEcho.value) {
|
||||
return message;
|
||||
}
|
||||
initCmdEchoBuffer.value += message;
|
||||
const lineBreakIndex = initCmdEchoBuffer.value.search(/\r?\n/);
|
||||
if (lineBreakIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lineBreakLength = initCmdEchoBuffer.value[lineBreakIndex] === '\r' ? 2 : 1;
|
||||
const remaining = initCmdEchoBuffer.value.slice(lineBreakIndex + lineBreakLength);
|
||||
hideInitCmdEcho.value = false;
|
||||
initCmdEchoBuffer.value = '';
|
||||
initCmd.value = '';
|
||||
return remaining;
|
||||
};
|
||||
|
||||
const flushPromptBuffer = (message: string) => {
|
||||
if (!waitForPrompt.value) {
|
||||
return message;
|
||||
}
|
||||
waitForPromptBuffer.value += message;
|
||||
const promptIndex = waitForPromptBuffer.value.indexOf(waitForPrompt.value);
|
||||
if (promptIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const visible = waitForPromptBuffer.value.slice(promptIndex);
|
||||
waitForPrompt.value = '';
|
||||
waitForPromptBuffer.value = '';
|
||||
return visible;
|
||||
};
|
||||
|
||||
const onWSReceive = (message: MessageEvent) => {
|
||||
const wsMsg = JSON.parse(message.data);
|
||||
switch (wsMsg.type) {
|
||||
@@ -269,9 +312,14 @@ const onWSReceive = (message: MessageEvent) => {
|
||||
term.value.element && term.value.focus();
|
||||
if (wsMsg.data) {
|
||||
let receiveMsg = decodeBase64(wsMsg.data);
|
||||
if (initCmd.value != '') {
|
||||
receiveMsg = receiveMsg?.replace(initCmd.value.trim(), '').trim();
|
||||
initCmd.value = '';
|
||||
if (hideInitCmdEcho.value) {
|
||||
receiveMsg = stripInitCmdEchoLine(receiveMsg);
|
||||
}
|
||||
if (receiveMsg && waitForPrompt.value) {
|
||||
receiveMsg = flushPromptBuffer(receiveMsg);
|
||||
}
|
||||
if (!receiveMsg) {
|
||||
break;
|
||||
}
|
||||
term.value.write(receiveMsg);
|
||||
}
|
||||
@@ -507,9 +555,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.ai-notice-fade-enter-active,
|
||||
.ai-notice-fade-leave-active {
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 180ms ease;
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.ai-mask-fade-enter-active,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<li v-if="type === 'mysql' || type === 'mariadb' || type === 'mysql-cluster'">
|
||||
{{ $t('database.formatHelper', [remark]) }}
|
||||
</li>
|
||||
<li v-if="isDb()">{{ $t('database.supportUpType') }}</li>
|
||||
<li v-if="isDb() && type !== 'mongodb'">{{ $t('database.supportUpType') }}</li>
|
||||
<li v-if="type === 'website' || type === 'app'">
|
||||
{{ $t('website.websiteBackupWarn') }}
|
||||
</li>
|
||||
@@ -40,7 +40,7 @@
|
||||
:limit="1"
|
||||
class="float-left"
|
||||
ref="uploadRef"
|
||||
accept=".tar.gz,.sql,.gz,.zip"
|
||||
:accept="uploadAccept"
|
||||
:show-file-list="false"
|
||||
:on-exceed="handleExceed"
|
||||
:on-change="fileOnChange"
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<DialogPro
|
||||
v-model="recoverDialog"
|
||||
:title="name ? $t('commons.button.recover') + ' - ' + name : $t('commons.button.recover')"
|
||||
:title="title ? $t('commons.button.recover') + ' - ' + title : $t('commons.button.recover')"
|
||||
@close="handleRecoverClose"
|
||||
size="small"
|
||||
>
|
||||
@@ -134,7 +134,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computeSize } from '@/utils/size';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { transferTimeToSecond } from '@/utils/validate';
|
||||
@@ -198,6 +198,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
case 'postgresql':
|
||||
case 'mysql-cluster':
|
||||
case 'postgresql-cluster':
|
||||
case 'mongodb':
|
||||
title.value = name.value + ' [ ' + detailName.value + ' ]';
|
||||
if (detailName.value) {
|
||||
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${name.value}/${detailName.value}/`;
|
||||
@@ -253,10 +254,14 @@ const beforeUpload = (fileName: string) => {
|
||||
return false;
|
||||
}
|
||||
if (isDb()) {
|
||||
const allowedExtensions = ['.sql', '.sql.gz', '.tar.gz', '.zip'];
|
||||
const allowedExtensions = type.value === 'mongodb' ? ['.gz'] : ['.sql', '.sql.gz', '.tar.gz', '.zip'];
|
||||
const isValidFile = allowedExtensions.some((ext) => itemName.endsWith(ext));
|
||||
if (!isValidFile) {
|
||||
MsgError(i18n.global.t('database.supportUpType'));
|
||||
MsgError(
|
||||
type.value === 'mongodb'
|
||||
? i18n.global.t('commons.msg.unSupportType')
|
||||
: i18n.global.t('database.supportUpType'),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -340,6 +345,7 @@ const onRecover = async (row: File.File) => {
|
||||
|
||||
const isDb = () => {
|
||||
return (
|
||||
type.value === 'mongodb' ||
|
||||
type.value === 'mysql' ||
|
||||
type.value === 'mariadb' ||
|
||||
type.value === 'postgresql' ||
|
||||
@@ -347,6 +353,9 @@ const isDb = () => {
|
||||
type.value === 'postgresql-cluster'
|
||||
);
|
||||
};
|
||||
const uploadAccept = computed(() => {
|
||||
return type.value === 'mongodb' ? '.gz' : '.tar.gz,.sql,.gz,.zip';
|
||||
});
|
||||
const uploaderFiles = ref<UploadFiles>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
|
||||
|
||||
@@ -547,6 +547,10 @@ const message = {
|
||||
address: 'Database address',
|
||||
version: 'Database version',
|
||||
userHelper: 'Use root or a user with root privileges to access the remote database.',
|
||||
mongodbPermissionDbOwner: 'Database owner',
|
||||
mongodbPermissionRead: 'Read data',
|
||||
mongodbPermissionReadWrite: 'Read and write data',
|
||||
mongodbPermissionUserAdmin: 'User administrator',
|
||||
pgUserHelper: 'Use a database superuser account.',
|
||||
ssl: 'Use SSL',
|
||||
clientKey: 'Client private key',
|
||||
|
||||
@@ -553,6 +553,10 @@ const message = {
|
||||
address: 'Dirección de la base de datos',
|
||||
version: 'Versión de la base de datos',
|
||||
userHelper: 'El usuario root o un usuario con privilegios de root puede acceder a la base de datos remota.',
|
||||
mongodbPermissionDbOwner: 'Propietario de la base de datos',
|
||||
mongodbPermissionRead: 'Leer datos',
|
||||
mongodbPermissionReadWrite: 'Leer y escribir datos',
|
||||
mongodbPermissionUserAdmin: 'Administrador de usuarios',
|
||||
pgUserHelper: 'Usuario de base de datos con privilegios de superusuario.',
|
||||
ssl: 'Usar SSL',
|
||||
clientKey: 'Clave privada del cliente',
|
||||
|
||||
@@ -549,6 +549,10 @@ const message = {
|
||||
version: 'データベースバージョン',
|
||||
userHelper:
|
||||
'ルートユーザーまたはルート特権を持つデータベースユーザーは、リモートデータベースにアクセスできます。',
|
||||
mongodbPermissionDbOwner: 'データベース所有者',
|
||||
mongodbPermissionRead: 'データの読み取り',
|
||||
mongodbPermissionReadWrite: 'データの読み取りと書き込み',
|
||||
mongodbPermissionUserAdmin: 'ユーザー管理者',
|
||||
pgUserHelper: 'スーパーユーザーの特権を持つデータベースユーザー。',
|
||||
ssl: 'SSLを使用します',
|
||||
clientKey: 'クライアントの秘密鍵',
|
||||
|
||||
@@ -542,6 +542,10 @@ const message = {
|
||||
address: '데이터베이스 주소',
|
||||
version: '데이터베이스 버전',
|
||||
userHelper: '루트 사용자 또는 루트 권한을 가진 데이터베이스 사용자가 원격 데이터베이스에 접근할 수 있습니다.',
|
||||
mongodbPermissionDbOwner: '데이터베이스 소유자',
|
||||
mongodbPermissionRead: '데이터 읽기',
|
||||
mongodbPermissionReadWrite: '데이터 읽기 및 쓰기',
|
||||
mongodbPermissionUserAdmin: '사용자 관리자',
|
||||
pgUserHelper: '슈퍼 사용자 권한을 가진 데이터베이스 사용자.',
|
||||
ssl: 'SSL 사용',
|
||||
clientKey: '클라이언트 개인 키',
|
||||
|
||||
@@ -555,6 +555,10 @@ const message = {
|
||||
version: 'Versi pangkalan data',
|
||||
userHelper:
|
||||
'Pengguna root atau pengguna pangkalan data dengan keistimewaan root boleh mengakses pangkalan data jauh.',
|
||||
mongodbPermissionDbOwner: 'Pemilik pangkalan data',
|
||||
mongodbPermissionRead: 'Baca data',
|
||||
mongodbPermissionReadWrite: 'Baca dan tulis data',
|
||||
mongodbPermissionUserAdmin: 'Pentadbir pengguna',
|
||||
pgUserHelper: 'Pengguna pangkalan data dengan keistimewaan superuser.',
|
||||
ssl: 'Gunakan SSL',
|
||||
clientKey: 'Kunci peribadi klien',
|
||||
|
||||
@@ -550,6 +550,10 @@ const message = {
|
||||
version: 'Versão do banco de dados',
|
||||
userHelper:
|
||||
'O usuário root ou um usuário do banco de dados com privilégios de root pode acessar o banco de dados remoto.',
|
||||
mongodbPermissionDbOwner: 'Proprietário do banco de dados',
|
||||
mongodbPermissionRead: 'Ler dados',
|
||||
mongodbPermissionReadWrite: 'Ler e gravar dados',
|
||||
mongodbPermissionUserAdmin: 'Administrador de usuários',
|
||||
pgUserHelper: 'Usuário do banco de dados com privilégios de superusuário.',
|
||||
ssl: 'Usar SSL',
|
||||
clientKey: 'Chave privada do cliente',
|
||||
|
||||
@@ -543,6 +543,10 @@ const message = {
|
||||
version: 'Версия базы данных',
|
||||
userHelper:
|
||||
'Пользователь root или пользователь базы данных с привилегиями root может получить доступ к удаленной базе данных.',
|
||||
mongodbPermissionDbOwner: 'Владелец базы данных',
|
||||
mongodbPermissionRead: 'Чтение данных',
|
||||
mongodbPermissionReadWrite: 'Чтение и запись данных',
|
||||
mongodbPermissionUserAdmin: 'Администратор пользователей',
|
||||
pgUserHelper: 'Пользователь базы данных с привилегиями суперпользователя.',
|
||||
ssl: 'Использовать SSL',
|
||||
clientKey: 'Приватный ключ клиента',
|
||||
|
||||
@@ -551,6 +551,10 @@ const message = {
|
||||
version: 'Veritabanı sürümü',
|
||||
userHelper:
|
||||
'Root kullanıcı veya root yetkilerine sahip bir veritabanı kullanıcısı uzak veritabanına erişebilir.',
|
||||
mongodbPermissionDbOwner: 'Veritabanı sahibi',
|
||||
mongodbPermissionRead: 'Veri okuma',
|
||||
mongodbPermissionReadWrite: 'Veri okuma ve yazma',
|
||||
mongodbPermissionUserAdmin: 'Kullanıcı yöneticisi',
|
||||
pgUserHelper: 'Süper kullanıcı yetkilerine sahip veritabanı kullanıcısı.',
|
||||
ssl: 'SSL Kullan',
|
||||
clientKey: 'İstemci özel anahtarı',
|
||||
|
||||
@@ -521,6 +521,10 @@ const message = {
|
||||
address: '資料庫位址',
|
||||
version: '資料庫版本',
|
||||
userHelper: 'root 使用者或擁有 root 權限的資料庫使用者',
|
||||
mongodbPermissionDbOwner: '資料庫擁有者',
|
||||
mongodbPermissionRead: '讀取資料',
|
||||
mongodbPermissionReadWrite: '讀取和寫入資料',
|
||||
mongodbPermissionUserAdmin: '使用者管理員',
|
||||
pgUserHelper: '具有超級管理員權限的資料庫使用者',
|
||||
ssl: '使用 SSL',
|
||||
clientKey: '用戶端私鑰',
|
||||
|
||||
@@ -510,6 +510,10 @@ const message = {
|
||||
address: '数据库地址',
|
||||
version: '数据库版本',
|
||||
userHelper: 'root 用户或拥有 root 权限的数据库用户',
|
||||
mongodbPermissionDbOwner: '数据库所有者',
|
||||
mongodbPermissionRead: '读取数据',
|
||||
mongodbPermissionReadWrite: '读取和写入数据',
|
||||
mongodbPermissionUserAdmin: '用户管理员',
|
||||
pgUserHelper: '拥有超级管理员权限的数据库用户',
|
||||
ssl: '使用 SSL',
|
||||
clientKey: '客户端私钥',
|
||||
|
||||
@@ -117,6 +117,31 @@ const databaseRouter = {
|
||||
detail: 'database.remote',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'mongodb',
|
||||
name: 'MongoDB',
|
||||
component: () => import('@/views/database/mongodb/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/databases',
|
||||
requiresAuth: false,
|
||||
parent: 'menu.database',
|
||||
title: 'MongoDB',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'mongodb/remote',
|
||||
name: 'MongoDB-Remote',
|
||||
component: () => import('@/views/database/mongodb/remote/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/databases',
|
||||
requiresAuth: false,
|
||||
parent: 'menu.database',
|
||||
title: 'MongoDB',
|
||||
detail: 'database.remote',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface GlobalState {
|
||||
currentDB: string;
|
||||
currentPgDB: string;
|
||||
currentRedisDB: string;
|
||||
currentMongodbDB: string;
|
||||
showEntranceWarn: boolean;
|
||||
defaultNetwork: string;
|
||||
defaultIO: string;
|
||||
|
||||
@@ -44,6 +44,7 @@ const GlobalStore = defineStore({
|
||||
currentDB: '',
|
||||
currentPgDB: '',
|
||||
currentRedisDB: '',
|
||||
currentMongodbDB: '',
|
||||
showEntranceWarn: true,
|
||||
defaultNetwork: 'all',
|
||||
defaultIO: 'all',
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
<el-option label="Mariadb" value="mariadb" />
|
||||
<el-option label="PostgreSQL" value="postgresql" />
|
||||
<el-option label="PostgreSQL-Cluster" value="postgresql-cluster" />
|
||||
<el-option label="MongoDB" value="mongodb" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</LayoutCol>
|
||||
|
||||
@@ -287,7 +287,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
recordShow.value = true;
|
||||
dialogData.value = params;
|
||||
if (dialogData.value.rowData.type === 'database') {
|
||||
const data = await listDbItems('mysql,mariadb,postgresql');
|
||||
const data = await listDbItems('mysql,mariadb,mysql-cluster,postgresql,postgresql-cluster,mongodb');
|
||||
let itemDBs = data.data || [];
|
||||
for (const item of itemDBs) {
|
||||
if (item.id == dialogData.value.rowData.dbName) {
|
||||
|
||||
@@ -21,5 +21,9 @@ const buttons = [
|
||||
label: 'Redis',
|
||||
path: '/databases/redis',
|
||||
},
|
||||
{
|
||||
label: 'MongoDB',
|
||||
path: '/databases/mongodb',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
94
frontend/src/views/database/mongodb/bind/index.vue
Normal file
94
frontend/src/views/database/mongodb/bind/index.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<DrawerPro
|
||||
v-model="bindVisible"
|
||||
:header="$t('database.userBind')"
|
||||
:resource="form.name"
|
||||
@close="handleClose"
|
||||
size="small"
|
||||
>
|
||||
<el-form v-loading="loading" ref="changeFormRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item :label="$t('commons.login.username')" prop="username">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input type="password" clearable show-password v-model="form.password" />
|
||||
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="bindVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="onSubmit(changeFormRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { bindMongodbUser } from '@/api/modules/database';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref();
|
||||
const bindVisible = ref(false);
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const changeFormRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
database: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
username: [Rules.requiredInput, Rules.name],
|
||||
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
database: string;
|
||||
name: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
form.database = params.database;
|
||||
form.name = params.name;
|
||||
form.username = '';
|
||||
form.password = '';
|
||||
bindVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
bindVisible.value = false;
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
await bindMongodbUser(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
bindVisible.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
207
frontend/src/views/database/mongodb/conn/index.vue
Normal file
207
frontend/src/views/database/mongodb/conn/index.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<DrawerPro v-model="dialogVisible" :header="$t('database.databaseConnInfo')" @close="handleClose" size="small">
|
||||
<el-form @submit.prevent v-loading="loading" :model="form" label-position="top">
|
||||
<el-form-item v-if="form.from === 'local'">
|
||||
<template #label>
|
||||
<div class="conn-label">
|
||||
<span>{{ $t('database.containerConn') }}</span>
|
||||
<el-button link @click="copyConnURL(true)" icon="DocumentCopy">
|
||||
{{ $t('database.copyConnURL') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-card class="mini-border-card">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('database.connAddress')">
|
||||
<el-tooltip
|
||||
v-if="loadMongoInfo(true).length > 48"
|
||||
:content="loadMongoInfo(true)"
|
||||
placement="top"
|
||||
>
|
||||
{{ loadMongoInfo(true).substring(0, 48) }}...
|
||||
</el-tooltip>
|
||||
<span v-else>
|
||||
{{ loadMongoInfo(true) }}
|
||||
</span>
|
||||
<CopyButton :content="loadMongoInfo(true)" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.port')">
|
||||
27017
|
||||
<CopyButton content="27017" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<span class="input-help">
|
||||
{{ $t('database.containerConnHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="conn-label">
|
||||
<span>{{ $t('database.remoteConn') }}</span>
|
||||
<el-button link @click="copyConnURL(false)" icon="DocumentCopy">
|
||||
{{ $t('database.copyConnURL') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-card class="mini-border-card">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="$t('database.connAddress')">
|
||||
<el-tooltip
|
||||
v-if="loadMongoInfo(false).length > 48"
|
||||
:content="loadMongoInfo(false)"
|
||||
placement="top"
|
||||
>
|
||||
{{ loadMongoInfo(false).substring(0, 48) }}...
|
||||
</el-tooltip>
|
||||
<span v-else>
|
||||
{{ loadMongoInfo(false) }}
|
||||
</span>
|
||||
<CopyButton :content="loadMongoInfo(false)" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('commons.table.port')">
|
||||
{{ form.port }}
|
||||
<CopyButton :content="form.port + ''" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<span v-if="form.from === 'local'" class="input-help">
|
||||
{{ $t('database.remoteConnHelper2') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider border-style="dashed" />
|
||||
|
||||
<el-form-item :label="$t('commons.login.username')">
|
||||
<el-tag>{{ form.username || '-' }}</el-tag>
|
||||
<CopyButton v-if="form.username" :content="form.username" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="form.from === 'local' ? $t('database.rootPassword') : $t('commons.login.password')">
|
||||
<el-tag>{{ form.password || '-' }}</el-tag>
|
||||
<CopyButton v-if="form.password" :content="form.password" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="dialogVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { getDatabase } from '@/api/modules/database';
|
||||
import { getAppConnInfo } from '@/api/modules/app';
|
||||
import { getAgentSettingInfo } from '@/api/modules/setting';
|
||||
import { copyText } from '@/utils/clipboard';
|
||||
import i18n from '@/lang';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
from: '',
|
||||
status: '',
|
||||
systemIP: '',
|
||||
password: '',
|
||||
serviceName: '',
|
||||
containerName: '',
|
||||
remoteIP: '',
|
||||
port: 0,
|
||||
type: '',
|
||||
database: '',
|
||||
username: '',
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
from: string;
|
||||
type: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
form.from = params.from;
|
||||
form.type = params.type;
|
||||
form.database = params.database;
|
||||
await loadConnInfo();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const loadSystemIP = async () => {
|
||||
const res = await getAgentSettingInfo();
|
||||
form.systemIP = res.data.systemIP || globalStore.currentNodeAddr || i18n.global.t('database.localIP');
|
||||
};
|
||||
|
||||
const loadConnInfo = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadSystemIP();
|
||||
if (form.from === 'local') {
|
||||
const res = await getAppConnInfo(form.type, form.database);
|
||||
form.status = res.data.status || '';
|
||||
form.username = res.data.username || '';
|
||||
form.password = res.data.password || '';
|
||||
form.port = res.data.port || 27017;
|
||||
form.serviceName = res.data.serviceName || '';
|
||||
form.containerName = res.data.containerName || '';
|
||||
form.remoteIP = form.systemIP;
|
||||
return;
|
||||
}
|
||||
const res = await getDatabase(form.database);
|
||||
form.status = '';
|
||||
form.username = res.data.username || '';
|
||||
form.password = res.data.password || '';
|
||||
form.port = res.data.port || 27017;
|
||||
form.serviceName = '';
|
||||
form.containerName = '';
|
||||
form.remoteIP = res.data.address || '';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMongoInfo = (isContainer: boolean) => {
|
||||
if (isContainer) {
|
||||
return form.from === 'local' ? form.containerName : form.systemIP;
|
||||
}
|
||||
return form.from === 'local' ? form.systemIP : form.remoteIP;
|
||||
};
|
||||
|
||||
const copyConnURL = (isContainer: boolean) => {
|
||||
const host = loadMongoInfo(isContainer);
|
||||
const port = isContainer && form.from === 'local' ? 27017 : form.port;
|
||||
const user = form.username || '<username>';
|
||||
const encodedPassword = encodeURIComponent(form.password || '<password>');
|
||||
copyText(`mongodb://${user}:${encodedPassword}@${host}:${port}/admin?authSource=admin`);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conn-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
96
frontend/src/views/database/mongodb/delete/index.vue
Normal file
96
frontend/src/views/database/mongodb/delete/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<DialogPro v-model="open" :title="$t('commons.button.delete') + ' - ' + dbName" size="small">
|
||||
<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">
|
||||
{{ $t('database.deleteBackupHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div>
|
||||
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
|
||||
<span style="font-size: 12px; color: red; font-weight: 500">{{ dbName }}</span>
|
||||
<span style="font-size: 12px">{{ $t('database.deleteHelper') }}</span>
|
||||
</div>
|
||||
<el-input v-model="deleteInfo" :placeholder="dbName"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="open = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="deleteInfo != dbName || loading" type="primary" @click="submit">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { deleteMongodbDB } from '@/api/modules/database';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
let deleteReq = ref({
|
||||
id: 0,
|
||||
type: '',
|
||||
database: '',
|
||||
deleteBackup: false,
|
||||
forceDelete: false,
|
||||
});
|
||||
let open = ref(false);
|
||||
let loading = ref(false);
|
||||
let deleteInfo = ref('');
|
||||
let dbName = ref('');
|
||||
|
||||
const deleteForm = ref<FormInstance>();
|
||||
|
||||
interface DialogProps {
|
||||
id: number;
|
||||
type: string;
|
||||
name: string;
|
||||
database: string;
|
||||
}
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const acceptParams = async (prop: DialogProps) => {
|
||||
deleteReq.value = {
|
||||
id: prop.id,
|
||||
type: prop.type,
|
||||
database: prop.database,
|
||||
deleteBackup: false,
|
||||
forceDelete: false,
|
||||
};
|
||||
dbName.value = prop.name;
|
||||
deleteInfo.value = '';
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
deleteMongodbDB(deleteReq.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
open.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
831
frontend/src/views/database/mongodb/index.vue
Normal file
831
frontend/src/views/database/mongodb/index.vue
Normal file
@@ -0,0 +1,831 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="app-status mt-5" v-if="currentDB?.from === 'remote'">
|
||||
<el-card>
|
||||
<div class="flex w-full flex-col gap-4 md:flex-row">
|
||||
<div class="flex flex-wrap gap-4 ml-3">
|
||||
<el-tag class="float-left" effect="dark" type="success">MongoDB</el-tag>
|
||||
<el-tag>{{ $t('app.version') }}: {{ currentDB?.version }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<LayoutContent title="MongoDB">
|
||||
<template #app v-if="currentDB?.from === 'local'">
|
||||
<AppStatus
|
||||
ref="appStatusRef"
|
||||
app-key="mongodb"
|
||||
:app-name="appName"
|
||||
:hide-setting="true"
|
||||
v-model:loading="loading"
|
||||
v-model:mask-show="maskShow"
|
||||
@is-exist="checkExist"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #leftToolBar>
|
||||
<el-button
|
||||
v-if="currentDB && (currentDB.from !== 'local' || mongodbStatus === 'Running')"
|
||||
type="primary"
|
||||
@click="openCreateDrawer"
|
||||
>
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
<el-button v-if="currentDB" type="primary" plain @click="onLoadConn">
|
||||
{{ $t('database.databaseConnInfo') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="currentDB && (currentDB.from !== 'local' || mongodbStatus === 'Running')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="onLoadFromRemote"
|
||||
>
|
||||
{{ $t('database.loadFromRemote') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="goRemoteDB">
|
||||
{{ $t('database.remoteDB') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!currentDB || currentDB.from !== 'local' || mongodbStatus !== 'Running'"
|
||||
@click="goTerminal"
|
||||
>
|
||||
{{ $t('menu.terminal') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain :disabled="currentDB?.from !== 'local'" @click="goDashboard">
|
||||
{{ $t('database.manage') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #rightToolBar>
|
||||
<el-select
|
||||
v-if="currentDB"
|
||||
v-model="currentDBName"
|
||||
@change="changeDatabase"
|
||||
class="p-w-200"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<template #prefix>{{ $t('commons.table.type') }}</template>
|
||||
<el-option-group :label="$t('commons.table.local')">
|
||||
<div v-for="(item, index) in dbOptionsLocal" :key="index">
|
||||
<el-option v-if="item.from === 'local'" :value="item.database" class="optionClass">
|
||||
<span v-if="item.database.length < 25">{{ item.database }}</span>
|
||||
<el-tooltip v-else :content="item.database" placement="top">
|
||||
<span>{{ item.database.substring(0, 25) }}...</span>
|
||||
</el-tooltip>
|
||||
</el-option>
|
||||
</div>
|
||||
<el-button link type="primary" class="jumpAdd" @click="goRouter('app')" icon="Position">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-button>
|
||||
</el-option-group>
|
||||
<el-option-group :label="$t('database.remote')">
|
||||
<div v-for="(item, index) in dbOptionsRemote" :key="index">
|
||||
<el-option v-if="item.from === 'remote'" :value="item.database" class="optionClass">
|
||||
<span v-if="item.database.length < 25">{{ item.database }}</span>
|
||||
<el-tooltip v-else :content="item.database" placement="top">
|
||||
<span>{{ item.database.substring(0, 25) }}...</span>
|
||||
</el-tooltip>
|
||||
</el-option>
|
||||
</div>
|
||||
<el-button link type="primary" class="jumpAdd" @click="goRouter('remote')" icon="Position">
|
||||
{{ $t('database.createRemoteDB') }}
|
||||
</el-button>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
</template>
|
||||
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
v-if="currentDB"
|
||||
:class="{ mask: maskShow }"
|
||||
:pagination-config="paginationConfig"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="tableRows"
|
||||
:heightDiff="370"
|
||||
>
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="180" sortable>
|
||||
<template #default="{ row }">
|
||||
<Tooltip v-if="!row.isDelete" :islink="false" :text="row.name" />
|
||||
<div v-else>
|
||||
<span>{{ row.name }}</span>
|
||||
<el-tag round type="info" class="ml-1" size="small">
|
||||
{{ $t('database.isDelete') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.login.username')"
|
||||
prop="username"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center" v-if="row.username">
|
||||
<span>
|
||||
{{ row.username }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button
|
||||
:disabled="row.isDelete"
|
||||
style="margin-left: -3px"
|
||||
type="primary"
|
||||
link
|
||||
@click="onBind(row)"
|
||||
>
|
||||
{{ $t('database.userBind') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.username === ''">-</span>
|
||||
<div v-else-if="row.password" class="flex items-center flex-wrap">
|
||||
<div class="star-center" v-if="!row.showPassword">
|
||||
<span>**********</span>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="row.showPassword">
|
||||
{{ row.password }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = true"
|
||||
icon="View"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<el-button
|
||||
v-if="row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = false"
|
||||
icon="Hide"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<div>
|
||||
<CopyButton :content="row.password" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button
|
||||
:disabled="row.isDelete"
|
||||
style="margin-left: -3px"
|
||||
link
|
||||
type="primary"
|
||||
@click="onChangePassword(row)"
|
||||
>
|
||||
{{ $t('database.passwordHelper') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.description')"
|
||||
prop="description"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<fu-input-rw-switch
|
||||
v-model="row.description"
|
||||
@enter="onChange(row)"
|
||||
@blur="onChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.date')"
|
||||
prop="createdAt"
|
||||
min-width="200"
|
||||
sortable
|
||||
:formatter="dateFormat"
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="mobile ? 0 : 10"
|
||||
:min-width="mobile ? 'auto' : 300"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
<div v-if="isLoaded && dbOptionsLocal.length === 0 && dbOptionsRemote.length === 0" class="app-warn">
|
||||
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
|
||||
<span>{{ $t('app.checkInstalledWarn', ['MongoDB']) }}</span>
|
||||
<span @click="goRouter('app')" class="flex items-center justify-center gap-0.5">
|
||||
<el-icon><Position /></el-icon>
|
||||
{{ $t('database.goInstall') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/images/no_app.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<el-card
|
||||
v-if="mongodbStatus !== 'Running' && currentDB && !loading && maskShow && currentDB.from === 'local'"
|
||||
class="mask-prompt"
|
||||
>
|
||||
<span>{{ $t('commons.service.serviceNotStarted', ['MongoDB']) }}</span>
|
||||
</el-card>
|
||||
|
||||
<DrawerPro
|
||||
v-model="createVisible"
|
||||
:header="$t('commons.button.create')"
|
||||
@close="handleCreateClose"
|
||||
size="normal"
|
||||
>
|
||||
<el-form ref="formRef" v-loading="submitLoading" :model="createForm" :rules="rules" label-position="top">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model.trim="createForm.name" clearable @input="handleNameInput" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.username')" prop="username">
|
||||
<el-input v-model.trim="createForm.username" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input v-model.trim="createForm.password" type="password" clearable show-password>
|
||||
<template #append>
|
||||
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.permission')" prop="permission">
|
||||
<el-select v-model="createForm.permission" class="w-full">
|
||||
<el-option
|
||||
v-for="item in permissionOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input
|
||||
v-model.trim="createForm.description"
|
||||
type="textarea"
|
||||
clearable
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="submitLoading" @click="createVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="submitLoading" type="primary" @click="submitCreate(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
|
||||
<DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
|
||||
<div class="flex justify-center items-center gap-2 flex-wrap">
|
||||
{{ $t('app.checkInstalledWarn', [dashboardName]) }}
|
||||
<el-link icon="Position" type="primary" @click="getAppDetail">
|
||||
{{ $t('database.goInstall') }}
|
||||
</el-link>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<Conn ref="connRef" />
|
||||
<BindDialog ref="bindRef" @search="search" />
|
||||
<UploadDialog ref="uploadRef" />
|
||||
<Backups ref="dialogBackupRef" />
|
||||
<PasswordDialog ref="passwordRef" @search="search" />
|
||||
<PrivilegesDialog ref="privilegesRef" @search="search" />
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
<AppResources ref="checkRef" />
|
||||
<DeleteDialog ref="deleteRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { getRandomStr } from '@/utils/id';
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElForm, ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { App } from '@/api/interface/app';
|
||||
import { Database } from '@/api/interface/database';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import TerminalDialog from '@/components/terminal/database.vue';
|
||||
import UploadDialog from '@/components/upload/index.vue';
|
||||
import BindDialog from '@/views/database/mongodb/bind/index.vue';
|
||||
import Conn from '@/views/database/mongodb/conn/index.vue';
|
||||
import DeleteDialog from '@/views/database/mongodb/delete/index.vue';
|
||||
import PasswordDialog from '@/views/database/mongodb/password/index.vue';
|
||||
import PrivilegesDialog from '@/views/database/mongodb/permission/index.vue';
|
||||
import AppResources from '@/views/database/postgresql/check/index.vue';
|
||||
import { getAppConnInfo, getAppPort } from '@/api/modules/app';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import {
|
||||
addMongodbDB,
|
||||
deleteCheckMongodbDB,
|
||||
listDatabases,
|
||||
loadMongodbFromRemote,
|
||||
searchMongodbDBs,
|
||||
updateMongodbDescription,
|
||||
} from '@/api/modules/database';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { routerToName, routerToNameWithQuery } from '@/utils/router';
|
||||
import { GlobalStore } from '@/store';
|
||||
import Tooltip from '@/components/tooltip/index.vue';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const maskShow = ref(true);
|
||||
const submitLoading = ref(false);
|
||||
const searchName = ref('');
|
||||
const createVisible = ref(false);
|
||||
const appStatusRef = ref();
|
||||
const bindRef = ref();
|
||||
const connRef = ref();
|
||||
const passwordRef = ref();
|
||||
const uploadRef = ref();
|
||||
const checkRef = ref();
|
||||
const deleteRef = ref();
|
||||
const dialogBackupRef = ref();
|
||||
const dialogTerminalRef = ref();
|
||||
const dialogPortJumpRef = ref();
|
||||
const privilegesRef = ref();
|
||||
const mongoExpressPort = ref(0);
|
||||
const dashboardName = ref('mongo-express');
|
||||
const dashboardKey = ref('mongo-express');
|
||||
const mongodbStatus = ref('');
|
||||
const open = ref(false);
|
||||
const appName = ref('');
|
||||
const isLoaded = ref(false);
|
||||
const currentDB = ref<Database.DatabaseOption>();
|
||||
const currentDBName = ref('');
|
||||
const dbOptionsLocal = ref<Array<Database.DatabaseOption>>([]);
|
||||
const dbOptionsRemote = ref<Array<Database.DatabaseOption>>([]);
|
||||
|
||||
const tableRows = ref<Array<Database.MongodbDBInfo>>([]);
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'mongodb-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('mongodb-page-size')) || 20,
|
||||
total: 0,
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
permission: 'readWrite',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.dbName],
|
||||
username: [Rules.requiredInput, Rules.name],
|
||||
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
|
||||
permission: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
const permissionOptions = [
|
||||
{ label: i18n.global.t('database.mongodbPermissionDbOwner'), value: 'dbOwner' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionRead'), value: 'read' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionReadWrite'), value: 'readWrite' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionUserAdmin'), value: 'userAdmin' },
|
||||
];
|
||||
|
||||
const onLoadConn = () => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
connRef.value?.acceptParams({
|
||||
from: currentDB.value.from,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadFromRemote = async () => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm(i18n.global.t('database.loadFromRemoteHelper'), i18n.global.t('commons.msg.infoTitle'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
loading.value = true;
|
||||
await loadMongodbFromRemote({
|
||||
from: currentDB.value.from,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
})
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
await loadData(true);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const goDashboard = () => {
|
||||
if (currentDB.value?.from !== 'local') {
|
||||
return;
|
||||
}
|
||||
if (mongoExpressPort.value === 0) {
|
||||
dashboardName.value = 'mongo-express';
|
||||
dashboardKey.value = 'mongo-express';
|
||||
open.value = true;
|
||||
return;
|
||||
}
|
||||
dialogPortJumpRef.value?.acceptParams({ port: mongoExpressPort.value });
|
||||
};
|
||||
|
||||
const getAppDetail = () => {
|
||||
routerToNameWithQuery('AppAll', { install: dashboardKey.value });
|
||||
};
|
||||
|
||||
const loadMongoExpressPort = async () => {
|
||||
const res = await getAppPort('mongo-express', '');
|
||||
mongoExpressPort.value = res.data;
|
||||
};
|
||||
|
||||
const goRemoteDB = async () => {
|
||||
if (currentDB.value) {
|
||||
globalStore.currentMongodbDB = currentDBName.value;
|
||||
}
|
||||
routerToName('MongoDB-Remote');
|
||||
};
|
||||
|
||||
const goRouter = async (target: string) => {
|
||||
if (target === 'app') {
|
||||
routerToNameWithQuery('AppAll', { install: 'mongodb' });
|
||||
return;
|
||||
}
|
||||
routerToName('MongoDB-Remote');
|
||||
};
|
||||
|
||||
const loadData = async (resetPage = false) => {
|
||||
if (!currentDBName.value) {
|
||||
tableRows.value = [];
|
||||
paginationConfig.total = 0;
|
||||
return;
|
||||
}
|
||||
if (resetPage) {
|
||||
paginationConfig.currentPage = 1;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await searchMongodbDBs({
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
info: searchName.value,
|
||||
database: currentDBName.value,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
});
|
||||
tableRows.value = (res.data.items || []).map((item) => ({
|
||||
...item,
|
||||
showPassword: false,
|
||||
}));
|
||||
paginationConfig.total = res.data.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const search = (column?: { prop?: string; order?: string }) => {
|
||||
if (column) {
|
||||
paginationConfig.orderBy = column.order ? column.prop || 'createdAt' : 'createdAt';
|
||||
paginationConfig.order = column.order || 'null';
|
||||
loadData();
|
||||
return;
|
||||
}
|
||||
loadData(true);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('database.permission'),
|
||||
disabled: (row: Database.MongodbDBInfo) => {
|
||||
return !row.username || row.isDelete;
|
||||
},
|
||||
click: (row: Database.MongodbDBInfo) => {
|
||||
let param = {
|
||||
database: currentDBName.value,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
};
|
||||
privilegesRef.value.acceptParams(param);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('database.backupList'),
|
||||
disabled: (row: Database.MongodbDBInfo) => {
|
||||
return row.isDelete;
|
||||
},
|
||||
click: (row: Database.MongodbDBInfo) => {
|
||||
openBackupList(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('database.loadBackup'),
|
||||
disabled: (row: Database.MongodbDBInfo) => {
|
||||
return row.isDelete;
|
||||
},
|
||||
click: (row: Database.MongodbDBInfo) => {
|
||||
openUploadDialog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Database.MongodbDBInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const checkExist = (data: App.CheckInstalled | boolean) => {
|
||||
if (!data || typeof data === 'boolean') {
|
||||
mongodbStatus.value = '';
|
||||
maskShow.value = false;
|
||||
tableRows.value = [];
|
||||
paginationConfig.total = 0;
|
||||
return;
|
||||
}
|
||||
mongodbStatus.value = data.status || '';
|
||||
if (data.isExist) {
|
||||
loadData(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = async (row: Database.MongodbDBInfo) => {
|
||||
await updateMongodbDescription({ id: row.id, description: row.description });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
};
|
||||
|
||||
const changeDatabase = async () => {
|
||||
for (const item of dbOptionsLocal.value) {
|
||||
if (item.database === currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
appName.value = item.database;
|
||||
globalStore.currentMongodbDB = item.database;
|
||||
await nextTick();
|
||||
appStatusRef.value?.onCheck('mongodb', item.database);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const item of dbOptionsRemote.value) {
|
||||
if (item.database === currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
appName.value = '';
|
||||
mongodbStatus.value = '';
|
||||
maskShow.value = false;
|
||||
globalStore.currentMongodbDB = item.database;
|
||||
await loadData(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadDBOptions = async () => {
|
||||
try {
|
||||
const res = await listDatabases('mongodb');
|
||||
const datas = res.data || [];
|
||||
dbOptionsLocal.value = [];
|
||||
dbOptionsRemote.value = [];
|
||||
currentDB.value = undefined;
|
||||
currentDBName.value = globalStore.currentMongodbDB;
|
||||
for (const item of datas) {
|
||||
if (currentDBName.value && item.database === currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
if (item.from === 'local') {
|
||||
appName.value = item.database;
|
||||
}
|
||||
}
|
||||
if (item.from === 'local') {
|
||||
dbOptionsLocal.value.push(item);
|
||||
} else {
|
||||
dbOptionsRemote.value.push(item);
|
||||
}
|
||||
}
|
||||
if (!currentDB.value && dbOptionsLocal.value.length !== 0) {
|
||||
currentDB.value = dbOptionsLocal.value[0];
|
||||
currentDBName.value = dbOptionsLocal.value[0].database;
|
||||
appName.value = dbOptionsLocal.value[0].database;
|
||||
}
|
||||
if (!currentDB.value && dbOptionsRemote.value.length !== 0) {
|
||||
currentDB.value = dbOptionsRemote.value[0];
|
||||
currentDBName.value = dbOptionsRemote.value[0].database;
|
||||
appName.value = '';
|
||||
}
|
||||
if (currentDB.value) {
|
||||
globalStore.currentMongodbDB = currentDBName.value;
|
||||
if (currentDB.value.from === 'remote') {
|
||||
maskShow.value = false;
|
||||
await loadData(true);
|
||||
} else {
|
||||
await nextTick();
|
||||
appStatusRef.value?.onCheck('mongodb', currentDBName.value);
|
||||
}
|
||||
} else {
|
||||
maskShow.value = false;
|
||||
tableRows.value = [];
|
||||
paginationConfig.total = 0;
|
||||
}
|
||||
} finally {
|
||||
isLoaded.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const openBackupList = (row: { name: string }) => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
const params: {
|
||||
type: string;
|
||||
name: string;
|
||||
detailName: string;
|
||||
status?: string;
|
||||
} = {
|
||||
type: 'mongodb',
|
||||
name: currentDBName.value,
|
||||
detailName: row.name,
|
||||
};
|
||||
if (currentDB.value.from === 'local') {
|
||||
params.status = mongodbStatus.value;
|
||||
}
|
||||
dialogBackupRef.value?.acceptParams(params);
|
||||
};
|
||||
|
||||
const openUploadDialog = (row: { name: string }) => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
uploadRef.value?.acceptParams({
|
||||
type: 'mongodb',
|
||||
name: currentDBName.value,
|
||||
detailName: row.name,
|
||||
remark: '.gz',
|
||||
});
|
||||
};
|
||||
|
||||
const onBind = async (row: Database.MongodbDBInfo) => {
|
||||
bindRef.value.acceptParams({
|
||||
database: currentDBName.value,
|
||||
name: row.name,
|
||||
});
|
||||
};
|
||||
|
||||
const onChangePassword = async (row: Database.MongodbDBInfo) => {
|
||||
passwordRef.value.acceptParams({
|
||||
database: currentDBName.value,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
});
|
||||
};
|
||||
|
||||
const shellQuote = (value: string) => {
|
||||
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
||||
};
|
||||
|
||||
const buildMongoTerminalCommand = (username: string, password: string) => {
|
||||
return `mongosh "mongodb://127.0.0.1:27017/admin?authSource=admin" --username ${shellQuote(
|
||||
username,
|
||||
)} --password ${shellQuote(password)}\r`;
|
||||
};
|
||||
|
||||
const goTerminal = async () => {
|
||||
if (!currentDB.value || currentDB.value.from !== 'local' || mongodbStatus.value !== 'Running') {
|
||||
return;
|
||||
}
|
||||
const res = await getAppConnInfo('mongodb', currentDBName.value);
|
||||
const connInfo = res.data;
|
||||
if (!connInfo?.containerName) {
|
||||
return;
|
||||
}
|
||||
dialogTerminalRef.value?.acceptParams({
|
||||
containerID: connInfo.containerName,
|
||||
command: '/bin/sh',
|
||||
initCmd: buildMongoTerminalCommand(connInfo.username || 'root', connInfo.password || ''),
|
||||
waitForPrompt: 'admin> ',
|
||||
resourceName: currentDBName.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateDrawer = () => {
|
||||
createForm.name = '';
|
||||
createForm.username = '';
|
||||
random();
|
||||
createForm.permission = 'readWrite';
|
||||
createForm.description = '';
|
||||
createVisible.value = true;
|
||||
};
|
||||
|
||||
const handleCreateClose = () => {
|
||||
createVisible.value = false;
|
||||
};
|
||||
|
||||
const handleNameInput = () => {
|
||||
createForm.username = createForm.name;
|
||||
};
|
||||
|
||||
const random = () => {
|
||||
createForm.password = getRandomStr(16);
|
||||
};
|
||||
|
||||
const submitCreate = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !currentDB.value) {
|
||||
return;
|
||||
}
|
||||
const valid = await formEl.validate().catch(() => false);
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
await addMongodbDB({
|
||||
name: createForm.name,
|
||||
from: currentDB.value.from,
|
||||
database: currentDBName.value,
|
||||
username: createForm.username,
|
||||
password: createForm.password,
|
||||
permission: createForm.permission,
|
||||
description: createForm.description,
|
||||
})
|
||||
.then(async () => {
|
||||
createVisible.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (row: Database.MongodbDBInfo) => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
const res = await deleteCheckMongodbDB({
|
||||
id: row.id,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
});
|
||||
if (res.data && res.data.length > 0) {
|
||||
checkRef.value.acceptParams({ items: res.data });
|
||||
} else {
|
||||
deleteRef.value.acceptParams({
|
||||
id: row.id,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
name: row.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMongoExpressPort();
|
||||
loadDBOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jumpAdd {
|
||||
margin-top: 10px;
|
||||
margin-left: 15px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.optionClass {
|
||||
min-width: 350px;
|
||||
}
|
||||
</style>
|
||||
98
frontend/src/views/database/mongodb/password/index.vue
Normal file
98
frontend/src/views/database/mongodb/password/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div>
|
||||
<DrawerPro
|
||||
v-model="changeVisible"
|
||||
:header="$t('database.changePassword')"
|
||||
:resource="form.name"
|
||||
@close="handleClose"
|
||||
size="small"
|
||||
>
|
||||
<el-form v-loading="loading" ref="changeFormRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item :label="$t('commons.login.username')">
|
||||
<el-input disabled v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input type="password" clearable show-password v-model="form.password" />
|
||||
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="changeVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="onSubmit(changeFormRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { updateMongodbPassword } from '@/api/modules/database';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref();
|
||||
const changeVisible = ref(false);
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const changeFormRef = ref<FormInstance>();
|
||||
const form = reactive({
|
||||
database: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
database: string;
|
||||
name: string;
|
||||
username: string;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
form.database = params.database;
|
||||
form.name = params.name;
|
||||
form.username = params.username;
|
||||
form.password = '';
|
||||
changeVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
changeVisible.value = false;
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
await updateMongodbPassword({
|
||||
database: form.database,
|
||||
name: form.name,
|
||||
password: form.password,
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
changeVisible.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
113
frontend/src/views/database/mongodb/permission/index.vue
Normal file
113
frontend/src/views/database/mongodb/permission/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<DrawerPro
|
||||
v-model="changeVisible"
|
||||
:header="$t('database.permission')"
|
||||
:resource="form.name"
|
||||
@close="handleClose"
|
||||
size="small"
|
||||
>
|
||||
<el-form v-loading="loading" :model="form" label-position="top">
|
||||
<el-form-item :label="$t('database.userBind')">
|
||||
<el-tag>
|
||||
{{ form.username }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.permission')" prop="permission">
|
||||
<el-select v-model="form.permission" class="w-full">
|
||||
<el-option
|
||||
v-for="item in permissionOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="loading" @click="changeVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading || !form.permission" type="primary" @click="onSubmit()">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { changeMongodbPrivileges, loadMongodbPrivileges } from '@/api/modules/database';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref();
|
||||
const changeVisible = ref(false);
|
||||
const form = reactive({
|
||||
database: '',
|
||||
name: '',
|
||||
username: '',
|
||||
permission: '',
|
||||
});
|
||||
|
||||
const permissionOptions = [
|
||||
{ label: i18n.global.t('database.mongodbPermissionDbOwner'), value: 'dbOwner' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionRead'), value: 'read' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionReadWrite'), value: 'readWrite' },
|
||||
{ label: i18n.global.t('database.mongodbPermissionUserAdmin'), value: 'userAdmin' },
|
||||
];
|
||||
|
||||
interface DialogProps {
|
||||
database: string;
|
||||
name: string;
|
||||
username: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
form.database = params.database;
|
||||
form.name = params.name;
|
||||
form.username = params.username;
|
||||
form.permission = '';
|
||||
changeVisible.value = true;
|
||||
loading.value = true;
|
||||
await loadMongodbPrivileges({
|
||||
database: form.database,
|
||||
name: form.name,
|
||||
username: form.username,
|
||||
})
|
||||
.then((res) => {
|
||||
form.permission = res.data || '';
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
changeVisible.value = false;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const param = {
|
||||
database: form.database,
|
||||
name: form.name,
|
||||
username: form.username,
|
||||
permission: form.permission,
|
||||
};
|
||||
loading.value = true;
|
||||
await changeMongodbPrivileges(param)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
changeVisible.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
84
frontend/src/views/database/mongodb/remote/delete/index.vue
Normal file
84
frontend/src/views/database/mongodb/remote/delete/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<DialogPro v-model="open" :title="$t('database.unBindRemoteDB') + ' - ' + deleteReq.database" size="small">
|
||||
<el-form ref="deleteForm" v-loading="loading" @submit.prevent>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('database.unBindForce')" />
|
||||
<span class="input-help">
|
||||
{{ $t('database.unBindForceHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="deleteReq.deleteBackup" :label="$t('app.deleteBackup')" />
|
||||
<span class="input-help">
|
||||
{{ $t('database.deleteBackupHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<span style="font-size: 12px">{{ $t('database.unBindRemoteHelper') }}</span>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="open = false" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { deleteDatabase } from '@/api/modules/database';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const deleteReq = ref({
|
||||
id: 0,
|
||||
database: '',
|
||||
deleteBackup: false,
|
||||
forceDelete: false,
|
||||
});
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const deleteForm = ref<FormInstance>();
|
||||
|
||||
interface DialogProps {
|
||||
id: number;
|
||||
database: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const acceptParams = async (prop: DialogProps) => {
|
||||
deleteReq.value = {
|
||||
id: prop.id,
|
||||
database: prop.database,
|
||||
deleteBackup: false,
|
||||
forceDelete: false,
|
||||
};
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
deleteDatabase(deleteReq.value)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
emit('search');
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
open.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
166
frontend/src/views/database/mongodb/remote/index.vue
Normal file
166
frontend/src/views/database/mongodb/remote/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<LayoutContent :title="$t('database.remoteDB', 2)" backName="MongoDB">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onOpenDialog('create')">
|
||||
{{ $t('database.createRemoteDB') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
|
||||
<el-table-column show-overflow-tooltip :label="$t('commons.table.name')" prop="name" sortable />
|
||||
<el-table-column :label="$t('database.version')" prop="version" />
|
||||
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
|
||||
<el-table-column :label="$t('commons.login.username')" prop="username" />
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center flex-wrap">
|
||||
<div class="star-center">
|
||||
<span v-if="!row.showPassword">**********</span>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="row.showPassword">
|
||||
{{ row.password }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = true"
|
||||
icon="View"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<el-button
|
||||
v-if="row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = false"
|
||||
icon="Hide"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<div>
|
||||
<CopyButton :content="row.password" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="description"
|
||||
:label="$t('commons.table.description')"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
width="170px"
|
||||
:buttons="buttons"
|
||||
:ellipsis="10"
|
||||
:label="$t('commons.table.operate')"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<AppResources ref="checkRef"></AppResources>
|
||||
<OperateDialog ref="dialogRef" @search="search" />
|
||||
<DeleteDialog ref="deleteRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
|
||||
import AppResources from '@/views/database/postgresql/check/index.vue';
|
||||
import OperateDialog from '@/views/database/mongodb/remote/operate/index.vue';
|
||||
import DeleteDialog from '@/views/database/mongodb/remote/delete/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { Database } from '@/api/interface/database';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const dialogRef = ref();
|
||||
const checkRef = ref();
|
||||
const deleteRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'mongodb-remote-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('mongodb-remote-page-size')) || 20,
|
||||
total: 0,
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
const params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
info: searchName.value,
|
||||
type: 'mongodb',
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
};
|
||||
const res = await searchDatabases(params);
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Database.DatabaseInfo> = {
|
||||
name: '',
|
||||
type: 'mongodb',
|
||||
from: 'remote',
|
||||
version: '8.x',
|
||||
address: '',
|
||||
port: 27017,
|
||||
initialDB: 'admin',
|
||||
username: 'root',
|
||||
password: '',
|
||||
timeout: 30,
|
||||
description: '',
|
||||
},
|
||||
) => {
|
||||
dialogRef.value!.acceptParams({
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (row: Database.DatabaseInfo) => {
|
||||
const res = await deleteCheckDatabase(row.id);
|
||||
if (res.data && res.data.length > 0) {
|
||||
checkRef.value.acceptParams({ items: res.data });
|
||||
} else {
|
||||
deleteRef.value.acceptParams({
|
||||
id: row.id,
|
||||
database: row.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.unbind'),
|
||||
click: (row: Database.DatabaseInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
188
frontend/src/views/database/mongodb/remote/operate/index.vue
Normal file
188
frontend/src/views/database/mongodb/remote/operate/index.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="drawerVisible"
|
||||
:header="title"
|
||||
@close="handleClose"
|
||||
:resource="dialogData.title === 'create' ? '' : dialogData.rowData?.name"
|
||||
size="large"
|
||||
>
|
||||
<el-form ref="formRef" v-loading="loading" label-position="top" :model="dialogData.rowData" :rules="rules">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-if="dialogData.title === 'create'" clearable v-model.trim="dialogData.rowData!.name" />
|
||||
<el-tag v-else>{{ dialogData.rowData!.name }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.version')" prop="version">
|
||||
<el-radio-group v-model="dialogData.rowData!.version" @change="isOK = false">
|
||||
<el-radio label="8.x" value="8.x" />
|
||||
<el-radio label="7.x" value="7.x" />
|
||||
<el-radio label="6.x" value="6.x" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.address')" prop="address">
|
||||
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.address" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.port')" prop="port">
|
||||
<el-input @change="isOK = false" clearable v-model.number="dialogData.rowData!.port" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.initialDB')" prop="initialDB">
|
||||
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.initialDB" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.username')" prop="username">
|
||||
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.username" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input
|
||||
@change="isOK = false"
|
||||
type="password"
|
||||
clearable
|
||||
show-password
|
||||
v-model.trim="dialogData.rowData!.password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.timeout')" prop="timeout">
|
||||
<el-input-number
|
||||
class="p-w-200"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
step-strictly
|
||||
:step="1"
|
||||
v-model.number="dialogData.rowData!.timeout"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input clearable v-model.trim="dialogData.rowData!.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button @click="onSubmit(formRef, 'check')">
|
||||
{{ $t('terminal.testConn') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="!isOK" @click="onSubmit(formRef, dialogData.title)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { Database } from '@/api/interface/database';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { checkNumberRange, Rules } from '@/global/form-rules';
|
||||
import { addDatabase, checkDatabase, editDatabase } from '@/api/modules/database';
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
rowData?: Database.DatabaseInfo;
|
||||
getTableList?: () => Promise<any>;
|
||||
}
|
||||
|
||||
const title = ref<string>('');
|
||||
const drawerVisible = ref(false);
|
||||
const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const isOK = ref(false);
|
||||
const loading = ref();
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value = params;
|
||||
isOK.value = false;
|
||||
dialogData.value.rowData.type = 'mongodb';
|
||||
dialogData.value.rowData.from = 'remote';
|
||||
if (!dialogData.value.rowData.initialDB) {
|
||||
dialogData.value.rowData.initialDB = 'admin';
|
||||
}
|
||||
if (dialogData.value.rowData.version.startsWith('6.')) {
|
||||
dialogData.value.rowData.version = '6.x';
|
||||
}
|
||||
if (dialogData.value.rowData.version.startsWith('7.')) {
|
||||
dialogData.value.rowData.version = '7.x';
|
||||
}
|
||||
if (dialogData.value.rowData.version.startsWith('8.')) {
|
||||
dialogData.value.rowData.version = '8.x';
|
||||
}
|
||||
title.value = i18n.global.t('database.' + dialogData.value.title + 'RemoteDB');
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.simpleName, Rules.noSpace],
|
||||
version: [Rules.requiredSelect],
|
||||
address: [Rules.ipV4V6OrDomain],
|
||||
port: [Rules.port],
|
||||
username: [Rules.requiredInput],
|
||||
password: [Rules.requiredInput],
|
||||
timeout: [Rules.number, checkNumberRange(1, 600)],
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined, operation: string) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
dialogData.value.rowData.type = 'mongodb';
|
||||
dialogData.value.rowData.from = 'remote';
|
||||
loading.value = true;
|
||||
|
||||
if (operation === 'check') {
|
||||
await checkDatabase(dialogData.value.rowData)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.data) {
|
||||
isOK.value = true;
|
||||
MsgSuccess(i18n.global.t('terminal.connTestOk'));
|
||||
} else {
|
||||
MsgError(i18n.global.t('terminal.connTestFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
MsgError(i18n.global.t('terminal.connTestFailed'));
|
||||
});
|
||||
}
|
||||
|
||||
if (operation === 'create') {
|
||||
await addDatabase(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
if (operation === 'edit') {
|
||||
await editDatabase(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
@@ -256,8 +256,8 @@
|
||||
baseInfo.prettyDistro
|
||||
? baseInfo.prettyDistro
|
||||
: baseInfo.platformVersion
|
||||
? baseInfo.platform + '-' + baseInfo.platformVersion
|
||||
: baseInfo.platform
|
||||
? baseInfo.platform + '-' + baseInfo.platformVersion
|
||||
: baseInfo.platform
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
@@ -830,8 +830,8 @@ const handleCopy = () => {
|
||||
(baseInfo.value.prettyDistro
|
||||
? baseInfo.value.prettyDistro
|
||||
: baseInfo.value.platformVersion
|
||||
? baseInfo.value.platform + '-' + baseInfo.value.platformVersion
|
||||
: baseInfo.value.platform) +
|
||||
? baseInfo.value.platform + '-' + baseInfo.value.platformVersion
|
||||
: baseInfo.value.platform) +
|
||||
'\n' +
|
||||
i18n.global.t('home.kernelVersion') +
|
||||
': ' +
|
||||
|
||||
Reference in New Issue
Block a user