141 lines
3.7 KiB
Go
141 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultBaseURL = "http://localhost:8080"
|
|
configEnv = "CMSCTL_CONFIG"
|
|
)
|
|
|
|
// Config is deliberately small and JSON-compatible so operators can inspect
|
|
// or move it between machines. The file is written with mode 0600.
|
|
type Config struct {
|
|
BaseURL string `json:"base_url,omitempty"`
|
|
FinderURL string `json:"finder_url,omitempty"`
|
|
AccessToken string `json:"access_token,omitempty"`
|
|
TokenType string `json:"token_type,omitempty"`
|
|
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
func defaultConfig() Config {
|
|
return Config{BaseURL: defaultBaseURL, TokenType: "Bearer"}
|
|
}
|
|
|
|
func configPath(override string) (string, error) {
|
|
if override != "" {
|
|
return override, nil
|
|
}
|
|
if p := os.Getenv(configEnv); p != "" {
|
|
return p, nil
|
|
}
|
|
// UserConfigDir follows the platform convention: $XDG_CONFIG_HOME or
|
|
// ~/.config on Unix, %AppData% on Windows, and ~/Library/Application
|
|
// Support on macOS. Keep the explicit XDG check for callers that set it
|
|
// in environments where UserConfigDir cannot resolve it yet.
|
|
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
|
|
return filepath.Join(dir, "cmsctl", "config.json"), nil
|
|
}
|
|
if dir, err := os.UserConfigDir(); err == nil && dir != "" {
|
|
return filepath.Join(dir, "cmsctl", "config.json"), nil
|
|
}
|
|
// Last-resort Unix-compatible fallback for minimal environments without a
|
|
// discoverable platform config directory.
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("find user config directory: %w", err)
|
|
}
|
|
return filepath.Join(home, ".config", "cmsctl", "config.json"), nil
|
|
}
|
|
|
|
func loadConfig(path string) (Config, error) {
|
|
cfg := defaultConfig()
|
|
body, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return cfg, nil
|
|
}
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("read config %s: %w", path, err)
|
|
}
|
|
if err := json.Unmarshal(body, &cfg); err != nil {
|
|
return Config{}, fmt.Errorf("parse config %s: %w", path, err)
|
|
}
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = defaultBaseURL
|
|
}
|
|
if cfg.TokenType == "" {
|
|
cfg.TokenType = "Bearer"
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func saveConfig(path string, cfg Config) error {
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = defaultBaseURL
|
|
}
|
|
if cfg.TokenType == "" {
|
|
cfg.TokenType = "Bearer"
|
|
}
|
|
body, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("encode config: %w", err)
|
|
}
|
|
body = append(body, '\n')
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return fmt.Errorf("create config directory: %w", err)
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".config-*")
|
|
if err != nil {
|
|
return fmt.Errorf("create config temp file: %w", err)
|
|
}
|
|
tmpName := tmp.Name()
|
|
defer os.Remove(tmpName)
|
|
if err := tmp.Chmod(0o600); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("protect config temp file: %w", err)
|
|
}
|
|
if _, err := tmp.Write(body); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("write config: %w", err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("sync config: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("close config: %w", err)
|
|
}
|
|
if err := os.Rename(tmpName, path); err != nil {
|
|
return fmt.Errorf("replace config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func clearToken(cfg *Config) {
|
|
cfg.AccessToken = ""
|
|
cfg.TokenType = "Bearer"
|
|
cfg.ExpiresAt = time.Time{}
|
|
}
|
|
|
|
func effectiveBaseURL(cfg Config) string {
|
|
base := strings.TrimRight(cfg.BaseURL, "/")
|
|
if base == "" {
|
|
return defaultBaseURL
|
|
}
|
|
return base
|
|
}
|
|
|
|
func effectiveFinderURL(cfg Config) string {
|
|
if cfg.FinderURL != "" {
|
|
return strings.TrimRight(cfg.FinderURL, "/")
|
|
}
|
|
return effectiveBaseURL(cfg) + "/find"
|
|
}
|