mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
chore: 支持迁移用户
This commit is contained in:
@@ -277,8 +277,8 @@ func (uc *ToolboxMigrationUsecase) run(conn *request.ToolboxMigrationConnection,
|
||||
return
|
||||
}
|
||||
|
||||
// 数据库先于依赖它的项目和网站建立
|
||||
priority := map[string]int{"database": 0, "database_user": 1, "project": 2, "website": 3}
|
||||
// 用户最先建,PG 的 dump 带 OWNER TO,角色不存在会让整个导入失败
|
||||
priority := map[string]int{"database_user": 0, "database": 1, "project": 2, "website": 3}
|
||||
slices.SortStableFunc(items, func(a, b types.MigrationItem) int {
|
||||
return priority[a.Type] - priority[b.Type]
|
||||
})
|
||||
@@ -371,6 +371,12 @@ func (uc *ToolboxMigrationUsecase) pull(
|
||||
}
|
||||
detail.Item = item
|
||||
|
||||
// 用户没有可备份的内容,直接建到目标
|
||||
if item.Type == "database_user" {
|
||||
uc.setStage(item.Key, types.MigrationStageImport)
|
||||
return uc.importDatabaseUser(ctx, detail)
|
||||
}
|
||||
|
||||
// 备份期间停止来源避免文件不一致,备份落盘后立即恢复;数据库为在线导出无需停止
|
||||
stopped := req.StopSource && item.Status == "running" && item.Type != "database"
|
||||
if stopped {
|
||||
|
||||
@@ -71,6 +71,10 @@ func (uc *ToolboxMigrationUsecase) checkConflicts(ctx context.Context, items []t
|
||||
}) {
|
||||
item.Blockers = append(item.Blockers, uc.t.Get("a database with the same name already exists on the target server"))
|
||||
}
|
||||
case "database_user":
|
||||
if !uc.mysqlName.MatchString(item.TargetName) {
|
||||
item.Blockers = append(item.Blockers, uc.t.Get("the name contains characters not allowed by AcePanel"))
|
||||
}
|
||||
case "project":
|
||||
item.TargetPath = filepath.Join(projectPath, item.TargetName)
|
||||
if !uc.resourceName.MatchString(item.TargetName) {
|
||||
@@ -114,7 +118,7 @@ func (uc *ToolboxMigrationUsecase) importDatabase(ctx context.Context, detail *t
|
||||
warnings := make([]string, 0)
|
||||
switch {
|
||||
case database.Username != "" && database.Password != "":
|
||||
create.CreateUser = true
|
||||
create.CreateUser = !uc.databaseUserExists(ctx, server.ID, database.Username)
|
||||
create.Username = database.Username
|
||||
create.Password = database.Password
|
||||
create.Host = lo.CoalesceOrEmpty(database.Host, "localhost")
|
||||
@@ -140,6 +144,39 @@ func (uc *ToolboxMigrationUsecase) importDatabase(ctx context.Context, detail *t
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
// importDatabaseUser 在目标创建数据库用户
|
||||
func (uc *ToolboxMigrationUsecase) importDatabaseUser(ctx context.Context, detail *types.MigrationDetail) ([]string, error) {
|
||||
user := detail.DatabaseUser
|
||||
targetType := user.Type
|
||||
if targetType == "mariadb" {
|
||||
targetType = "mysql"
|
||||
}
|
||||
server, err := uc.databaseServer.GetByName(ctx, "local_"+targetType)
|
||||
if err != nil {
|
||||
return nil, errors.New(uc.t.Get("no compatible database server is installed on the target"))
|
||||
}
|
||||
|
||||
if err = uc.databaseUser.Create(ctx, &request.DatabaseUserCreate{
|
||||
ServerID: server.ID, Username: detail.Item.TargetName,
|
||||
Password: user.Password, Host: user.Host,
|
||||
}); err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to create database user on target: %v", err))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// databaseUserExists 目标该数据库服务上是否已有同名用户
|
||||
func (uc *ToolboxMigrationUsecase) databaseUserExists(ctx context.Context, serverID uint, username string) bool {
|
||||
users, _, err := uc.databaseUser.List(ctx, 1, 10000, "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return slices.ContainsFunc(users, func(user *DatabaseUser) bool {
|
||||
return user.ServerID == serverID && user.Username == username
|
||||
})
|
||||
}
|
||||
|
||||
// importWebsite 创建目标网站、导入文件并还原配置
|
||||
func (uc *ToolboxMigrationUsecase) importWebsite(ctx context.Context, detail *types.MigrationDetail, archive string) ([]string, error) {
|
||||
website := detail.Website
|
||||
|
||||
@@ -93,7 +93,7 @@ func (a *baotaAdapter) Items(ctx context.Context) ([]types.MigrationItem, error)
|
||||
websites[i].DependsOn = append(websites[i].DependsOn, databases[index].Key)
|
||||
}
|
||||
}
|
||||
return slices.Concat(websites, databases, a.projectItems(ctx)), nil
|
||||
return slices.Concat(websites, databases, a.databaseUserItems(ctx), a.projectItems(ctx)), nil
|
||||
}
|
||||
|
||||
// websiteItems 列出静态、PHP 与反代网站
|
||||
@@ -135,6 +135,67 @@ func (a *baotaAdapter) websiteItems(ctx context.Context) ([]types.MigrationItem,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// mysqlSubtype 宝塔的 MySQL 可能实际是 MariaDB,版本文件里能区分
|
||||
func (a *baotaAdapter) mysqlSubtype(ctx context.Context) string {
|
||||
version, err := a.fileContent(ctx, "/www/server/mysql/version.pl")
|
||||
if err == nil && strings.Contains(strings.ToLower(version), "mariadb") {
|
||||
return "mariadb"
|
||||
}
|
||||
|
||||
return "mysql"
|
||||
}
|
||||
|
||||
// databaseUserItems 列出数据库用户
|
||||
func (a *baotaAdapter) databaseUserItems(ctx context.Context) []types.MigrationItem {
|
||||
data, err := a.call(ctx, http.MethodPost, "/data?action=getData", map[string]any{
|
||||
"table": "databases", "p": 1, "limit": 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
subtype := a.mysqlSubtype(ctx)
|
||||
|
||||
items := make([]types.MigrationItem, 0)
|
||||
for _, row := range a.rows(data) {
|
||||
username, password := cast.ToString(row["username"]), cast.ToString(row["password"])
|
||||
if username == "" || password == "" || cast.ToInt(row["db_type"]) != 0 || cast.ToInt(row["sid"]) != 0 {
|
||||
continue
|
||||
}
|
||||
host := lo.CoalesceOrEmpty(cast.ToString(row["accept"]), "localhost")
|
||||
key := biz.MigrationItemKey("database_user", username+"@"+host)
|
||||
// 多个库共用同一用户时只保留一项
|
||||
if slices.ContainsFunc(items, func(item types.MigrationItem) bool { return item.Key == key }) {
|
||||
continue
|
||||
}
|
||||
items = append(items, types.MigrationItem{
|
||||
Key: key, Type: "database_user", Subtype: subtype, Name: username + "@" + host, Status: "running",
|
||||
TargetName: username, SourceID: username, Version: host,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// databaseUserDetail 重新读取用户密码,迁移执行时密码可能已被改过
|
||||
func (a *baotaAdapter) databaseUserDetail(ctx context.Context, item types.MigrationItem) (*types.MigrationDatabaseUser, error) {
|
||||
data, err := a.call(ctx, http.MethodPost, "/data?action=getData", map[string]any{
|
||||
"table": "databases", "p": 1, "limit": 10000, "search": item.SourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, ok := lo.Find(a.rows(data), func(row map[string]any) bool {
|
||||
return cast.ToString(row["username"]) == item.SourceID
|
||||
})
|
||||
if !ok {
|
||||
return nil, errors.New(a.t.Get("resource no longer exists on the source server"))
|
||||
}
|
||||
|
||||
return &types.MigrationDatabaseUser{
|
||||
Type: item.Subtype, Username: item.SourceID,
|
||||
Password: cast.ToString(row["password"]), Host: item.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// databaseItems 列出本机 MySQL 数据库
|
||||
func (a *baotaAdapter) databaseItems(ctx context.Context) []types.MigrationItem {
|
||||
data, err := a.call(ctx, http.MethodPost, "/data?action=getData", map[string]any{
|
||||
@@ -143,12 +204,7 @@ func (a *baotaAdapter) databaseItems(ctx context.Context) []types.MigrationItem
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// 宝塔的 MySQL 可能实际是 MariaDB,版本文件里能区分
|
||||
subtype := "mysql"
|
||||
if version, versionErr := a.fileContent(ctx, "/www/server/mysql/version.pl"); versionErr == nil &&
|
||||
strings.Contains(strings.ToLower(version), "mariadb") {
|
||||
subtype = "mariadb"
|
||||
}
|
||||
subtype := a.mysqlSubtype(ctx)
|
||||
|
||||
items := make([]types.MigrationItem, 0)
|
||||
for _, row := range a.rows(data) {
|
||||
@@ -234,6 +290,8 @@ func (a *baotaAdapter) Detail(ctx context.Context, item types.MigrationItem) (*t
|
||||
detail.Website, err = a.websiteDetail(ctx, item)
|
||||
case "database":
|
||||
detail.Database, err = a.databaseDetail(ctx, item)
|
||||
case "database_user":
|
||||
detail.DatabaseUser, err = a.databaseUserDetail(ctx, item)
|
||||
case "project":
|
||||
detail.Project, err = a.projectDetail(ctx, item)
|
||||
default:
|
||||
|
||||
@@ -71,7 +71,7 @@ func (a *onePanelAdapter) Items(ctx context.Context) ([]types.MigrationItem, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(websites, a.databaseItems(ctx)...), nil
|
||||
return slices.Concat(websites, a.databaseItems(ctx), a.databaseUserItems(ctx)), nil
|
||||
}
|
||||
|
||||
// websiteItems 列出静态、反代与 PHP 运行环境网站
|
||||
@@ -153,6 +153,68 @@ func (a *onePanelAdapter) databaseItems(ctx context.Context) []types.MigrationIt
|
||||
return items
|
||||
}
|
||||
|
||||
// databaseUserDetail 重新读取用户密码,迁移执行时密码可能已被改过
|
||||
func (a *onePanelAdapter) databaseUserDetail(ctx context.Context, item types.MigrationItem) (*types.MigrationDatabaseUser, error) {
|
||||
data, err := a.api(ctx, http.MethodPost, "/databases/users/search", map[string]any{"database": item.SourceGroup})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, ok := lo.Find(a.rows(data), func(row map[string]any) bool {
|
||||
return cast.ToString(row["username"]) == item.SourceID && cast.ToString(row["host"]) == item.Version
|
||||
})
|
||||
if !ok {
|
||||
return nil, errors.New(a.t.Get("resource no longer exists on the source server"))
|
||||
}
|
||||
|
||||
return &types.MigrationDatabaseUser{
|
||||
Type: item.Subtype, Username: item.SourceID,
|
||||
Password: cast.ToString(row["password"]), Host: item.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// databaseUserItems 列出各数据库服务下的用户,1Panel 的用户接口直接返回明文密码
|
||||
func (a *onePanelAdapter) databaseUserItems(ctx context.Context) []types.MigrationItem {
|
||||
items := make([]types.MigrationItem, 0)
|
||||
for _, subtype := range []string{"mysql", "mariadb", "postgresql"} {
|
||||
data, err := a.api(ctx, http.MethodGet, "/databases/db/list/"+subtype, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, server := range a.rows(data) {
|
||||
name := cast.ToString(server["database"])
|
||||
if name == "" || strings.EqualFold(cast.ToString(server["from"]), "remote") {
|
||||
continue
|
||||
}
|
||||
items = append(items, a.usersOf(ctx, subtype, name)...)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// usersOf 列出指定数据库服务下的用户
|
||||
func (a *onePanelAdapter) usersOf(ctx context.Context, subtype, server string) []types.MigrationItem {
|
||||
data, err := a.api(ctx, http.MethodPost, "/databases/users/search", map[string]any{"database": server})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
items := make([]types.MigrationItem, 0)
|
||||
for _, row := range a.rows(data) {
|
||||
username, password := cast.ToString(row["username"]), cast.ToString(row["password"])
|
||||
// 密码不可见的用户迁过去也连不上,不如不迁
|
||||
if username == "" || password == "" {
|
||||
continue
|
||||
}
|
||||
host := lo.CoalesceOrEmpty(cast.ToString(row["host"]), "%")
|
||||
items = append(items, types.MigrationItem{
|
||||
Key: biz.MigrationItemKey("database_user", subtype+":"+server+":"+username+"@"+host),
|
||||
Type: "database_user", Subtype: subtype, Name: username + "@" + host, Status: "running",
|
||||
TargetName: username, SourceID: username, SourceGroup: server, Version: host,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// databasesOf 列出指定数据库服务下的库
|
||||
func (a *onePanelAdapter) databasesOf(ctx context.Context, subtype, server, version string) []types.MigrationItem {
|
||||
path := "/databases/search"
|
||||
@@ -188,6 +250,8 @@ func (a *onePanelAdapter) Detail(ctx context.Context, item types.MigrationItem)
|
||||
detail.Website, err = a.websiteDetail(ctx, item)
|
||||
case "database":
|
||||
detail.Database, err = a.databaseDetail(ctx, item)
|
||||
case "database_user":
|
||||
detail.DatabaseUser, err = a.databaseUserDetail(ctx, item)
|
||||
default:
|
||||
err = errors.New(a.t.Get("unsupported migration resource type: %s", item.Type))
|
||||
}
|
||||
|
||||
+13
-4
@@ -71,10 +71,11 @@ type MigrationItem struct {
|
||||
|
||||
// MigrationDetail 迁移执行前重新读取的资源详情
|
||||
type MigrationDetail struct {
|
||||
Item MigrationItem
|
||||
Website *MigrationWebsite
|
||||
Database *MigrationDatabase
|
||||
Project *MigrationProject
|
||||
Item MigrationItem
|
||||
Website *MigrationWebsite
|
||||
Database *MigrationDatabase
|
||||
DatabaseUser *MigrationDatabaseUser
|
||||
Project *MigrationProject
|
||||
}
|
||||
|
||||
// MigrationWebsite 网站详情
|
||||
@@ -130,6 +131,14 @@ type MigrationDatabase struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
// MigrationDatabaseUser 数据库用户详情
|
||||
type MigrationDatabaseUser struct {
|
||||
Type string // mysql / postgresql
|
||||
Username string
|
||||
Password string
|
||||
Host string
|
||||
}
|
||||
|
||||
// MigrationProject 项目详情
|
||||
type MigrationProject struct {
|
||||
Type ProjectType
|
||||
|
||||
Reference in New Issue
Block a user