Files
YMhut-box-C-/server/unified-management/internal/config/database.go
T
QWQLwToo 962a2f2143
build-winui / winui (push) Waiting to run
更新 update 门户站点界面和后台功能
2026-06-27 18:09:11 +08:00

190 lines
5.2 KiB
Go

package config
import (
"errors"
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
)
type MySQLInput struct {
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
Username string `json:"username"`
Password string `json:"password"`
Charset string `json:"charset"`
ParseTime bool `json:"parseTime"`
TLS string `json:"tls"`
}
type SafeDatabaseConfig struct {
Provider string `json:"provider"`
SQLitePath string `json:"sqlitePath"`
MySQLDSN string `json:"mysqlDsn"`
MySQLHost string `json:"mysqlHost"`
MySQLPort int `json:"mysqlPort"`
MySQLDatabase string `json:"mysqlDatabase"`
MySQLUser string `json:"mysqlUser"`
HasPassword bool `json:"hasPassword"`
}
func BuildMySQLDSN(input MySQLInput) (string, error) {
host := strings.TrimSpace(input.Host)
if host == "" {
host = "127.0.0.1"
}
port := input.Port
if port <= 0 {
port = 3306
}
database := strings.TrimSpace(input.Database)
username := strings.TrimSpace(input.Username)
if database == "" {
return "", errors.New("mysql database is required")
}
if username == "" {
return "", errors.New("mysql username is required")
}
params := url.Values{}
params.Set("charset", firstNonEmpty(strings.TrimSpace(input.Charset), "utf8mb4"))
params.Set("parseTime", strconv.FormatBool(input.ParseTime))
if tls := strings.TrimSpace(input.TLS); tls != "" {
params.Set("tls", tls)
}
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", username, input.Password, host, port, database, params.Encode()), nil
}
func NormalizeDatabase(baseDir string, current DatabaseConfig, incoming DatabaseConfig, keepPassword bool) (DatabaseConfig, error) {
next := current
structuredChanged := false
if incoming.Provider != "" {
next.Provider = strings.ToLower(strings.TrimSpace(incoming.Provider))
}
if next.Provider == "" {
next.Provider = "sqlite"
}
if incoming.SQLitePath != "" {
next.SQLitePath = incoming.SQLitePath
}
if next.SQLitePath != "" && !filepath.IsAbs(next.SQLitePath) && !strings.HasPrefix(strings.ToLower(next.SQLitePath), "file:") {
next.SQLitePath = filepath.Join(baseDir, next.SQLitePath)
}
if incoming.MySQLHost != "" {
next.MySQLHost = strings.TrimSpace(incoming.MySQLHost)
structuredChanged = true
}
if incoming.MySQLPort > 0 {
next.MySQLPort = incoming.MySQLPort
structuredChanged = true
}
if incoming.MySQLDatabase != "" {
next.MySQLDatabase = strings.TrimSpace(incoming.MySQLDatabase)
structuredChanged = true
}
if incoming.MySQLUser != "" {
next.MySQLUser = strings.TrimSpace(incoming.MySQLUser)
structuredChanged = true
}
if incoming.MySQLPassword != "" || !keepPassword {
next.MySQLPassword = incoming.MySQLPassword
structuredChanged = true
}
if incoming.MySQLDSN != "" {
next.MySQLDSN = strings.TrimSpace(incoming.MySQLDSN)
}
if next.MySQLHost == "" {
next.MySQLHost = "127.0.0.1"
}
if next.MySQLPort <= 0 {
next.MySQLPort = 3306
}
if next.Provider == "sqlite" {
next.MySQLDSN = ""
} else if next.Provider == "mysql" {
if structuredChanged || next.MySQLDSN == "" {
dsn, err := BuildMySQLDSN(MySQLInput{
Host: next.MySQLHost,
Port: next.MySQLPort,
Database: next.MySQLDatabase,
Username: next.MySQLUser,
Password: next.MySQLPassword,
Charset: "utf8mb4",
ParseTime: true,
})
if err != nil {
return DatabaseConfig{}, err
}
next.MySQLDSN = dsn
}
if strings.TrimSpace(next.MySQLDSN) == "" {
return DatabaseConfig{}, errors.New("mysql connection is required")
}
} else {
return DatabaseConfig{}, errors.New("provider must be sqlite or mysql")
}
if strings.TrimSpace(next.SQLitePath) == "" {
return DatabaseConfig{}, errors.New("sqlite path is required")
}
if next.MaxOpenConns <= 0 {
next.MaxOpenConns = 10
}
if next.MaxIdleConns <= 0 {
next.MaxIdleConns = 4
}
if next.ConnMaxLifetimeSeconds <= 0 {
next.ConnMaxLifetimeSeconds = 300
}
if next.HealthIntervalSec <= 0 {
next.HealthIntervalSec = 30
}
return next, nil
}
func SafeDatabase(baseDir string, cfg DatabaseConfig) SafeDatabaseConfig {
return SafeDatabaseConfig{
Provider: firstNonEmpty(cfg.Provider, "sqlite"),
SQLitePath: relativeToBase(baseDir, cfg.SQLitePath),
MySQLDSN: MaskDSN(cfg.MySQLDSN),
MySQLHost: cfg.MySQLHost,
MySQLPort: cfg.MySQLPort,
MySQLDatabase: cfg.MySQLDatabase,
MySQLUser: cfg.MySQLUser,
HasPassword: strings.TrimSpace(cfg.MySQLPassword) != "" || dsnHasPassword(cfg.MySQLDSN),
}
}
func MaskDSN(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
at := strings.Index(value, "@")
colon := strings.Index(value, ":")
if at > -1 && colon > -1 && colon < at {
return value[:colon+1] + "******" + value[at:]
}
return value
}
func relativeToBase(base, value string) string {
if strings.TrimSpace(value) == "" {
return ""
}
if base != "" {
if rel, err := filepath.Rel(base, value); err == nil && !strings.HasPrefix(rel, "..") && rel != "." {
return filepath.ToSlash(rel)
}
}
return filepath.ToSlash(value)
}
func dsnHasPassword(value string) bool {
value = strings.TrimSpace(value)
at := strings.Index(value, "@")
colon := strings.Index(value, ":")
return at > -1 && colon > -1 && colon < at && colon+1 < at
}