snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
build / build (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-16 05:24:45 +05:00
commit cda7c1a474
272 changed files with 31973 additions and 0 deletions

104
config/config.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}