snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
build / build (push) Failing after 3s
Some checks failed
build / build (push) Failing after 3s
This commit is contained in:
104
config/config.go
Normal file
104
config/config.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/traggo/server/config/mode"
|
||||
)
|
||||
|
||||
var (
|
||||
prefix = "traggo"
|
||||
files = []string{".env.development.local", ".env.development", ".env.local", ".env"}
|
||||
absoluteFiles = []string{"/etc/traggo/server.ini"}
|
||||
osExecutable = os.Executable
|
||||
osStat = os.Stat
|
||||
)
|
||||
|
||||
// Config represents the application configuration.
|
||||
type Config struct {
|
||||
Port int `default:"3030"`
|
||||
LogLevel LogLevel `default:"info" split_words:"true"`
|
||||
DefaultUserName string `default:"admin" split_words:"true"`
|
||||
DefaultUserPass string `default:"admin" split_words:"true"`
|
||||
PassStrength int `default:"10" split_words:"true"`
|
||||
DatabaseDialect string `default:"sqlite3" split_words:"true"`
|
||||
DatabaseConnection string `default:"data/traggo.db" split_words:"true"`
|
||||
}
|
||||
|
||||
// Get loads the application config.
|
||||
func Get() (Config, []FutureLog) {
|
||||
var logs []FutureLog
|
||||
dir, log := getExecutableOrWorkDir()
|
||||
if log != nil {
|
||||
logs = append(logs, *log)
|
||||
}
|
||||
|
||||
for _, file := range getFiles(dir) {
|
||||
_, fileErr := osStat(file)
|
||||
if fileErr == nil {
|
||||
if err := godotenv.Load(file); err != nil {
|
||||
logs = append(logs, FutureLog{
|
||||
Level: zerolog.FatalLevel,
|
||||
Msg: fmt.Sprintf("cannot load file %s: %s", file, err)})
|
||||
} else {
|
||||
logs = append(logs, FutureLog{
|
||||
Level: zerolog.DebugLevel,
|
||||
Msg: fmt.Sprintf("Loading file %s", file)})
|
||||
}
|
||||
} else if os.IsNotExist(fileErr) {
|
||||
continue
|
||||
} else {
|
||||
logs = append(logs, FutureLog{
|
||||
Level: zerolog.WarnLevel,
|
||||
Msg: fmt.Sprintf("cannot read file %s because %s", file, fileErr)})
|
||||
}
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
err := envconfig.Process(prefix, &config)
|
||||
if err != nil {
|
||||
logs = append(logs, FutureLog{
|
||||
Level: zerolog.FatalLevel,
|
||||
Msg: fmt.Sprintf("cannot parse env params: %s", err)})
|
||||
}
|
||||
|
||||
return config, logs
|
||||
}
|
||||
|
||||
func getExecutableOrWorkDir() (string, *FutureLog) {
|
||||
dir, err := getExecutableDir()
|
||||
// when using `go run main.go` the executable lives in th temp directory therefore the env.development
|
||||
// will not be read, this enforces that the current work directory is used in dev mode.
|
||||
if err != nil || mode.Get() == mode.Dev {
|
||||
return filepath.Dir("."), err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func getExecutableDir() (string, *FutureLog) {
|
||||
ex, err := osExecutable()
|
||||
if err != nil {
|
||||
return "", &FutureLog{
|
||||
Level: zerolog.ErrorLevel,
|
||||
Msg: "Could not get path of executable using working directory instead. " + err.Error()}
|
||||
}
|
||||
return filepath.Dir(ex), nil
|
||||
}
|
||||
|
||||
func getFiles(relativeTo string) []string {
|
||||
var result []string
|
||||
for _, file := range files {
|
||||
result = append(result, filepath.Join(relativeTo, file))
|
||||
}
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
result = append(result, filepath.Join(homeDir, ".config/traggo/server.ini"))
|
||||
}
|
||||
result = append(result, absoluteFiles...)
|
||||
return result
|
||||
}
|
||||
160
config/config_test.go
Normal file
160
config/config_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traggo/server/config/mode"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
absoluteFiles = []string{}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestGet_withoutFiles_containsDefault(t *testing.T) {
|
||||
conf, logs := Get()
|
||||
|
||||
assert.Empty(t, logs)
|
||||
assert.Equal(t, 3030, conf.Port)
|
||||
}
|
||||
|
||||
func TestGet_withoutFiles_usesEnvVariables(t *testing.T) {
|
||||
assert.Nil(t, os.Setenv("TRAGGO_PORT", "9999"))
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
|
||||
conf, logs := Get()
|
||||
|
||||
assert.Empty(t, logs)
|
||||
assert.Equal(t, 9999, conf.Port)
|
||||
}
|
||||
|
||||
func TestGet_withFile_usesFile(t *testing.T) {
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
assert.Nil(t, ioutil.WriteFile(".env", []byte("TRAGGO_PORT=5555"), 0777))
|
||||
defer os.Remove(".env")
|
||||
|
||||
conf, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `Loading file .env`,
|
||||
Level: zerolog.DebugLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
assert.Equal(t, 5555, conf.Port)
|
||||
}
|
||||
|
||||
func TestGet_withInvalidEnvParam_errs(t *testing.T) {
|
||||
assert.Nil(t, os.Setenv("TRAGGO_PORT", "asdasd"))
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
|
||||
_, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `cannot parse env params: envconfig.Process: assigning TRAGGO_PORT to Port: converting 'asdasd' to type int. details: strconv.ParseInt: parsing "asdasd": invalid syntax`,
|
||||
Level: zerolog.FatalLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
}
|
||||
|
||||
func TestGet_prodMode_findFileRelativeToExecutable(t *testing.T) {
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
oldMode := mode.Get()
|
||||
mode.Set(mode.Prod)
|
||||
defer func() {
|
||||
mode.Set(oldMode)
|
||||
}()
|
||||
old := osExecutable
|
||||
defer func() {
|
||||
osExecutable = old
|
||||
}()
|
||||
osExecutable = func() (string, error) {
|
||||
return "./testpath/some.exe", nil
|
||||
}
|
||||
assert.Nil(t, os.MkdirAll("./testpath", 0777))
|
||||
defer os.RemoveAll("./testpath")
|
||||
|
||||
assert.Nil(t, ioutil.WriteFile("./testpath/.env", []byte("TRAGGO_PORT=6666"), 0777))
|
||||
|
||||
conf, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `Loading file ` + filepath.Join("testpath", ".env"),
|
||||
Level: zerolog.DebugLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
assert.Equal(t, 6666, conf.Port)
|
||||
}
|
||||
|
||||
func TestGet_errsOnOsExecutable(t *testing.T) {
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
oldMode := mode.Get()
|
||||
mode.Set(mode.Prod)
|
||||
defer func() {
|
||||
mode.Set(oldMode)
|
||||
}()
|
||||
old := osExecutable
|
||||
defer func() {
|
||||
osExecutable = old
|
||||
}()
|
||||
osExecutable = func() (string, error) {
|
||||
return "", errors.New("oops")
|
||||
}
|
||||
|
||||
_, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `Could not get path of executable using working directory instead. oops`,
|
||||
Level: zerolog.ErrorLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
}
|
||||
|
||||
func TestGet_withoutFilePermission(t *testing.T) {
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
old := osStat
|
||||
defer func() {
|
||||
osStat = old
|
||||
}()
|
||||
osStat = func(name string) (os.FileInfo, error) {
|
||||
if name == ".env" {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
return os.Stat(name)
|
||||
}
|
||||
|
||||
_, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `cannot read file .env because permission denied`,
|
||||
Level: zerolog.WarnLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
}
|
||||
|
||||
func TestGet_checksAbsoluteFiles(t *testing.T) {
|
||||
defer os.Unsetenv("TRAGGO_PORT")
|
||||
assert.Nil(t, os.MkdirAll("./absolutepath", 0777))
|
||||
defer os.RemoveAll("./absolutepath")
|
||||
path, err := filepath.Abs("./absolutepath/.env")
|
||||
assert.Nil(t, err)
|
||||
absoluteFiles = []string{path}
|
||||
defer func() {
|
||||
absoluteFiles = []string{}
|
||||
}()
|
||||
assert.Nil(t, ioutil.WriteFile("./absolutepath/.env", []byte("TRAGGO_PORT=7777"), 0777))
|
||||
|
||||
conf, logs := Get()
|
||||
|
||||
expected := []FutureLog{{
|
||||
Msg: `Loading file ` + path,
|
||||
Level: zerolog.DebugLevel,
|
||||
}}
|
||||
assert.Equal(t, expected, logs)
|
||||
assert.Equal(t, 7777, conf.Port)
|
||||
}
|
||||
10
config/error.go
Normal file
10
config/error.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
// FutureLog is an intermediate type for log messages. It is used before the config was loaded because without loaded
|
||||
// config we do not know the log level, so we log these messages once the config was initialized.
|
||||
type FutureLog struct {
|
||||
Level zerolog.Level
|
||||
Msg string
|
||||
}
|
||||
25
config/loglevel.go
Normal file
25
config/loglevel.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// LogLevel type that provides helper methods for decoding.
|
||||
type LogLevel zerolog.Level
|
||||
|
||||
// Decode decodes a string to a log level.
|
||||
func (ll *LogLevel) Decode(value string) error {
|
||||
if level, err := zerolog.ParseLevel(value); err == nil {
|
||||
*ll = LogLevel(level)
|
||||
return nil
|
||||
}
|
||||
*ll = LogLevel(zerolog.InfoLevel)
|
||||
return errors.New("unknown log level")
|
||||
}
|
||||
|
||||
// AsZeroLogLevel converts the LogLevel to a zerolog.Level.
|
||||
func (ll LogLevel) AsZeroLogLevel() zerolog.Level {
|
||||
return zerolog.Level(ll)
|
||||
}
|
||||
22
config/loglevel_test.go
Normal file
22
config/loglevel_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogLevel_Decode_success(t *testing.T) {
|
||||
ll := new(LogLevel)
|
||||
err := ll.Decode("fatal")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, ll.AsZeroLogLevel(), zerolog.FatalLevel)
|
||||
}
|
||||
|
||||
func TestLogLevel_Decode_fail(t *testing.T) {
|
||||
ll := new(LogLevel)
|
||||
err := ll.Decode("asdasdasdasdasdasd")
|
||||
assert.EqualError(t, err, "unknown log level")
|
||||
assert.Equal(t, ll.AsZeroLogLevel(), zerolog.InfoLevel)
|
||||
}
|
||||
20
config/mode/mode.go
Normal file
20
config/mode/mode.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package mode
|
||||
|
||||
const (
|
||||
// Dev for development mode
|
||||
Dev = "dev"
|
||||
// Prod for production mode
|
||||
Prod = "prod"
|
||||
)
|
||||
|
||||
var mode = Dev
|
||||
|
||||
// Set sets the new mode.
|
||||
func Set(newMode string) {
|
||||
mode = newMode
|
||||
}
|
||||
|
||||
// Get returns the current mode.
|
||||
func Get() string {
|
||||
return mode
|
||||
}
|
||||
17
config/mode/mode_test.go
Normal file
17
config/mode/mode_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package mode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
mode = Prod
|
||||
assert.Equal(t, Prod, Get())
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
Set(Prod)
|
||||
assert.Equal(t, Prod, mode)
|
||||
}
|
||||
Reference in New Issue
Block a user