Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
Go
70 lines
2.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/secrets"
|
|
)
|
|
|
|
// Open picks a backend from what the address looks like.
|
|
//
|
|
// A path is SQLite and a postgres:// URL is PostgreSQL, so an existing
|
|
// configuration keeps working untouched and moving to PostgreSQL is one line
|
|
// changed rather than a new setting to discover. A gateway on one machine has
|
|
// no reason to run a database server, and a gateway that has outgrown that
|
|
// should not have to be reconfigured twice.
|
|
//
|
|
// An address of the form @/path/to/file reads the real address out of that
|
|
// file, which is how a DSN carrying a password stays out of a configuration
|
|
// file that gets copied, diffed and pasted into issues.
|
|
func Open(ctx context.Context, address string) (*DB, error) {
|
|
address, err := resolveAddress(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isPostgres(address) {
|
|
return OpenPostgres(ctx, address)
|
|
}
|
|
return OpenSQLite(ctx, address)
|
|
}
|
|
|
|
// isPostgres reports whether an address names a PostgreSQL server.
|
|
//
|
|
// By scheme, because that is the only part of a DSN that is reliably there.
|
|
// A bare host:port would be ambiguous with a relative file path, so it is not
|
|
// accepted: an address this cannot classify should fail loudly rather than
|
|
// quietly create a SQLite file named after someone's database server.
|
|
func isPostgres(address string) bool {
|
|
return strings.HasPrefix(address, "postgres://") ||
|
|
strings.HasPrefix(address, "postgresql://")
|
|
}
|
|
|
|
// resolveAddress expands an @file indirection.
|
|
//
|
|
// The file is read whole and trimmed, so a trailing newline — which every
|
|
// editor and every `echo` adds — does not become part of a password.
|
|
func resolveAddress(address string) (string, error) {
|
|
if !strings.HasPrefix(address, "@") {
|
|
return address, nil
|
|
}
|
|
path := strings.TrimPrefix(address, "@")
|
|
// A file, or env: or vault: (the secrets package says how).
|
|
b, err := secrets.Read(context.Background(), path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading the store address from %s: %w", path, err)
|
|
}
|
|
out := strings.TrimSpace(string(b))
|
|
if out == "" {
|
|
return "", fmt.Errorf("%s is empty, so there is no store address in it", path)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ResolveAddress expands an @file store address, as Open does.
|
|
func ResolveAddress(address string) (string, error) { return resolveAddress(address) }
|
|
|
|
// IsPostgres reports whether a resolved address names a PostgreSQL server.
|
|
func IsPostgres(address string) bool { return isPostgres(address) }
|