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