Handle config options

This commit is contained in:
nemunaire 2025-10-28 19:23:39 +07:00
commit accd7e75d8
6 changed files with 192 additions and 1 deletions

View file

@ -8,12 +8,19 @@ import (
"syscall"
"github.com/nemunaire/repeater/internal/app"
"github.com/nemunaire/repeater/internal/config"
)
//go:embed all:static
var assets embed.FS
func main() {
// Load and parse options
cfg, err := config.ConsolidateConfig()
if err != nil {
log.Fatal(err)
}
// Create application instance
application := app.New(assets)
@ -34,7 +41,7 @@ func main() {
}()
// Start the server
if err := application.Run(":8080"); err != nil {
if err := application.Run(cfg.Bind); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

24
internal/config/cli.go Normal file
View file

@ -0,0 +1,24 @@
package config
import (
"flag"
)
// declareFlags registers flags for the structure Options.
func declareFlags(o *Config) {
flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
}
// parseCLI parse the flags and treats extra args as configuration filename.
func parseCLI(o *Config) error {
flag.Parse()
for _, conf := range flag.Args() {
err := parseFile(o, conf)
if err != nil {
return err
}
}
return nil
}

79
internal/config/config.go Normal file
View file

@ -0,0 +1,79 @@
package config
import (
"flag"
"log"
"os"
"path"
"strings"
)
type Config struct {
Bind string
}
// ConsolidateConfig fills an Options struct by reading configuration from
// config files, environment, then command line.
//
// Should be called only one time.
func ConsolidateConfig() (opts *Config, err error) {
// Define defaults options
opts = &Config{
Bind: ":8080",
}
declareFlags(opts)
// Establish a list of possible configuration file locations
configLocations := []string{
"repeater.conf",
}
if home, err := os.UserConfigDir(); err == nil {
configLocations = append(configLocations, path.Join(home, "repeater", "repeater.conf"))
}
configLocations = append(configLocations, path.Join("etc", "repeater.conf"))
// If config file exists, read configuration from it
for _, filename := range configLocations {
if _, e := os.Stat(filename); !os.IsNotExist(e) {
log.Printf("Loading configuration from %s\n", filename)
err = parseFile(opts, filename)
if err != nil {
return
}
break
}
}
// Then, overwrite that by what is present in the environment
err = parseEnvironmentVariables(opts)
if err != nil {
return
}
// Finaly, command line takes precedence
err = parseCLI(opts)
if err != nil {
return
}
return
}
// parseLine treats a config line and place the read value in the variable
// declared to the corresponding flag.
func parseLine(o *Config, line string) (err error) {
fields := strings.SplitN(line, "=", 2)
orig_key := strings.TrimSpace(fields[0])
value := strings.TrimSpace(fields[1])
key := strings.TrimPrefix(orig_key, "REPEATER_")
key = strings.Replace(key, "_", "-", -1)
key = strings.ToLower(key)
err = flag.Set(key, value)
return
}

27
internal/config/custom.go Normal file
View file

@ -0,0 +1,27 @@
package config
import (
"net/url"
)
type URL struct {
URL *url.URL
}
func (i *URL) String() string {
if i.URL != nil {
return i.URL.String()
} else {
return ""
}
}
func (i *URL) Set(value string) error {
u, err := url.Parse(value)
if err != nil {
return err
}
*i.URL = *u
return nil
}

21
internal/config/env.go Normal file
View file

@ -0,0 +1,21 @@
package config
import (
"fmt"
"os"
"strings"
)
// parseEnvironmentVariables analyzes all the environment variables to find
// each one starting by REPEATER_
func parseEnvironmentVariables(o *Config) (err error) {
for _, line := range os.Environ() {
if strings.HasPrefix(line, "REPEATER_") {
err := parseLine(o, line)
if err != nil {
return fmt.Errorf("error in environment (%q): %w", line, err)
}
}
}
return
}

33
internal/config/file.go Normal file
View file

@ -0,0 +1,33 @@
package config
import (
"bufio"
"fmt"
"os"
"strings"
)
// parseFile opens the file at the given filename path, then treat each line
// not starting with '#' as a configuration statement.
func parseFile(o *Config, filename string) error {
fp, err := os.Open(filename)
if err != nil {
return err
}
defer fp.Close()
scanner := bufio.NewScanner(fp)
n := 0
for scanner.Scan() {
n += 1
line := strings.TrimSpace(scanner.Text())
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
err := parseLine(o, line)
if err != nil {
return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err)
}
}
}
return nil
}