chore: 支持迁移用户

This commit is contained in:
耗子
2026-08-15 10:02:37 +08:00
parent 556dd6fb07
commit b070d2d0d0
5 changed files with 189 additions and 15 deletions
+8 -2
View File
@@ -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 {
+38 -1
View File
@@ -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
+65 -7
View File
@@ -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:
+65 -1
View File
@@ -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
View File
@@ -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