1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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: "data/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
}
|