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 }