mcp-framework/config/config.go
2026-04-13 15:33:48 +02:00

161 lines
3.4 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)
const (
CurrentVersion = 1
DefaultFile = "config.json"
)
type FileConfig[T any] struct {
Version int `json:"version"`
CurrentProfile string `json:"current_profile"`
Profiles map[string]T `json:"profiles"`
}
type Store[T any] struct {
dirName string
fileName string
}
func NewStore[T any](dirName string) Store[T] {
return NewStoreWithFile[T](dirName, DefaultFile)
}
func NewStoreWithFile[T any](dirName, fileName string) Store[T] {
return Store[T]{
dirName: dirName,
fileName: fileName,
}
}
func (s Store[T]) Default() FileConfig[T] {
return FileConfig[T]{
Version: CurrentVersion,
Profiles: map[string]T{},
}
}
func (s Store[T]) ConfigPath() (string, error) {
userConfigDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("resolve user config dir: %w", err)
}
return filepath.Join(userConfigDir, s.dirName, s.fileName), nil
}
func (s Store[T]) Load(path string) (FileConfig[T], error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return s.Default(), nil
}
return FileConfig[T]{}, fmt.Errorf("read config %s: %w", path, err)
}
if len(data) == 0 {
return s.Default(), nil
}
cfg := s.Default()
if err := json.Unmarshal(data, &cfg); err != nil {
return FileConfig[T]{}, fmt.Errorf("parse config %s: %w", path, err)
}
s.normalize(&cfg)
return cfg, nil
}
func (s Store[T]) LoadDefault() (FileConfig[T], string, error) {
path, err := s.ConfigPath()
if err != nil {
return FileConfig[T]{}, "", err
}
cfg, err := s.Load(path)
if err != nil {
return FileConfig[T]{}, "", err
}
return cfg, path, nil
}
func (s Store[T]) Save(path string, cfg FileConfig[T]) error {
s.normalize(&cfg)
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create config dir %s: %w", dir, err)
}
if err := os.Chmod(dir, 0o700); err != nil {
return fmt.Errorf("set config dir permissions %s: %w", dir, err)
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("encode config: %w", err)
}
data = append(data, '\n')
tmpFile, err := os.CreateTemp(dir, "config-*.json")
if err != nil {
return fmt.Errorf("create temp config in %s: %w", dir, err)
}
tmpPath := tmpFile.Name()
cleanup := true
defer func() {
_ = tmpFile.Close()
if cleanup {
_ = os.Remove(tmpPath)
}
}()
if err := tmpFile.Chmod(0o600); err != nil {
return fmt.Errorf("set temp config permissions %s: %w", tmpPath, err)
}
if _, err := tmpFile.Write(data); err != nil {
return fmt.Errorf("write temp config %s: %w", tmpPath, err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("close temp config %s: %w", tmpPath, err)
}
if err := os.Rename(tmpPath, path); err != nil {
return fmt.Errorf("replace config %s: %w", path, err)
}
if err := os.Chmod(path, 0o600); err != nil {
return fmt.Errorf("set config permissions %s: %w", path, err)
}
cleanup = false
return nil
}
func (s Store[T]) SaveDefault(cfg FileConfig[T]) (string, error) {
path, err := s.ConfigPath()
if err != nil {
return "", err
}
if err := s.Save(path, cfg); err != nil {
return "", err
}
return path, nil
}
func (s Store[T]) normalize(cfg *FileConfig[T]) {
if cfg.Version == 0 {
cfg.Version = CurrentVersion
}
if cfg.Profiles == nil {
cfg.Profiles = map[string]T{}
}
}