161 lines
3.4 KiB
Go
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{}
|
|
}
|
|
}
|