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{} } }