all repos

mugit @ 9c82bda

🐮 git server that your cow will love

mugit/internal/config/config.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
config: validate, thanks to my boy Claude, 4 months ago
1
package config
2
3
import (
4
	"errors"
5
	"fmt"
6
	"os"
7
	"path/filepath"
8
	"time"
9
10
	"gopkg.in/yaml.v2"
11
)
12
13
var ErrConfigNotFound = errors.New("no config file found")
14
15
type ServerConfig struct {
16
	Host string `yaml:"host"`
17
	Port int    `yaml:"port"`
18
}
19
20
type MetaConfig struct {
21
	Title       string `yaml:"title"`
22
	Description string `yaml:"description"`
23
	Host        string `yaml:"host"`
24
}
25
26
type RepoConfig struct {
27
	Dir     string   `yaml:"dir"`
28
	Readmes []string `yaml:"readmes"`
29
	Masters []string `yaml:"masters"`
30
}
31
32
type SSHConfig struct {
33
	Enable  bool     `yaml:"enable"`
34
	Port    int      `yaml:"port"`
35
	HostKey string   `yaml:"host_key"`
36
	Keys    []string `yaml:"keys"`
37
}
38
39
type MirrorConfig struct {
40
	Enable      bool   `yaml:"enable"`
41
	Interval    string `yaml:"interval"`
42
	GithubToken string `yaml:"github_token"`
43
}
44
45
type Config struct {
46
	Server ServerConfig `yaml:"server"`
47
	Meta   MetaConfig   `yaml:"meta"`
48
	Repo   RepoConfig   `yaml:"repo"`
49
	SSH    SSHConfig    `yaml:"ssh"`
50
	Mirror MirrorConfig `yaml:"mirror"`
51
}
52
53
// Load loads configuration with the following priority:
54
// 1. User provided fpath (if provided and exists)
55
// 2. /var/lib/mugit/config.yaml
56
// 3. $XDG_CONFIG_HOME/mugit/config.yaml or $HOME/.config/mugit/config.yaml
57
func Load(fpath string) (*Config, error) {
58
	// 4. /etc/mugit/config.yaml
59
	configPath, err := findConfigFile(fpath)
60
	if err != nil {
61
		return nil, err
62
	}
63
64
	configBytes, err := os.ReadFile(configPath)
65
	if err != nil {
66
		return nil, err
67
	}
68
69
	var config Config
70
	if cerr := yaml.Unmarshal(configBytes, &config); cerr != nil {
71
		return nil, fmt.Errorf("parsing config: %w", cerr)
72
	}
73
74
	if config.Repo.Dir, err = filepath.Abs(config.Repo.Dir); err != nil {
75
		return nil, err
76
	}
77
78
	if verr := config.validate(); verr != nil {
79
		return nil, verr
80
	}
81
82
	return &config, nil
83
}
84
85
func (c Config) validate() error {
86
	var errs []error
87
88
	// server
89
	if err := validatePort(c.Server.Port, "server.port"); err != nil {
90
		errs = append(errs, err)
91
	}
92
93
	// meta
94
	if c.Meta.Host == "" {
95
		errs = append(errs, errors.New("meta.host is required"))
96
	}
97
98
	// repo
99
	if err := validateDirExists(c.Repo.Dir, "repo.dir"); err != nil {
100
		errs = append(errs, err)
101
	}
102
	if len(c.Repo.Readmes) == 0 {
103
		errs = append(errs, errors.New("repo.readmes must have at least one value"))
104
	}
105
	if len(c.Repo.Masters) == 0 {
106
		errs = append(errs, errors.New("repo.masters must have at least one value"))
107
	}
108
109
	// ssh
110
	if c.SSH.Enable {
111
		if err := validatePort(c.SSH.Port, "ssh.port"); err != nil {
112
			errs = append(errs, err)
113
		}
114
		if c.SSH.Port == c.Server.Port {
115
			errs = append(errs, fmt.Errorf("ssh.port must differ from server.port (both are %d)", c.Server.Port))
116
		}
117
		if err := validateFileExists(c.SSH.HostKey, "ssh.host_key"); err != nil {
118
			errs = append(errs, err)
119
		}
120
		if len(c.SSH.Keys) == 0 {
121
			errs = append(errs, errors.New("ssh.keys must have at least one value when ssh is enabled"))
122
		}
123
	}
124
125
	// mirror
126
	if c.Mirror.Enable {
127
		if c.Mirror.Interval == "" {
128
			errs = append(errs, errors.New("mirror.interval is required when mirror is enabled"))
129
		} else if _, err := time.ParseDuration(c.Mirror.Interval); err != nil {
130
			errs = append(errs, fmt.Errorf("mirror.interval: invalid duration format: %w", err))
131
		}
132
	}
133
134
	return errors.Join(errs...)
135
}
136
137
func findConfigFile(userPath string) (string, error) {
138
	if userPath != "" {
139
		if _, err := os.Stat(userPath); err == nil {
140
			return userPath, nil
141
		}
142
	}
143
144
	path := "/var/lib/mugit/config.yaml"
145
	if _, err := os.Stat(path); err == nil {
146
		return path, nil
147
	}
148
149
	if configDir, err := os.UserConfigDir(); err == nil {
150
		p := filepath.Join(configDir, "mugit", "config.yaml")
151
		if _, err := os.Stat(p); err == nil {
152
			return p, nil
153
		}
154
	}
155
156
	path = "/etc/mugit/config.yaml"
157
	if _, err := os.Stat(path); err == nil {
158
		return path, nil
159
	}
160
161
	return "", ErrConfigNotFound
162
}
163
164
func validatePort(port int, fieldName string) error {
165
	if port < 1 || port > 65535 {
166
		return fmt.Errorf("%s must be between 1 and 65535, got %d", fieldName, port)
167
	}
168
	return nil
169
}
170
171
func validateDirExists(path string, fieldName string) error {
172
	if path == "" {
173
		return fmt.Errorf("%s is required", fieldName)
174
	}
175
	info, err := os.Stat(path)
176
	if err != nil {
177
		if os.IsNotExist(err) {
178
			return fmt.Errorf("%s: directory does not exist: %s", fieldName, path)
179
		}
180
		return fmt.Errorf("%s: cannot access directory: %w", fieldName, err)
181
	}
182
	if !info.IsDir() {
183
		return fmt.Errorf("%s: path exists but is not a directory: %s", fieldName, path)
184
	}
185
	return nil
186
}
187
188
func validateFileExists(path string, fieldName string) error {
189
	if path == "" {
190
		return fmt.Errorf("%s is required", fieldName)
191
	}
192
	info, err := os.Stat(path)
193
	if err != nil {
194
		if os.IsNotExist(err) {
195
			return fmt.Errorf("%s: file does not exist: %s", fieldName, path)
196
		}
197
		return fmt.Errorf("%s: cannot access file: %w", fieldName, err)
198
	}
199
	if info.IsDir() {
200
		return fmt.Errorf("%s: path is a directory, not a file: %s", fieldName, path)
201
	}
202
	return nil
203
}