Enter on File Path inputs opens a file browser (load/save, extension filter, hidden toggle, overwrite confirm). Enter on Connection String inputs opens a builder for PostgreSQL, MSSQL and SQLite with masked password/preview, parsing and optional connection test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
187 lines
4.4 KiB
Go
187 lines
4.4 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// ConnKind identifies the database type a connection string targets.
|
|
type ConnKind string
|
|
|
|
const (
|
|
ConnPostgres ConnKind = "postgres"
|
|
ConnMSSQL ConnKind = "mssql"
|
|
ConnSQLite ConnKind = "sqlite"
|
|
)
|
|
|
|
// connKinds lists the kinds offered by the builder dialog, in display order.
|
|
var connKinds = []ConnKind{ConnPostgres, ConnMSSQL, ConnSQLite}
|
|
|
|
// maskedPassword is substituted for the password in previews.
|
|
const maskedPassword = "****"
|
|
|
|
// ConnFields holds the editable parts of a connection string.
|
|
type ConnFields struct {
|
|
Kind ConnKind
|
|
Host string
|
|
Port string
|
|
Database string
|
|
User string
|
|
Password string
|
|
SSLMode string
|
|
FilePath string // SQLite only
|
|
|
|
// Extra keeps query parameters the builder has no field for, so that
|
|
// parsing and rebuilding an existing string does not drop them.
|
|
Extra url.Values
|
|
}
|
|
|
|
// DefaultConnFields returns sensible defaults for the given kind.
|
|
func DefaultConnFields(kind ConnKind) ConnFields {
|
|
f := ConnFields{Kind: kind}
|
|
switch kind {
|
|
case ConnPostgres:
|
|
f.Host, f.Port, f.User, f.SSLMode = "localhost", "5432", "postgres", "disable"
|
|
case ConnMSSQL:
|
|
f.Host, f.Port, f.User, f.SSLMode = "localhost", "1433", "sa", "disable"
|
|
}
|
|
return f
|
|
}
|
|
|
|
// SSLModes returns the valid SSL/encryption options for a kind.
|
|
func SSLModes(kind ConnKind) []string {
|
|
switch kind {
|
|
case ConnPostgres:
|
|
return []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}
|
|
case ConnMSSQL:
|
|
return []string{"disable", "false", "true"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f ConnFields) sslParam() string {
|
|
if f.Kind == ConnMSSQL {
|
|
return "encrypt"
|
|
}
|
|
return "sslmode"
|
|
}
|
|
|
|
// BuildConnString renders the fields as a connection string. With mask set,
|
|
// a non-empty password is replaced by asterisks (for previews).
|
|
func BuildConnString(f ConnFields, mask bool) string {
|
|
if f.Kind == ConnSQLite {
|
|
return f.FilePath
|
|
}
|
|
|
|
u := &url.URL{Scheme: "postgres"}
|
|
if f.Kind == ConnMSSQL {
|
|
u.Scheme = "sqlserver"
|
|
}
|
|
|
|
if f.Port != "" {
|
|
u.Host = net.JoinHostPort(f.Host, f.Port)
|
|
} else {
|
|
u.Host = f.Host
|
|
}
|
|
|
|
if f.User != "" {
|
|
if f.Password != "" {
|
|
pw := f.Password
|
|
if mask {
|
|
pw = maskedPassword
|
|
}
|
|
u.User = url.UserPassword(f.User, pw)
|
|
} else {
|
|
u.User = url.User(f.User)
|
|
}
|
|
}
|
|
|
|
query := url.Values{}
|
|
for k, v := range f.Extra {
|
|
query[k] = v
|
|
}
|
|
if f.Kind == ConnMSSQL {
|
|
if f.Database != "" {
|
|
query.Set("database", f.Database)
|
|
}
|
|
} else if f.Database != "" {
|
|
u.Path = "/" + f.Database
|
|
}
|
|
if f.SSLMode != "" {
|
|
query.Set(f.sslParam(), f.SSLMode)
|
|
}
|
|
u.RawQuery = query.Encode()
|
|
|
|
out := u.String()
|
|
if mask {
|
|
// url escapes '*' in the userinfo; keep the preview readable.
|
|
out = strings.Replace(out, url.QueryEscape(maskedPassword), maskedPassword, 1)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DetectConnKind guesses the kind from a connection string's scheme. Anything
|
|
// that is not a recognised URL is treated as a SQLite file path.
|
|
func DetectConnKind(s string) ConnKind {
|
|
lower := strings.ToLower(strings.TrimSpace(s))
|
|
switch {
|
|
case strings.HasPrefix(lower, "postgres://"), strings.HasPrefix(lower, "postgresql://"):
|
|
return ConnPostgres
|
|
case strings.HasPrefix(lower, "sqlserver://"), strings.HasPrefix(lower, "mssql://"):
|
|
return ConnMSSQL
|
|
}
|
|
return ConnSQLite
|
|
}
|
|
|
|
// ParseConnString splits a connection string into fields. An empty string
|
|
// yields the defaults for hint. Missing ports fall back to the kind default.
|
|
func ParseConnString(s string, hint ConnKind) (ConnFields, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return DefaultConnFields(hint), nil
|
|
}
|
|
|
|
kind := DetectConnKind(s)
|
|
if kind == ConnSQLite {
|
|
path := s
|
|
for _, prefix := range []string{"sqlite://", "sqlite3://"} {
|
|
path = strings.TrimPrefix(path, prefix)
|
|
}
|
|
return ConnFields{Kind: ConnSQLite, FilePath: path}, nil
|
|
}
|
|
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
return DefaultConnFields(kind), fmt.Errorf("invalid connection string: %w", err)
|
|
}
|
|
|
|
f := ConnFields{
|
|
Kind: kind,
|
|
Host: u.Hostname(),
|
|
Port: u.Port(),
|
|
}
|
|
if f.Port == "" {
|
|
f.Port = DefaultConnFields(kind).Port
|
|
}
|
|
if u.User != nil {
|
|
f.User = u.User.Username()
|
|
f.Password, _ = u.User.Password()
|
|
}
|
|
|
|
query := u.Query()
|
|
if kind == ConnMSSQL {
|
|
f.Database = query.Get("database")
|
|
query.Del("database")
|
|
} else {
|
|
f.Database = strings.TrimPrefix(u.Path, "/")
|
|
}
|
|
f.SSLMode = query.Get(f.sslParam())
|
|
query.Del(f.sslParam())
|
|
if len(query) > 0 {
|
|
f.Extra = query
|
|
}
|
|
return f, nil
|
|
}
|