summaryrefslogtreecommitdiff
path: root/config/config.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-05 14:17:59 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-05 14:17:59 +0200
commit71c39ed9f8ce3563130cdfc5099e9d7df60845a9 (patch)
treee610b8360f6364e3ab7c5e817ac94bd51cc0dfd1 /config/config.go
parentbef31e1d35fe88f2698cf0e3191f26a79623f66f (diff)
refactor: Replace env config with atomic YAML
Diffstat (limited to 'config/config.go')
-rw-r--r--config/config.go120
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
+}