cerrado @ v0.4

  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}