Initial commit on Forgejo
Fresh repository history for elektrine/tarakan-client hosted at https://git.elektrine.com/elektrine/tarakan-client.
This commit is contained in:
commit
58b29a7f84
70 changed files with 12994 additions and 0 deletions
163
internal/api/config.go
Normal file
163
internal/api/config.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config is how the client finds the Tarakan host and authenticates.
|
||||
// CLI flags and interactive /url /token override environment variables and
|
||||
// the config saved by `tarakan login`.
|
||||
type Config struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// LoadConfig builds config in this precedence order: explicit values,
|
||||
// environment variables, values saved by `tarakan login`, then defaults.
|
||||
func LoadConfig(url, token string) Config {
|
||||
saved, _ := LoadSavedConfig()
|
||||
return Config{
|
||||
BaseURL: firstNonEmpty(url, os.Getenv("TARAKAN_URL"), saved.BaseURL, DefaultBaseURL),
|
||||
Token: firstNonEmpty(token, os.Getenv("TARAKAN_API_TOKEN"), saved.Token),
|
||||
}.normalized()
|
||||
}
|
||||
|
||||
// SavedConfigPath returns the per-user file used by `tarakan login`.
|
||||
func SavedConfigPath() (string, error) {
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "tarakan", "config.json"), nil
|
||||
}
|
||||
|
||||
// LoadSavedConfig reads the persisted login, if one exists.
|
||||
func LoadSavedConfig() (Config, error) {
|
||||
path, err := SavedConfigPath()
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Config{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
var config Config
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return config.normalized(), nil
|
||||
}
|
||||
|
||||
// SaveConfig persists a login in a user-only file. The containing directory
|
||||
// and file permissions are tightened even when they already exist.
|
||||
func SaveConfig(config Config) (string, error) {
|
||||
config = config.normalized()
|
||||
path, err := SavedConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Chmod(dir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
raw, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := file.Chmod(0o600); err != nil {
|
||||
_ = file.Close()
|
||||
return "", err
|
||||
}
|
||||
if _, err := file.Write(raw); err != nil {
|
||||
_ = file.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// RemoveSavedConfig logs the client out. It is idempotent.
|
||||
func RemoveSavedConfig() error {
|
||||
path, err := SavedConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Remove(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// FromEnv is LoadConfig with no explicit overrides (env / defaults only).
|
||||
func FromEnv() (*Client, error) {
|
||||
return LoadConfig("", "").Client()
|
||||
}
|
||||
|
||||
// WithOverrides returns a copy with non-empty url/token applied.
|
||||
func (c Config) WithOverrides(url, token string) Config {
|
||||
if strings.TrimSpace(url) != "" {
|
||||
c.BaseURL = url
|
||||
}
|
||||
if strings.TrimSpace(token) != "" {
|
||||
c.Token = token
|
||||
}
|
||||
return c.normalized()
|
||||
}
|
||||
|
||||
// Client builds an HTTP client from this config.
|
||||
func (c Config) Client() (*Client, error) {
|
||||
return New(c.BaseURL, c.Token, nil)
|
||||
}
|
||||
|
||||
// MaskedToken is safe to show in UI (never the full secret).
|
||||
func (c Config) MaskedToken() string {
|
||||
t := strings.TrimSpace(c.Token)
|
||||
if t == "" {
|
||||
return "(not set)"
|
||||
}
|
||||
if len(t) <= 8 {
|
||||
return "****"
|
||||
}
|
||||
return t[:4] + "…" + t[len(t)-4:]
|
||||
}
|
||||
|
||||
// Summary is a one-line status for the interactive UI.
|
||||
func (c Config) Summary() string {
|
||||
return "url " + c.BaseURL + " token " + c.MaskedToken()
|
||||
}
|
||||
|
||||
func (c Config) normalized() Config {
|
||||
c.BaseURL = strings.TrimRight(strings.TrimSpace(c.BaseURL), "/")
|
||||
c.Token = strings.TrimSpace(c.Token)
|
||||
if c.BaseURL == "" {
|
||||
c.BaseURL = DefaultBaseURL
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue