Files
MyGoNavi/internal/app/connection_package_transfer.go
Syngnat 4d5c0e6bb9 feat(cli): 新增独立命令行与发布链
- 新增无头运行时及连接、查询、导出、批处理、审计和 MCP 命令
- 复用活动数据根、密文存储与跨进程锁,落实写入安全和取消语义
- 增加六平台 CLI 归档、独立校验和、Docker、npm 与 WinGet 分发
- 隔离 GUI/CLI 更新资产并强化 macOS 签名与公证门禁
- 补充并发、审计、事务及发布契约回归测试

Refs #902
2026-08-11 10:34:58 +08:00

755 lines
24 KiB
Go

package app
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/secretstore"
"github.com/google/uuid"
)
func newConnectionPackageItem(view connection.SavedConnectionView, bundle connectionSecretBundle, redisDbAliases map[string]string) connectionPackageItem {
return connectionPackageItem{
ID: view.ID,
Name: view.Name,
EnvironmentType: view.EnvironmentType,
IncludeDatabases: cloneStringSlice(view.IncludeDatabases),
IncludeDatabasePatterns: cloneStringSlice(view.IncludeDatabasePatterns),
ExcludeDatabasePatterns: cloneStringSlice(view.ExcludeDatabasePatterns),
IncludeRedisDatabases: cloneIntSlice(view.IncludeRedisDatabases),
SchemaVisibilityByDatabase: cloneSchemaVisibilityByDatabase(view.SchemaVisibilityByDatabase),
RedisDbAliases: cloneStringMap(redisDbAliases),
IconType: view.IconType,
IconColor: view.IconColor,
Config: stripConnectionSecretFields(view.Config),
Secrets: bundle,
}
}
func sanitizeConnectionPackageRedisDbAliases(value map[string]map[string]string) map[string]map[string]string {
if len(value) == 0 {
return nil
}
result := make(map[string]map[string]string, len(value))
for connectionID, aliases := range value {
id := strings.TrimSpace(connectionID)
if id == "" || len(aliases) == 0 {
continue
}
cleaned := make(map[string]string, len(aliases))
for dbIndex, alias := range aliases {
dbKey := strings.TrimSpace(dbIndex)
if dbKey == "" {
continue
}
// 仅接受数字下标,与前端 redisDbAlias 一致
if _, err := strconv.Atoi(dbKey); err != nil {
continue
}
label := strings.Join(strings.Fields(strings.TrimSpace(alias)), " ")
if label == "" {
continue
}
if len(label) > 64 {
label = label[:64]
}
cleaned[dbKey] = label
}
if len(cleaned) > 0 {
result[id] = cleaned
}
}
if len(result) == 0 {
return nil
}
return result
}
func collectRedisDbAliasesFromPackagePayload(payload connectionPackagePayload) map[string]map[string]string {
merged := make(map[string]map[string]string)
// 顶层字段(若有)
for connectionID, aliases := range sanitizeConnectionPackageRedisDbAliases(payload.RedisDbAliases) {
merged[connectionID] = cloneStringMap(aliases)
}
// 连接项内嵌字段优先覆盖(导出时与连接同级,更可靠)
for _, item := range payload.Connections {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
cleaned := sanitizeConnectionPackageRedisDbAliases(map[string]map[string]string{id: item.RedisDbAliases})
if aliases, ok := cleaned[id]; ok && len(aliases) > 0 {
merged[id] = aliases
}
}
if len(merged) == 0 {
return nil
}
return merged
}
func normalizeConnectionExportIDFilter(connectionIDs []string) map[string]struct{} {
if len(connectionIDs) == 0 {
return nil
}
filter := make(map[string]struct{}, len(connectionIDs))
for _, rawID := range connectionIDs {
id := strings.TrimSpace(rawID)
if id == "" {
continue
}
filter[id] = struct{}{}
}
if len(filter) == 0 {
return nil
}
return filter
}
func filterRedisDbAliasesByConnectionIDs(
aliases map[string]map[string]string,
filter map[string]struct{},
) map[string]map[string]string {
if filter == nil || len(aliases) == 0 {
return aliases
}
filtered := make(map[string]map[string]string, len(filter))
for connectionID, itemAliases := range aliases {
id := strings.TrimSpace(connectionID)
if _, ok := filter[id]; !ok {
continue
}
filtered[id] = itemAliases
}
if len(filtered) == 0 {
return nil
}
return filtered
}
func (a *App) buildConnectionPackagePayload(
redisDbAliases map[string]map[string]string,
connectionIDs []string,
) (connectionPackagePayload, error) {
repo := a.savedConnectionRepository()
var payload connectionPackagePayload
err := repo.withWriteLock(func() error {
var buildErr error
payload, buildErr = a.buildConnectionPackagePayloadUnlocked(repo, redisDbAliases, connectionIDs)
return buildErr
})
return payload, err
}
// buildConnectionPackagePayloadUnlocked must run while the saved-connection
// shared storage lock is held. Keeping the metadata and daily-secret reads in
// one critical section prevents a package or cloud snapshot from pairing two
// different connection revisions.
func (a *App) buildConnectionPackagePayloadUnlocked(
repo *savedConnectionRepository,
redisDbAliases map[string]map[string]string,
connectionIDs []string,
) (connectionPackagePayload, error) {
items, err := repo.List()
if err != nil {
return connectionPackagePayload{}, err
}
idFilter := normalizeConnectionExportIDFilter(connectionIDs)
aliasesByConnection := filterRedisDbAliasesByConnectionIDs(
sanitizeConnectionPackageRedisDbAliases(redisDbAliases),
idFilter,
)
connections := make([]connectionPackageItem, 0, len(items))
for _, item := range items {
itemID := strings.TrimSpace(item.ID)
if idFilter != nil {
if _, ok := idFilter[itemID]; !ok {
continue
}
}
bundle, bundleErr := repo.loadSecretBundle(item)
if bundleErr != nil {
return connectionPackagePayload{}, bundleErr
}
var itemAliases map[string]string
if aliasesByConnection != nil {
itemAliases = aliasesByConnection[itemID]
}
connections = append(connections, newConnectionPackageItem(item, bundle, itemAliases))
}
if idFilter != nil && len(connections) == 0 {
return connectionPackagePayload{}, errors.New(a.appText("app.connection_package.error.no_selected_connections", nil))
}
return connectionPackagePayload{
ExportedAt: time.Now().UTC().Format(time.RFC3339),
Connections: connections,
RedisDbAliases: aliasesByConnection,
}, nil
}
func (a *App) buildExportedConnectionPackage(options ConnectionExportOptions) ([]byte, error) {
payload, err := a.buildConnectionPackagePayload(options.RedisDbAliases, options.ConnectionIDs)
if err != nil {
return nil, err
}
if !options.IncludeSecrets {
for index := range payload.Connections {
payload.Connections[index].Secrets = connectionSecretBundle{}
}
}
normalizedPassword := normalizeConnectionPackagePassword(options.FilePassword)
if !options.IncludeSecrets || normalizedPassword == "" {
file, err := encryptConnectionPackageV2AppManaged(payload)
if err != nil {
return nil, err
}
return json.MarshalIndent(file, "", " ")
}
file, err := encryptConnectionPackageV2Protected(payload, normalizedPassword)
if err != nil {
return nil, err
}
return json.MarshalIndent(file, "", " ")
}
func newSavedConnectionInputFromPackageItem(item connectionPackageItem) connection.SavedConnectionInput {
id := strings.TrimSpace(item.ID)
if id == "" {
id = strings.TrimSpace(item.Config.ID)
}
config := item.Config
config.ID = id
config.SavePassword = false
secrets := item.Secrets
config.Password = secrets.Password
config.SSH.Password = secrets.SSHPassword
config.Proxy.Password = secrets.ProxyPassword
config.HTTPTunnel.Password = secrets.HTTPTunnelPassword
config.MySQLReplicaPassword = secrets.MySQLReplicaPassword
config.MongoReplicaPassword = secrets.MongoReplicaPassword
config.RedisSentinelPassword = secrets.RedisSentinelPassword
config.URI = secrets.OpaqueURI
config.DSN = secrets.OpaqueDSN
config.JVM.JMX.Password = secrets.JVMJMXPassword
config.JVM.Endpoint.APIKey = secrets.JVMEndpointAPIKey
config.JVM.Agent.APIKey = secrets.JVMAgentAPIKey
config.JVM.Diagnostic.APIKey = secrets.JVMDiagnosticAPIKey
config.ConnectionParams = mergeConnectionParams(config.ConnectionParams, secrets.SensitiveParams)
return connection.SavedConnectionInput{
ID: id,
Name: item.Name,
EnvironmentType: item.EnvironmentType,
Config: config,
IncludeDatabases: cloneStringSlice(item.IncludeDatabases),
IncludeDatabasePatterns: cloneStringSlice(item.IncludeDatabasePatterns),
ExcludeDatabasePatterns: cloneStringSlice(item.ExcludeDatabasePatterns),
IncludeRedisDatabases: cloneIntSlice(item.IncludeRedisDatabases),
SchemaVisibilityByDatabase: cloneSchemaVisibilityByDatabase(item.SchemaVisibilityByDatabase),
IconType: item.IconType,
IconColor: item.IconColor,
// 连接恢复包以最新导入文件为准;载荷中缺失的密文字段需要显式清空旧值。
ClearPrimaryPassword: strings.TrimSpace(secrets.Password) == "",
ClearSSHPassword: strings.TrimSpace(secrets.SSHPassword) == "",
ClearProxyPassword: strings.TrimSpace(secrets.ProxyPassword) == "",
ClearHTTPTunnelPassword: strings.TrimSpace(secrets.HTTPTunnelPassword) == "",
ClearMySQLReplicaPassword: strings.TrimSpace(secrets.MySQLReplicaPassword) == "",
ClearMongoReplicaPassword: strings.TrimSpace(secrets.MongoReplicaPassword) == "",
ClearRedisSentinelPassword: strings.TrimSpace(secrets.RedisSentinelPassword) == "",
ClearOpaqueURI: strings.TrimSpace(secrets.OpaqueURI) == "",
ClearOpaqueDSN: strings.TrimSpace(secrets.OpaqueDSN) == "",
ClearJVMJMXPassword: strings.TrimSpace(secrets.JVMJMXPassword) == "",
ClearJVMEndpointAPIKey: strings.TrimSpace(secrets.JVMEndpointAPIKey) == "",
ClearJVMAgentAPIKey: strings.TrimSpace(secrets.JVMAgentAPIKey) == "",
ClearJVMDiagnosticAPIKey: strings.TrimSpace(secrets.JVMDiagnosticAPIKey) == "",
ClearSensitiveParams: strings.TrimSpace(secrets.SensitiveParams) == "",
}
}
func dedupeImportedSavedConnectionViews(views []connection.SavedConnectionView) []connection.SavedConnectionView {
if len(views) < 2 {
return views
}
lastIndexByID := make(map[string]int, len(views))
for index, view := range views {
id := strings.TrimSpace(view.ID)
if id == "" {
continue
}
lastIndexByID[id] = index
}
result := make([]connection.SavedConnectionView, 0, len(views))
for index, view := range views {
id := strings.TrimSpace(view.ID)
if id != "" && lastIndexByID[id] != index {
continue
}
result = append(result, view)
}
return result
}
func dedupeImportedSavedConnectionInputs(inputs []connection.SavedConnectionInput) []connection.SavedConnectionInput {
if len(inputs) < 2 {
return inputs
}
lastIndexByID := make(map[string]int, len(inputs))
for index, input := range inputs {
id := strings.TrimSpace(input.ID)
if id == "" {
continue
}
lastIndexByID[id] = index
}
result := make([]connection.SavedConnectionInput, 0, len(inputs))
for index, input := range inputs {
id := strings.TrimSpace(input.ID)
if id != "" && lastIndexByID[id] != index {
continue
}
result = append(result, input)
}
return result
}
func normalizeImportedSavedConnectionInput(input connection.SavedConnectionInput) connection.SavedConnectionInput {
if strings.TrimSpace(input.ID) == "" && strings.TrimSpace(input.Config.ID) == "" {
input.ID = "conn-" + uuid.New().String()[:8]
}
if strings.TrimSpace(input.ID) == "" {
input.ID = strings.TrimSpace(input.Config.ID)
}
input.Config.ID = input.ID
return input
}
func (a *App) importSavedConnectionsAtomically(inputs []connection.SavedConnectionInput) ([]connection.SavedConnectionView, error) {
repo := a.savedConnectionRepository()
var result []connection.SavedConnectionView
err := repo.withWriteLock(func() error {
var importErr error
result, importErr = a.importSavedConnectionsUnlocked(repo, inputs)
return importErr
})
if err != nil {
return nil, err
}
return result, nil
}
// importSavedConnectionsUnlocked applies an import while the caller owns the
// saved-connection shared storage lock. Cloud restore uses this form so its
// pre-restore snapshot, import, and any rollback remain one atomic operation.
func (a *App) importSavedConnectionsUnlocked(repo *savedConnectionRepository, inputs []connection.SavedConnectionInput) ([]connection.SavedConnectionView, error) {
preparedInputs := make([]connection.SavedConnectionInput, 0, len(inputs))
for _, input := range inputs {
prepared, err := prepareSavedConnectionInput(normalizeImportedSavedConnectionInput(input))
if err != nil {
return nil, err
}
preparedInputs = append(preparedInputs, prepared)
}
finalInputs := dedupeImportedSavedConnectionInputs(preparedInputs)
result := make([]connection.SavedConnectionView, 0, len(finalInputs))
rollbackSnapshot, err := captureConnectionImportRollbackSnapshot(a, finalInputs)
if err != nil {
return nil, err
}
for _, input := range finalInputs {
view, saveErr := repo.saveUnlocked(input)
if saveErr != nil {
if rollbackErr := rollbackSnapshot.restoreUnlocked(a); rollbackErr != nil {
return nil, errors.Join(saveErr, fmt.Errorf("restore connection import rollback: %w", rollbackErr))
}
return nil, saveErr
}
result = append(result, view)
}
return dedupeImportedSavedConnectionViews(result), nil
}
func (a *App) importConnectionPackagePayload(payload connectionPackagePayload) ([]connection.SavedConnectionView, error) {
inputs := make([]connection.SavedConnectionInput, 0, len(payload.Connections))
for _, item := range payload.Connections {
inputs = append(inputs, newSavedConnectionInputFromPackageItem(item))
}
return a.importSavedConnectionsAtomically(inputs)
}
func (a *App) importConnectionPackagePayloadUnlocked(repo *savedConnectionRepository, payload connectionPackagePayload) ([]connection.SavedConnectionView, error) {
inputs := make([]connection.SavedConnectionInput, 0, len(payload.Connections))
for _, item := range payload.Connections {
inputs = append(inputs, newSavedConnectionInputFromPackageItem(item))
}
return a.importSavedConnectionsUnlocked(repo, inputs)
}
func connectionPackageImportResultFromViews(views []connection.SavedConnectionView, redisDbAliases map[string]map[string]string) ConnectionPackageImportResult {
return ConnectionPackageImportResult{
Connections: sanitizeSavedConnectionViews(views),
RedisDbAliases: sanitizeConnectionPackageRedisDbAliases(redisDbAliases),
}
}
func (a *App) ImportConnectionsPayload(raw string, password string) (ConnectionPackageImportResult, error) {
localizeError := func(err error) error {
return localizeConnectionPackageError(a.appText, err)
}
mysqlWorkbenchError := func(key string, params map[string]any) error {
return errors.New(a.appText(key, params))
}
empty := ConnectionPackageImportResult{}
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return empty, localizeError(errConnectionPackageUnsupported)
}
if len(trimmed) > connectionImportMaxFileBytes {
return empty, localizeError(errConnectionImportFileTooLarge)
}
if isConnectionPackageV2AppManaged(trimmed) {
var file connectionPackageFileV2
if err := json.Unmarshal([]byte(trimmed), &file); err != nil {
return empty, localizeError(errConnectionPackageUnsupported)
}
payload, err := decryptConnectionPackageV2AppManaged(file)
if err != nil {
return empty, localizeError(err)
}
views, err := a.importConnectionPackagePayload(payload)
if err != nil {
return empty, localizeError(err)
}
return connectionPackageImportResultFromViews(views, collectRedisDbAliasesFromPackagePayload(payload)), nil
}
if isConnectionPackageV2Protected(trimmed) {
var file connectionPackageFileV2Protected
if err := json.Unmarshal([]byte(trimmed), &file); err != nil {
return empty, localizeError(errConnectionPackageUnsupported)
}
payload, err := decryptConnectionPackageV2Protected(file, password)
if err != nil {
return empty, localizeError(err)
}
views, err := a.importConnectionPackagePayload(payload)
if err != nil {
return empty, localizeError(err)
}
return connectionPackageImportResultFromViews(views, collectRedisDbAliasesFromPackagePayload(payload)), nil
}
if isConnectionPackageEnvelope(trimmed) {
var file connectionPackageFile
if err := json.Unmarshal([]byte(trimmed), &file); err != nil {
return empty, localizeError(errConnectionPackageUnsupported)
}
payload, err := decryptConnectionPackage(file, password)
if err != nil {
return empty, localizeError(err)
}
views, err := a.importConnectionPackagePayload(payload)
if err != nil {
return empty, localizeError(err)
}
return connectionPackageImportResultFromViews(views, collectRedisDbAliasesFromPackagePayload(payload)), nil
}
if isMySQLWorkbenchXML(trimmed) {
inputs, err := parseMySQLWorkbenchXML(trimmed)
if err != nil {
return empty, mysqlWorkbenchError("file.backend.error.mysql_workbench_parse_failed", map[string]any{"detail": err.Error()})
}
if len(inputs) == 0 {
return empty, mysqlWorkbenchError("file.backend.error.mysql_workbench_no_connections", nil)
}
views, err := a.importSavedConnectionsAtomically(inputs)
if err != nil {
return empty, err
}
return connectionPackageImportResultFromViews(views, nil), nil
}
if isNavicatNCX(trimmed) {
inputs, err := parseNavicatNCXWithText(trimmed, a.appText)
if err != nil {
return empty, navicatWrapError(a.appText, "file.backend.error.navicat_ncx_parse_failed", nil, err)
}
if len(inputs) == 0 {
return empty, navicatError(a.appText, "file.backend.error.navicat_ncx_no_connections", nil)
}
views, err := a.importSavedConnectionsAtomically(inputs)
if err != nil {
return empty, err
}
return connectionPackageImportResultFromViews(views, nil), nil
}
var legacy []connection.LegacySavedConnection
if err := json.Unmarshal([]byte(trimmed), &legacy); err != nil {
return empty, localizeError(errConnectionPackageUnsupported)
}
views, err := a.ImportLegacyConnections(legacy)
if err != nil {
return empty, err
}
return connectionPackageImportResultFromViews(views, nil), nil
}
type connectionPackageImportRollbackSnapshot struct {
connectionsFileExists bool
connectionsFileData []byte
dailySecretsFileExists bool
dailySecretsFileData []byte
connectionSecrets map[string]securityUpdateSecretSnapshot
connectionCleanupRefs []string
}
func captureConnectionImportRollbackSnapshot(a *App, inputs []connection.SavedConnectionInput) (connectionPackageImportRollbackSnapshot, error) {
snapshot := connectionPackageImportRollbackSnapshot{
connectionSecrets: make(map[string]securityUpdateSecretSnapshot),
}
repo := a.savedConnectionRepository()
connectionFileData, connectionFileExists, err := readOptionalFile(repo.connectionsPath())
if err != nil {
return snapshot, err
}
snapshot.connectionsFileExists = connectionFileExists
snapshot.connectionsFileData = connectionFileData
dailySecretsFileData, dailySecretsFileExists, err := readOptionalFile(repo.dailySecrets().Path())
if err != nil {
return snapshot, err
}
snapshot.dailySecretsFileExists = dailySecretsFileExists
snapshot.dailySecretsFileData = dailySecretsFileData
existingConnections, err := repo.load()
if err != nil {
return snapshot, err
}
existingConnectionsByID := make(map[string]connection.SavedConnectionView, len(existingConnections))
for _, item := range existingConnections {
existingConnectionsByID[item.ID] = item
}
cleanupSet := make(map[string]struct{})
seenIDs := make(map[string]struct{})
for _, input := range inputs {
connectionID := strings.TrimSpace(input.ID)
if connectionID == "" {
connectionID = strings.TrimSpace(input.Config.ID)
}
if connectionID == "" {
continue
}
if _, alreadySeen := seenIDs[connectionID]; alreadySeen {
continue
}
seenIDs[connectionID] = struct{}{}
defaultRef, refErr := secretstore.BuildRef(savedConnectionSecretKind, connectionID)
if refErr == nil {
cleanupSet[defaultRef] = struct{}{}
}
existing, ok := existingConnectionsByID[connectionID]
if !ok || !savedConnectionViewHasSecrets(existing) {
continue
}
ref := strings.TrimSpace(existing.SecretRef)
if ref == "" {
ref = defaultRef
}
if ref == "" {
continue
}
secretSnapshot, captureErr := captureSecurityUpdateSecretSnapshot(a.secretStore, ref)
if captureErr != nil {
return snapshot, captureErr
}
snapshot.connectionSecrets[ref] = secretSnapshot
cleanupSet[ref] = struct{}{}
}
snapshot.connectionCleanupRefs = make([]string, 0, len(cleanupSet))
for ref := range cleanupSet {
snapshot.connectionCleanupRefs = append(snapshot.connectionCleanupRefs, ref)
}
return snapshot, nil
}
func (s connectionPackageImportRollbackSnapshot) restoreUnlocked(a *App) error {
repo := a.savedConnectionRepository()
var restoreErr error
if err := repo.dailySecrets().RestoreUnlocked(s.dailySecretsFileExists, s.dailySecretsFileData); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
if s.connectionsFileExists {
if err := writeSavedConnectionsFileAtomic(repo.connectionsPath(), s.connectionsFileData); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
} else if err := os.Remove(repo.connectionsPath()); err != nil && !os.IsNotExist(err) {
restoreErr = errors.Join(restoreErr, err)
}
for ref, secretSnapshot := range s.connectionSecrets {
if err := restoreSecurityUpdateSecretSnapshot(a.secretStore, ref, secretSnapshot); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
}
for _, ref := range s.connectionCleanupRefs {
if _, alreadyRestored := s.connectionSecrets[ref]; alreadyRestored {
continue
}
if err := deleteSecurityUpdateSecretRef(a.secretStore, ref); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
}
return restoreErr
}
// --- MySQL Workbench XML import ---
func isMySQLWorkbenchXML(content string) bool {
return strings.Contains(content, "<data") && strings.Contains(content, "grt_format") && strings.Contains(content, "db.mgmt.Connection")
}
// mysqlWorkbenchData is the root XML element.
type mysqlWorkbenchData struct {
XMLName xml.Name `xml:"data"`
Value mysqlWorkbenchTopValue `xml:"value"`
}
type mysqlWorkbenchTopValue struct {
Values []mysqlWorkbenchConnection `xml:"value"`
}
type mysqlWorkbenchConnection struct {
StructName string `xml:"struct-name,attr"`
Values []mysqlWorkbenchValue `xml:"value"`
Links []mysqlWorkbenchLinkValue `xml:"link"`
}
type mysqlWorkbenchValue struct {
Type string `xml:"type,attr"`
Key string `xml:"key,attr"`
StructName string `xml:"struct-name,attr"`
Content string `xml:",chardata"`
Children []mysqlWorkbenchValue `xml:"value"`
}
type mysqlWorkbenchLinkValue struct {
Key string `xml:"key,attr"`
Content string `xml:",chardata"`
}
func parseMySQLWorkbenchXML(content string) ([]connection.SavedConnectionInput, error) {
var data mysqlWorkbenchData
if err := xml.Unmarshal([]byte(content), &data); err != nil {
return nil, err
}
var inputs []connection.SavedConnectionInput
for _, conn := range data.Value.Values {
if conn.StructName != "db.mgmt.Connection" {
continue
}
input := parseMySQLWorkbenchConnection(conn)
inputs = append(inputs, input)
}
return inputs, nil
}
func parseMySQLWorkbenchConnection(conn mysqlWorkbenchConnection) connection.SavedConnectionInput {
params := make(map[string]string)
connName := ""
driverKey := ""
for _, v := range conn.Values {
key := strings.TrimSpace(v.Key)
switch {
case key == "name" && v.Type == "string":
connName = strings.TrimSpace(v.Content)
case key == "parameterValues" && v.Type == "dict":
for _, child := range v.Children {
childKey := strings.TrimSpace(child.Key)
if childKey == "" {
continue
}
params[childKey] = strings.TrimSpace(child.Content)
}
}
}
for _, link := range conn.Links {
if strings.TrimSpace(link.Key) == "driver" {
driverKey = strings.TrimSpace(link.Content)
}
}
host := params["hostName"]
port := 3306
if p, err := strconv.Atoi(params["port"]); err == nil && p > 0 {
port = p
}
user := params["userName"]
schema := params["schema"]
password := params["password"]
useSSL := false
if v, err := strconv.Atoi(params["useSSL"]); err == nil && v > 0 {
useSSL = true
}
dbType := "mysql"
if strings.Contains(driverKey, "mariadb") {
dbType = "mariadb"
}
connID := "conn-" + uuid.New().String()[:8]
config := connection.ConnectionConfig{
ID: connID,
Type: dbType,
Host: host,
Port: port,
User: user,
Password: password,
Database: schema,
UseSSL: useSSL,
}
if connName == "" {
connName = fmt.Sprintf("%s@%s:%d", user, host, port)
}
return connection.SavedConnectionInput{
ID: connID,
Name: connName,
Config: config,
}
}