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