diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-05 14:17:59 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-05 14:17:59 +0200 |
| commit | 71c39ed9f8ce3563130cdfc5099e9d7df60845a9 (patch) | |
| tree | e610b8360f6364e3ab7c5e817ac94bd51cc0dfd1 /config/config.go | |
| parent | bef31e1d35fe88f2698cf0e3191f26a79623f66f (diff) | |
refactor: Replace env config with atomic YAML
Diffstat (limited to 'config/config.go')
| -rw-r--r-- | config/config.go | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..98ccebc --- /dev/null +++ b/config/config.go @@ -0,0 +1,120 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Database DatabaseConfig `yaml:"database"` + Server ServerConfig `yaml:"server"` + Storage StorageConfig `yaml:"storage"` + Strava StravaConfig `yaml:"strava"` +} + +type DatabaseConfig struct { + Path string `yaml:"path"` +} + +type ServerConfig struct { + Address string `yaml:"address"` + PublicURL string `yaml:"public_url"` +} + +type StorageConfig struct { + GPXDir string `yaml:"gpx_dir"` +} + +type StravaConfig struct { + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + AccessToken string `yaml:"access_token"` + RefreshToken string `yaml:"refresh_token"` + ExpiresAt int64 `yaml:"expires_at"` +} + +func Default() Config { + return Config{ + Database: DatabaseConfig{Path: "biking_home.db"}, + Server: ServerConfig{ + Address: "127.0.0.1:8080", + PublicURL: "http://localhost:8080", + }, + Storage: StorageConfig{GPXDir: "rides"}, + } +} + +func Load(filename string) (Config, error) { + data, err := os.ReadFile(filename) + if err != nil { + return Config{}, fmt.Errorf("read config %q: %w", filename, err) + } + config := Default() + if err := yaml.Unmarshal(data, &config); err != nil { + return Config{}, fmt.Errorf("parse config %q: %w", filename, err) + } + if config.Database.Path == "" { + return Config{}, fmt.Errorf("config %q: database.path is required", filename) + } + if config.Server.Address == "" || config.Server.PublicURL == "" { + return Config{}, fmt.Errorf("config %q: server.address and server.public_url are required", filename) + } + if config.Storage.GPXDir == "" { + return Config{}, fmt.Errorf("config %q: storage.gpx_dir is required", filename) + } + return config, nil +} + +func Save(filename string, config Config) error { + data, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + directory := filepath.Dir(filename) + temporary, err := os.CreateTemp(directory, "."+filepath.Base(filename)+".tmp-*") + if err != nil { + return fmt.Errorf("create temporary config: %w", err) + } + temporaryName := temporary.Name() + removeTemporary := true + defer func() { + if removeTemporary { + _ = os.Remove(temporaryName) + } + }() + if err := temporary.Chmod(0o600); err != nil { + _ = temporary.Close() + return fmt.Errorf("set config permissions: %w", err) + } + if _, err := temporary.Write(data); err != nil { + _ = temporary.Close() + return fmt.Errorf("write temporary config: %w", err) + } + if err := temporary.Sync(); err != nil { + _ = temporary.Close() + return fmt.Errorf("sync temporary config: %w", err) + } + if err := temporary.Close(); err != nil { + return fmt.Errorf("close temporary config: %w", err) + } + if err := os.Rename(temporaryName, filename); err != nil { + return fmt.Errorf("replace config: %w", err) + } + removeTemporary = false + return syncDirectory(directory) +} + +func syncDirectory(directory string) error { + dir, err := os.Open(directory) + if err != nil { + return fmt.Errorf("open config directory: %w", err) + } + defer dir.Close() + if err := dir.Sync(); err != nil { + return fmt.Errorf("sync config directory: %w", err) + } + return nil +} |