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:
王贺
2026-04-14 18:40:44 +08:00
committed by GitHub
parent 8362578355
commit 394339d7dc
52 changed files with 4338 additions and 31 deletions

View File

@@ -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)

View 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)
}

View File

@@ -31,6 +31,7 @@ var (
dbCommonService = service.NewIDBCommonService()
mysqlService = service.NewIMysqlService()
postgresqlService = service.NewIPostgresqlService()
mongodbService = service.NewIMongodbService()
databaseService = service.NewIDatabaseService()
redisService = service.NewIRedisService()

View File

@@ -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}
}

View File

@@ -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"`

View 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"`

View 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"`
}

View 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)
}
}

View File

@@ -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

View 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)
}
}

View File

@@ -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))

View File

@@ -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{

View File

@@ -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",

View 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")
}

View File

@@ -21,6 +21,7 @@ var (
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo()
imageRepoRepo = repo.NewIImageRepoRepo()

View 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
}

View File

@@ -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

View File

@@ -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=

View File

@@ -57,6 +57,7 @@ func InitAgentDB() {
migrations.UpdateDatabase,
migrations.AddGPUMonitor,
migrations.UpdateDatabaseMysql,
migrations.AddDatabaseMongodb,
migrations.InitIptablesStatus,
migrations.UpdateWebsite,
migrations.AddisIPtoWebsiteSSL,

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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;

View File

@@ -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 });
};

View File

@@ -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,
});
};

View File

@@ -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,

View File

@@ -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>();

View File

@@ -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',

View File

@@ -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',

View File

@@ -549,6 +549,10 @@ const message = {
version: 'データベースバージョン',
userHelper:
'ルートユーザーまたはルート特権を持つデータベースユーザーはリモートデータベースにアクセスできます',
mongodbPermissionDbOwner: 'データベース所有者',
mongodbPermissionRead: 'データの読み取り',
mongodbPermissionReadWrite: 'データの読み取りと書き込み',
mongodbPermissionUserAdmin: 'ユーザー管理者',
pgUserHelper: 'スーパーユーザーの特権を持つデータベースユーザー',
ssl: 'SSLを使用します',
clientKey: 'クライアントの秘密鍵',

View File

@@ -542,6 +542,10 @@ const message = {
address: '데이터베이스 주소',
version: '데이터베이스 버전',
userHelper: '루트 사용자 또는 루트 권한을 가진 데이터베이스 사용자가 원격 데이터베이스에 접근할 있습니다.',
mongodbPermissionDbOwner: '데이터베이스 소유자',
mongodbPermissionRead: '데이터 읽기',
mongodbPermissionReadWrite: '데이터 읽기 쓰기',
mongodbPermissionUserAdmin: '사용자 관리자',
pgUserHelper: '슈퍼 사용자 권한을 가진 데이터베이스 사용자.',
ssl: 'SSL 사용',
clientKey: '클라이언트 개인 ',

View File

@@ -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',

View File

@@ -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',

View File

@@ -543,6 +543,10 @@ const message = {
version: 'Версия базы данных',
userHelper:
'Пользователь root или пользователь базы данных с привилегиями root может получить доступ к удаленной базе данных.',
mongodbPermissionDbOwner: 'Владелец базы данных',
mongodbPermissionRead: 'Чтение данных',
mongodbPermissionReadWrite: 'Чтение и запись данных',
mongodbPermissionUserAdmin: 'Администратор пользователей',
pgUserHelper: 'Пользователь базы данных с привилегиями суперпользователя.',
ssl: 'Использовать SSL',
clientKey: 'Приватный ключ клиента',

View File

@@ -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ı',

View File

@@ -521,6 +521,10 @@ const message = {
address: '資料庫位址',
version: '資料庫版本',
userHelper: 'root 使用者或擁有 root 權限的資料庫使用者',
mongodbPermissionDbOwner: '資料庫擁有者',
mongodbPermissionRead: '讀取資料',
mongodbPermissionReadWrite: '讀取和寫入資料',
mongodbPermissionUserAdmin: '使用者管理員',
pgUserHelper: '具有超級管理員權限的資料庫使用者',
ssl: '使用 SSL',
clientKey: '用戶端私鑰',

View File

@@ -510,6 +510,10 @@ const message = {
address: '数据库地址',
version: '数据库版本',
userHelper: 'root 用户或拥有 root 权限的数据库用户',
mongodbPermissionDbOwner: '数据库所有者',
mongodbPermissionRead: '读取数据',
mongodbPermissionReadWrite: '读取和写入数据',
mongodbPermissionUserAdmin: '用户管理员',
pgUserHelper: '拥有超级管理员权限的数据库用户',
ssl: '使用 SSL',
clientKey: '客户端私钥',

View File

@@ -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',
},
},
],
},
],

View File

@@ -47,6 +47,7 @@ export interface GlobalState {
currentDB: string;
currentPgDB: string;
currentRedisDB: string;
currentMongodbDB: string;
showEntranceWarn: boolean;
defaultNetwork: string;
defaultIO: string;

View File

@@ -44,6 +44,7 @@ const GlobalStore = defineStore({
currentDB: '',
currentPgDB: '',
currentRedisDB: '',
currentMongodbDB: '',
showEntranceWarn: true,
defaultNetwork: 'all',
defaultIO: 'all',

View File

@@ -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>

View File

@@ -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) {

View File

@@ -21,5 +21,9 @@ const buttons = [
label: 'Redis',
path: '/databases/redis',
},
{
label: 'MongoDB',
path: '/databases/mongodb',
},
];
</script>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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') +
': ' +