1package config
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "log/slog"
8 "os"
9 "path"
10 "path/filepath"
11 "strconv"
12 "strings"
13
14 "git.gabrielgio.me/cerrado/pkg/u"
15 "git.sr.ht/~emersion/go-scfg"
16)
17
18var (
19 ErrScanPath = errors.New("Scan path does not exist")
20 ErrRepoPath = errors.New("Repository path does not exist")
21 ErrInvalidProperty = errors.New("Invalid property")
22)
23
24type OrderBy int
25
26const (
27 Unordered OrderBy = iota
28 AlphabeticalAsc
29 AlphabeticalDesc
30 LastCommitAsc
31 LastCommitDesc
32)
33
34type (
35
36 // scan represents piece of the scan from the configuration file.
37 scan struct {
38 Path string
39 Public bool
40 }
41
42 // configuration represents file configuration.
43 // fields needs to be exported to cmp to work
44 configuration struct {
45 AESKey string
46 ListenAddr string
47 OrderBy string
48 Passphrase string
49 RemoveSuffix bool
50 Repositories []*GitRepositoryConfiguration
51 RootReadme string
52 Scans []*scan
53 SyntaxHighlight string
54 Hostname string
55 }
56
57 // This is a per repository configuration.
58 GitRepositoryConfiguration struct {
59 Name string
60 Path string
61 Description string
62 Public bool
63 About string
64 }
65
66 // ConfigurationRepository represents the configuration repository (as in
67 // database repositories).
68 // This holds all the function necessary to ask for configuration
69 // information.
70 ConfigurationRepository struct {
71 aesKey []byte
72 listenAddr string
73 orderBy OrderBy
74 passphrase []byte
75 removeSuffix bool
76 repositories []*GitRepositoryConfiguration
77 rootReadme string
78 syntaxHighlight string
79 hostname string
80 }
81)
82
83func LoadConfigurationRepository(configPath string) (*ConfigurationRepository, error) {
84 f, err := os.Open(configPath)
85 if err != nil {
86 return nil, err
87 }
88
89 config, err := parse(f)
90 if err != nil {
91 return nil, err
92 }
93
94 repo := &ConfigurationRepository{
95 aesKey: []byte(config.AESKey),
96 listenAddr: config.ListenAddr,
97 passphrase: []byte(config.Passphrase),
98 repositories: config.Repositories,
99 rootReadme: config.RootReadme,
100 hostname: config.Hostname,
101 syntaxHighlight: config.SyntaxHighlight,
102 removeSuffix: config.RemoveSuffix,
103 orderBy: parseOrderBy(config.OrderBy),
104 }
105
106 for _, scan := range config.Scans {
107 if scan.Path != "" {
108 err = repo.expandOnScanPath(scan.Path, scan.Public)
109 if err != nil {
110 return nil, err
111 }
112 }
113 }
114
115 return repo, nil
116}
117
118// GetRootReadme returns root read path
119func (c *ConfigurationRepository) GetRootReadme() string {
120 return c.rootReadme
121}
122
123func (c *ConfigurationRepository) GetHostname() string {
124 return c.hostname
125}
126
127func (c *ConfigurationRepository) GetOrderBy() OrderBy {
128 return c.orderBy
129}
130
131func (c *ConfigurationRepository) GetSyntaxHighlight() string {
132 return c.syntaxHighlight
133}
134
135func (c *ConfigurationRepository) GetListenAddr() string {
136 return c.listenAddr
137}
138
139func (c *ConfigurationRepository) GetPassphrase() []byte {
140 return c.passphrase
141}
142
143func (c *ConfigurationRepository) GetBase64AesKey() []byte {
144 return c.aesKey
145}
146
147func (c *ConfigurationRepository) IsAuthEnabled() bool {
148 return len(c.passphrase) != 0
149}
150
151// GetByName returns configuration of repository for a given name.
152// It returns nil if there is not match for it.
153func (c *ConfigurationRepository) GetByName(name string) *GitRepositoryConfiguration {
154 for _, r := range c.repositories {
155 if r.Name == name {
156 return r
157 }
158 }
159 return nil
160}
161
162// List returns all the configuration for all repositories.
163func (c *ConfigurationRepository) List() []*GitRepositoryConfiguration {
164 return c.repositories
165}
166
167// expandOnScanPath scans the scanPath for folders taking them as repositories
168// and applying them default configuration.
169func (c *ConfigurationRepository) expandOnScanPath(scanPath string, public bool) error {
170 if !u.FileExist(scanPath) {
171 return ErrScanPath
172 }
173
174 entries, err := os.ReadDir(scanPath)
175 if err != nil {
176 return err
177 }
178
179 for _, e := range entries {
180 if !e.IsDir() {
181 continue
182 }
183
184 fullPath := path.Join(scanPath, e.Name())
185 if !c.repoExits(fullPath) {
186
187 name := e.Name()
188 if c.removeSuffix {
189 name = strings.TrimSuffix(name, ".git")
190 }
191
192 c.repositories = append(c.repositories, &GitRepositoryConfiguration{
193 Name: name,
194 Path: fullPath,
195 Public: public,
196 })
197 }
198 }
199 return nil
200}
201
202func (c *ConfigurationRepository) repoExits(path string) bool {
203 for _, r := range c.repositories {
204 if path == r.Path {
205 return true
206 }
207 }
208 return false
209}
210
211func parse(r io.Reader) (*configuration, error) {
212 block, err := scfg.Read(r)
213 if err != nil {
214 return nil, err
215 }
216
217 config := defaultConfiguration()
218
219 err = setScan(block, &config.Scans)
220 if err != nil {
221 return nil, err
222 }
223
224 err = setRootReadme(block, &config.RootReadme)
225 if err != nil {
226 return nil, err
227 }
228
229 err = setHostname(block, &config.Hostname)
230 if err != nil {
231 return nil, err
232 }
233
234 err = setListenAddr(block, &config.ListenAddr)
235 if err != nil {
236 return nil, err
237 }
238
239 err = setPassphrase(block, &config.Passphrase)
240 if err != nil {
241 return nil, err
242 }
243
244 err = setAESKey(block, &config.AESKey)
245 if err != nil {
246 return nil, err
247 }
248
249 err = setSyntaxHighlight(block, &config.SyntaxHighlight)
250 if err != nil {
251 return nil, err
252 }
253
254 err = setOrderby(block, &config.OrderBy)
255 if err != nil {
256 return nil, err
257 }
258
259 err = setRemoveSuffix(block, &config.RemoveSuffix)
260 if err != nil {
261 return nil, err
262 }
263
264 err = setRepositories(block, &config.Repositories)
265 if err != nil {
266 return nil, err
267 }
268
269 return config, nil
270}
271
272func setRepositories(block scfg.Block, repositories *[]*GitRepositoryConfiguration) error {
273 blocks := block.GetAll("repository")
274
275 for _, r := range blocks {
276 if len(r.Params) != 1 {
277 return fmt.Errorf(
278 "Invlid number of params for repository: %w",
279 ErrInvalidProperty,
280 )
281 }
282
283 path := u.FirstOrZero(r.Params)
284 repository := defaultRepisotryConfiguration(path)
285
286 for _, d := range r.Children {
287 // under repository there is only single param properties
288 if len(d.Params) != 1 {
289 return fmt.Errorf(
290 "Invlid number of params for %s: %w",
291 d.Name,
292 ErrInvalidProperty,
293 )
294 }
295
296 switch d.Name {
297 case "name":
298 if err := setString(d, &repository.Name); err != nil {
299 return err
300 }
301 case "description":
302 if err := setString(d, &repository.Description); err != nil {
303 return err
304 }
305 case "public":
306 if err := setBool(d, &repository.Public); err != nil {
307 return err
308 }
309 case "about":
310 if err := setString(d, &repository.About); err != nil {
311 return err
312 }
313 }
314 }
315
316 *repositories = append(*repositories, repository)
317 }
318
319 return nil
320}
321
322func defaultConfiguration() *configuration {
323 return &configuration{
324 Scans: defaultScans(),
325 RootReadme: "",
326 Hostname: defaultHostname(),
327 ListenAddr: defaultAddr(),
328 Repositories: make([]*GitRepositoryConfiguration, 0),
329 }
330}
331
332func defaultHostname() string {
333 return "https://localhost:8080"
334}
335
336func defaultScans() []*scan {
337 return []*scan{}
338}
339
340func defaultAddr() string {
341 return "tcp://localhost:8080"
342}
343
344func defaultRepisotryConfiguration(path string) *GitRepositoryConfiguration {
345 return &GitRepositoryConfiguration{
346 Path: path,
347 Name: filepath.Base(path),
348 Description: "",
349 Public: false,
350 About: "README.md",
351 }
352}
353
354func setRootReadme(block scfg.Block, readme *string) error {
355 scanDir := block.Get("root-readme")
356 return setString(scanDir, readme)
357}
358
359func setHostname(block scfg.Block, hostname *string) error {
360 scanDir := block.Get("hostname")
361 return setString(scanDir, hostname)
362}
363
364func setPassphrase(block scfg.Block, listenAddr *string) error {
365 scanDir := block.Get("passphrase")
366 return setString(scanDir, listenAddr)
367}
368
369func setAESKey(block scfg.Block, listenAddr *string) error {
370 scanDir := block.Get("aes-key")
371 return setString(scanDir, listenAddr)
372}
373
374func setSyntaxHighlight(block scfg.Block, listenAddr *string) error {
375 scanDir := block.Get("syntax-highlight")
376 return setString(scanDir, listenAddr)
377}
378
379func setOrderby(block scfg.Block, orderBy *string) error {
380 scanDir := block.Get("order-by")
381 return setString(scanDir, orderBy)
382}
383
384func setListenAddr(block scfg.Block, listenAddr *string) error {
385 scanDir := block.Get("listen-addr")
386 return setString(scanDir, listenAddr)
387}
388
389func setRemoveSuffix(block scfg.Block, remove *bool) error {
390 scanDir := block.Get("remove-suffix")
391 return setBool(scanDir, remove)
392}
393
394func setScan(block scfg.Block, scans *[]*scan) error {
395 for _, scanDir := range block.GetAll("scan") {
396 s := &scan{}
397 if scanDir == nil {
398 return nil
399 }
400 err := setString(scanDir, &s.Path)
401 if err != nil {
402 return err
403 }
404
405 public := scanDir.Children.Get("public")
406 err = setBool(public, &s.Public)
407 if err != nil {
408 return err
409 }
410
411 *scans = append(*scans, s)
412 }
413
414 return nil
415}
416
417func setBool(dir *scfg.Directive, field *bool) error {
418 if dir != nil {
419
420 p1, _ := u.First(dir.Params)
421 v, err := strconv.ParseBool(p1)
422 if err != nil {
423 return fmt.Errorf("Error parsing bool param of %s: %w", dir.Name, err)
424 }
425 *field = v
426 }
427 return nil
428}
429
430func setString(dir *scfg.Directive, field *string) error {
431 if dir != nil {
432 *field = u.FirstOrZero(dir.Params)
433 }
434 return nil
435}
436
437func parseOrderBy(s string) OrderBy {
438 switch s {
439 case "":
440 return LastCommitAsc
441 case "unordered":
442 return Unordered
443 case "alphabetical-asc":
444 return AlphabeticalAsc
445 case "alphabetical-desc":
446 return AlphabeticalDesc
447 case "lastcommit-asc":
448 return LastCommitAsc
449 case "lastcommit-desc":
450 return LastCommitDesc
451 default:
452 slog.Warn("Invalid order-by using default unordered")
453 return LastCommitAsc
454
455 }
456}