mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 00:50:27 +00:00
feat: add environment variables configuration support (#1088)
This commit is contained in:
8 files changed
+908
-82
No files matched your search
@@ -53,7 +53,13 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Run Unit tests.
|
||||
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
|
||||
run: |
|
||||
# Get packages for testing and coverage (including cmd/web for flags_test.go)
|
||||
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
|
||||
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
|
||||
# Filter out main.go from coverage report to avoid lowering coverage percentage
|
||||
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
|
||||
mv coverage_filtered.txt coverage.txt
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
@@ -63,8 +69,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
||||
needs: [ test ]
|
||||
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
|
||||
needs: [test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -96,8 +102,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-14, ubuntu-latest ]
|
||||
needs: [ test ]
|
||||
os: [macos-14, ubuntu-latest]
|
||||
needs: [test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -129,7 +135,7 @@ jobs:
|
||||
build-web:
|
||||
name: Build web check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ test ]
|
||||
needs: [test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -149,7 +155,7 @@ jobs:
|
||||
build-docker:
|
||||
name: Build docker check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ test ]
|
||||
needs: [test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
|
||||
+170
-51
@@ -3,19 +3,23 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
@@ -23,62 +27,177 @@ type args struct {
|
||||
}
|
||||
|
||||
func parse() *args {
|
||||
var cliArgs args
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
cfg := &args{}
|
||||
|
||||
cliConfig := loadCliArgs()
|
||||
loadConfigFile(cfg, *cliConfig.configPath)
|
||||
loadEnvVars(cfg)
|
||||
// override with non-default command line arguments
|
||||
overrideWithCliArgs(cfg, cliConfig)
|
||||
// set default values for any unset fields
|
||||
setDefaults(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// loadCliArgs parses command line arguments and returns initial config
|
||||
func loadCliArgs() *args {
|
||||
cfg := &args{}
|
||||
cfg.Address = flag.String("A", "", "Bind Address")
|
||||
cfg.Port = flag.Int("P", 0, "Bind Port")
|
||||
cfg.Username = flag.String("u", "", "Web Authentication Username")
|
||||
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
|
||||
cfg.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
|
||||
// args priority: config file > cli args
|
||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||
if cfgArgs.Address == nil {
|
||||
cfgArgs.Address = cliArgs.Address
|
||||
// Parse white download directories from comma-separated string
|
||||
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
cfg.WhiteDownloadDirs = dirs
|
||||
}
|
||||
if cfgArgs.Port == nil {
|
||||
cfgArgs.Port = cliArgs.Port
|
||||
}
|
||||
if cfgArgs.Username == nil {
|
||||
cfgArgs.Username = cliArgs.Username
|
||||
}
|
||||
if cfgArgs.Password == nil {
|
||||
cfgArgs.Password = cliArgs.Password
|
||||
}
|
||||
if cfgArgs.ApiToken == nil {
|
||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||
}
|
||||
if cfgArgs.StorageDir == nil {
|
||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
||||
}
|
||||
return cfgArgs
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func loadConfig(path string) *args {
|
||||
var args args
|
||||
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||
func overrideWithCliArgs(cfg *args, cliConfig *args) {
|
||||
// Only override if the cli value is not empty/zero
|
||||
if cliConfig.Address != nil && *cliConfig.Address != "" {
|
||||
cfg.Address = cliConfig.Address
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
if cliConfig.Port != nil && *cliConfig.Port != 0 {
|
||||
cfg.Port = cliConfig.Port
|
||||
}
|
||||
|
||||
if cliConfig.Username != nil && *cliConfig.Username != "" {
|
||||
cfg.Username = cliConfig.Username
|
||||
}
|
||||
|
||||
if cliConfig.Password != nil && *cliConfig.Password != "" {
|
||||
cfg.Password = cliConfig.Password
|
||||
}
|
||||
|
||||
if cliConfig.ApiToken != nil && *cliConfig.ApiToken != "" {
|
||||
cfg.ApiToken = cliConfig.ApiToken
|
||||
}
|
||||
|
||||
if cliConfig.StorageDir != nil && *cliConfig.StorageDir != "" {
|
||||
cfg.StorageDir = cliConfig.StorageDir
|
||||
}
|
||||
|
||||
if cliConfig.WhiteDownloadDirs != nil {
|
||||
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaults sets default values for any unset configuration fields
|
||||
func setDefaults(cfg *args) {
|
||||
if cfg.Address == nil {
|
||||
address := "0.0.0.0"
|
||||
cfg.Address = &address
|
||||
}
|
||||
|
||||
if cfg.Port == nil {
|
||||
port := 9999
|
||||
cfg.Port = &port
|
||||
}
|
||||
|
||||
if cfg.Username == nil {
|
||||
username := "gopeed"
|
||||
cfg.Username = &username
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfigFile loads configuration from file
|
||||
func loadConfigFile(cfg *args, configPath string) {
|
||||
if !filepath.IsAbs(configPath) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||
return &args
|
||||
return
|
||||
}
|
||||
path = filepath.Join(dir, path)
|
||||
configPath = filepath.Join(dir, configPath)
|
||||
}
|
||||
file, err := os.ReadFile(path)
|
||||
|
||||
file, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &args
|
||||
return
|
||||
}
|
||||
fmt.Println("config file read failed, reason:" + err.Error())
|
||||
return &args
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(file, &args); err != nil {
|
||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
||||
return &args
|
||||
|
||||
if err = json.Unmarshal(file, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
|
||||
func loadEnvVars(cfg *args) {
|
||||
v := reflect.ValueOf(cfg).Elem()
|
||||
t := reflect.TypeOf(cfg).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// Get json tag as environment variable suffix
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" || jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove options like omitempty
|
||||
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
|
||||
jsonTag = jsonTag[:commaIdx]
|
||||
}
|
||||
|
||||
// Convert to uppercase and add GOPEED_ prefix
|
||||
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
|
||||
envValue := os.Getenv(envKey)
|
||||
|
||||
if envValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set value based on field type
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
// Create new pointer instance
|
||||
newVal := reflect.New(field.Type().Elem())
|
||||
field.Set(newVal)
|
||||
}
|
||||
|
||||
switch field.Type().Elem().Kind() {
|
||||
case reflect.String:
|
||||
field.Elem().SetString(envValue)
|
||||
case reflect.Int:
|
||||
if intVal, err := strconv.Atoi(envValue); err == nil {
|
||||
field.Elem().SetInt(int64(intVal))
|
||||
}
|
||||
default:
|
||||
// For complex types like DownloadConfig, try JSON unmarshaling
|
||||
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
|
||||
var config base.DownloaderStoreConfig
|
||||
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
|
||||
field.Set(reflect.ValueOf(&config))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field.Kind() == reflect.Slice {
|
||||
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
dirs := strings.Split(envValue, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
field.Set(reflect.ValueOf(dirs))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("config file loaded: %s\n", path)
|
||||
return &args
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
func TestSetDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *args
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "empty config should get defaults",
|
||||
input: &args{},
|
||||
expected: &args{
|
||||
Address: stringPtr("0.0.0.0"),
|
||||
Port: intPtr(9999),
|
||||
Username: stringPtr("gopeed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial config should only fill missing defaults",
|
||||
input: &args{
|
||||
Address: stringPtr("127.0.0.1"),
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("127.0.0.1"),
|
||||
Port: intPtr(9999),
|
||||
Username: stringPtr("gopeed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full config should remain unchanged",
|
||||
input: &args{
|
||||
Address: stringPtr("192.168.1.1"),
|
||||
Port: intPtr(8080),
|
||||
Username: stringPtr("admin"),
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("192.168.1.1"),
|
||||
Port: intPtr(8080),
|
||||
Username: stringPtr("admin"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setDefaults(tt.input)
|
||||
if !reflect.DeepEqual(tt.input, tt.expected) {
|
||||
t.Errorf("setDefaults() = %+v, want %+v", tt.input, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrideWithCliArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *args
|
||||
cliConfig *args
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "empty cli should not override",
|
||||
config: &args{Address: stringPtr("192.168.1.1")},
|
||||
cliConfig: &args{
|
||||
Address: stringPtr(""),
|
||||
Port: intPtr(0),
|
||||
},
|
||||
expected: &args{Address: stringPtr("192.168.1.1")},
|
||||
},
|
||||
{
|
||||
name: "non-empty cli should override",
|
||||
config: &args{Address: stringPtr("192.168.1.1")},
|
||||
cliConfig: &args{
|
||||
Address: stringPtr("127.0.0.1"),
|
||||
Port: intPtr(8080),
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("127.0.0.1"),
|
||||
Port: intPtr(8080),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all fields override test",
|
||||
config: &args{
|
||||
Address: stringPtr("old_address"),
|
||||
Port: intPtr(1111),
|
||||
Username: stringPtr("old_user"),
|
||||
Password: stringPtr("old_pass"),
|
||||
ApiToken: stringPtr("old_token"),
|
||||
StorageDir: stringPtr("old_dir"),
|
||||
},
|
||||
cliConfig: &args{
|
||||
Address: stringPtr("new_address"),
|
||||
Port: intPtr(2222),
|
||||
Username: stringPtr("new_user"),
|
||||
Password: stringPtr("new_pass"),
|
||||
ApiToken: stringPtr("new_token"),
|
||||
StorageDir: stringPtr("new_dir"),
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("new_address"),
|
||||
Port: intPtr(2222),
|
||||
Username: stringPtr("new_user"),
|
||||
Password: stringPtr("new_pass"),
|
||||
ApiToken: stringPtr("new_token"),
|
||||
StorageDir: stringPtr("new_dir"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
overrideWithCliArgs(tt.config, tt.cliConfig)
|
||||
if !reflect.DeepEqual(tt.config, tt.expected) {
|
||||
t.Errorf("overrideWithCliArgs() = %+v, want %+v", tt.config, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFile(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir, err := os.MkdirTemp("", "flags_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configData string
|
||||
fileName string
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "valid config file",
|
||||
configData: `{
|
||||
"address": "192.168.1.100",
|
||||
"port": 8080,
|
||||
"username": "testuser",
|
||||
"password": "testpass",
|
||||
"apiToken": "testtoken",
|
||||
"storageDir": "/test/storage",
|
||||
"downloadConfig": {
|
||||
"downloadDir": "/test/downloads",
|
||||
"maxRunning": 10
|
||||
}
|
||||
}`,
|
||||
fileName: "valid_config.json",
|
||||
expected: &args{
|
||||
Address: stringPtr("192.168.1.100"),
|
||||
Port: intPtr(8080),
|
||||
Username: stringPtr("testuser"),
|
||||
Password: stringPtr("testpass"),
|
||||
ApiToken: stringPtr("testtoken"),
|
||||
StorageDir: stringPtr("/test/storage"),
|
||||
DownloadConfig: &base.DownloaderStoreConfig{
|
||||
DownloadDir: "/test/downloads",
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial config file",
|
||||
configData: `{
|
||||
"address": "10.0.0.1",
|
||||
"port": 3000
|
||||
}`,
|
||||
fileName: "partial_config.json",
|
||||
expected: &args{
|
||||
Address: stringPtr("10.0.0.1"),
|
||||
Port: intPtr(3000),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid json should not panic",
|
||||
configData: `{invalid json}`,
|
||||
fileName: "invalid_config.json",
|
||||
expected: &args{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test config file
|
||||
configPath := filepath.Join(tempDir, tt.fileName)
|
||||
err := os.WriteFile(configPath, []byte(tt.configData), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg := &args{}
|
||||
loadConfigFile(cfg, configPath)
|
||||
|
||||
if !reflect.DeepEqual(cfg, tt.expected) {
|
||||
t.Errorf("loadConfigFile() = %+v, want %+v", cfg, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test non-existent file
|
||||
t.Run("non-existent file", func(t *testing.T) {
|
||||
cfg := &args{}
|
||||
loadConfigFile(cfg, "/non/existent/file.json")
|
||||
expected := &args{}
|
||||
if !reflect.DeepEqual(cfg, expected) {
|
||||
t.Errorf("loadConfigFile() with non-existent file = %+v, want %+v", cfg, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadEnvVars(t *testing.T) {
|
||||
// Save original environment
|
||||
originalEnv := make(map[string]string)
|
||||
envKeys := []string{
|
||||
"GOPEED_ADDRESS", "GOPEED_PORT", "GOPEED_USERNAME",
|
||||
"GOPEED_PASSWORD", "GOPEED_APITOKEN", "GOPEED_STORAGEDIR",
|
||||
"GOPEED_DOWNLOADCONFIG", "GOPEED_WHITEDOWNLOADDIRS",
|
||||
}
|
||||
for _, key := range envKeys {
|
||||
originalEnv[key] = os.Getenv(key)
|
||||
}
|
||||
|
||||
// Clean up function
|
||||
cleanup := func() {
|
||||
for _, key := range envKeys {
|
||||
if val, exists := originalEnv[key]; exists {
|
||||
os.Setenv(key, val)
|
||||
} else {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envVars map[string]string
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "all environment variables set",
|
||||
envVars: map[string]string{
|
||||
"GOPEED_ADDRESS": "env.example.com",
|
||||
"GOPEED_PORT": "7777",
|
||||
"GOPEED_USERNAME": "envuser",
|
||||
"GOPEED_PASSWORD": "envpass",
|
||||
"GOPEED_APITOKEN": "envtoken",
|
||||
"GOPEED_STORAGEDIR": "/env/storage",
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("env.example.com"),
|
||||
Port: intPtr(7777),
|
||||
Username: stringPtr("envuser"),
|
||||
Password: stringPtr("envpass"),
|
||||
ApiToken: stringPtr("envtoken"),
|
||||
StorageDir: stringPtr("/env/storage"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial environment variables",
|
||||
envVars: map[string]string{
|
||||
"GOPEED_ADDRESS": "partial.example.com",
|
||||
"GOPEED_PORT": "5555",
|
||||
},
|
||||
expected: &args{
|
||||
Address: stringPtr("partial.example.com"),
|
||||
Port: intPtr(5555),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "downloadConfig from environment",
|
||||
envVars: map[string]string{
|
||||
"GOPEED_DOWNLOADCONFIG": `{"downloadDir": "/env/downloads", "maxRunning": 15}`,
|
||||
},
|
||||
expected: &args{
|
||||
DownloadConfig: &base.DownloaderStoreConfig{
|
||||
DownloadDir: "/env/downloads",
|
||||
MaxRunning: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid port should be ignored",
|
||||
envVars: map[string]string{
|
||||
"GOPEED_PORT": "invalid_port",
|
||||
},
|
||||
expected: &args{
|
||||
Port: intPtr(0), // Invalid port creates pointer with 0 value
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid json for downloadConfig should be ignored",
|
||||
envVars: map[string]string{
|
||||
"GOPEED_DOWNLOADCONFIG": `{invalid json}`,
|
||||
},
|
||||
expected: &args{
|
||||
DownloadConfig: &base.DownloaderStoreConfig{}, // Invalid JSON creates empty config
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clear all environment variables first
|
||||
for _, key := range envKeys {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
|
||||
// Set test environment variables
|
||||
for key, value := range tt.envVars {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
|
||||
cfg := &args{}
|
||||
loadEnvVars(cfg)
|
||||
|
||||
if !reflect.DeepEqual(cfg, tt.expected) {
|
||||
t.Errorf("loadEnvVars() = %+v, want %+v", cfg, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPriority(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir, err := os.MkdirTemp("", "flags_test_priority")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test config file
|
||||
configData := `{
|
||||
"address": "config.example.com",
|
||||
"port": 6666,
|
||||
"username": "configuser",
|
||||
"password": "configpass"
|
||||
}`
|
||||
configPath := filepath.Join(tempDir, "test_config.json")
|
||||
err = os.WriteFile(configPath, []byte(configData), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Save original environment
|
||||
originalEnv := make(map[string]string)
|
||||
envKeys := []string{"GOPEED_ADDRESS", "GOPEED_PORT", "GOPEED_USERNAME"}
|
||||
for _, key := range envKeys {
|
||||
originalEnv[key] = os.Getenv(key)
|
||||
}
|
||||
defer func() {
|
||||
for _, key := range envKeys {
|
||||
if val, exists := originalEnv[key]; exists {
|
||||
os.Setenv(key, val)
|
||||
} else {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Set environment variables
|
||||
os.Setenv("GOPEED_ADDRESS", "env.example.com")
|
||||
os.Setenv("GOPEED_PORT", "7777")
|
||||
|
||||
// Test configuration priority: CLI > ENV > Config File > Defaults
|
||||
cfg := &args{}
|
||||
|
||||
// Load config file
|
||||
loadConfigFile(cfg, configPath)
|
||||
|
||||
// Load environment variables (should override config file)
|
||||
loadEnvVars(cfg)
|
||||
|
||||
// Simulate CLI args (should override env vars)
|
||||
cliConfig := &args{
|
||||
Address: stringPtr("cli.example.com"),
|
||||
// Port not set in CLI, should remain from env
|
||||
// Username not set in CLI or env, should remain from config file
|
||||
}
|
||||
overrideWithCliArgs(cfg, cliConfig)
|
||||
|
||||
// Set defaults for missing fields
|
||||
setDefaults(cfg)
|
||||
|
||||
expected := &args{
|
||||
Address: stringPtr("cli.example.com"), // From CLI (highest priority)
|
||||
Port: intPtr(7777), // From ENV (overrides config file)
|
||||
Username: stringPtr("configuser"), // From config file (env not set)
|
||||
Password: stringPtr("configpass"), // From config file only
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg, expected) {
|
||||
t.Errorf("Configuration priority test failed.\nGot: %+v\nWant: %+v", cfg, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteConfigurationFlow(t *testing.T) {
|
||||
// This test simulates a complete configuration loading scenario
|
||||
// with all sources: defaults, config file, environment variables, and CLI args
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "flags_test_complete")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create comprehensive config file
|
||||
configData := `{
|
||||
"address": "config.host.com",
|
||||
"port": 5000,
|
||||
"username": "configuser",
|
||||
"password": "configpass",
|
||||
"apiToken": "configtoken",
|
||||
"storageDir": "/config/storage",
|
||||
"downloadConfig": {
|
||||
"downloadDir": "/config/downloads",
|
||||
"maxRunning": 8,
|
||||
"protocolConfig": {"http": {"maxConnections": 16}},
|
||||
"proxy": {
|
||||
"enable": true,
|
||||
"system": false,
|
||||
"scheme": "http",
|
||||
"host": "proxy.example.com:8080"
|
||||
}
|
||||
}
|
||||
}`
|
||||
configPath := filepath.Join(tempDir, "complete_config.json")
|
||||
err = os.WriteFile(configPath, []byte(configData), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Save and set environment variables
|
||||
originalEnv := make(map[string]string)
|
||||
envKeys := []string{"GOPEED_ADDRESS", "GOPEED_USERNAME", "GOPEED_APITOKEN", "GOPEED_WHITEDOWNLOADDIRS"}
|
||||
for _, key := range envKeys {
|
||||
originalEnv[key] = os.Getenv(key)
|
||||
}
|
||||
defer func() {
|
||||
for _, key := range envKeys {
|
||||
if val, exists := originalEnv[key]; exists {
|
||||
os.Setenv(key, val)
|
||||
} else {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("GOPEED_ADDRESS", "env.host.com")
|
||||
os.Setenv("GOPEED_USERNAME", "envuser")
|
||||
|
||||
// Simulate complete configuration loading
|
||||
cfg := &args{}
|
||||
|
||||
// Load from config file
|
||||
loadConfigFile(cfg, configPath)
|
||||
|
||||
// Override with environment variables
|
||||
loadEnvVars(cfg)
|
||||
|
||||
// Override with CLI arguments
|
||||
cliConfig := &args{
|
||||
Port: intPtr(9090),
|
||||
StorageDir: stringPtr("/cli/storage"),
|
||||
}
|
||||
overrideWithCliArgs(cfg, cliConfig)
|
||||
|
||||
// Set defaults
|
||||
setDefaults(cfg)
|
||||
|
||||
// Verify the final configuration follows priority rules
|
||||
if *cfg.Address != "env.host.com" {
|
||||
t.Errorf("Address should be from environment, got %s", *cfg.Address)
|
||||
}
|
||||
if *cfg.Port != 9090 {
|
||||
t.Errorf("Port should be from CLI, got %d", *cfg.Port)
|
||||
}
|
||||
if *cfg.Username != "envuser" {
|
||||
t.Errorf("Username should be from environment, got %s", *cfg.Username)
|
||||
}
|
||||
if *cfg.Password != "configpass" {
|
||||
t.Errorf("Password should be from config file, got %s", *cfg.Password)
|
||||
}
|
||||
if *cfg.ApiToken != "configtoken" {
|
||||
t.Errorf("ApiToken should be from config file, got %s", *cfg.ApiToken)
|
||||
}
|
||||
if *cfg.StorageDir != "/cli/storage" {
|
||||
t.Errorf("StorageDir should be from CLI, got %s", *cfg.StorageDir)
|
||||
}
|
||||
if cfg.DownloadConfig == nil {
|
||||
t.Error("DownloadConfig should be loaded from config file")
|
||||
} else {
|
||||
if cfg.DownloadConfig.DownloadDir != "/config/downloads" {
|
||||
t.Errorf("DownloadConfig.DownloadDir should be from config file, got %s", cfg.DownloadConfig.DownloadDir)
|
||||
}
|
||||
if cfg.DownloadConfig.MaxRunning != 8 {
|
||||
t.Errorf("DownloadConfig.MaxRunning should be from config file, got %d", cfg.DownloadConfig.MaxRunning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for creating pointers
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func intPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func TestWhiteDownloadDirs(t *testing.T) {
|
||||
t.Run("overrideWithCliArgs should handle WhiteDownloadDirs", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *args
|
||||
cliConfig *args
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "nil cli WhiteDownloadDirs should not override",
|
||||
config: &args{WhiteDownloadDirs: []string{"/old/dir1", "/old/dir2"}},
|
||||
cliConfig: &args{
|
||||
WhiteDownloadDirs: nil,
|
||||
},
|
||||
expected: &args{WhiteDownloadDirs: []string{"/old/dir1", "/old/dir2"}},
|
||||
},
|
||||
{
|
||||
name: "non-nil cli WhiteDownloadDirs should override",
|
||||
config: &args{WhiteDownloadDirs: []string{"/old/dir1"}},
|
||||
cliConfig: &args{
|
||||
WhiteDownloadDirs: []string{"/new/dir1", "/new/dir2"},
|
||||
},
|
||||
expected: &args{WhiteDownloadDirs: []string{"/new/dir1", "/new/dir2"}},
|
||||
},
|
||||
{
|
||||
name: "empty cli WhiteDownloadDirs should override with empty slice",
|
||||
config: &args{WhiteDownloadDirs: []string{"/old/dir1"}},
|
||||
cliConfig: &args{
|
||||
WhiteDownloadDirs: []string{},
|
||||
},
|
||||
expected: &args{WhiteDownloadDirs: []string{}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
overrideWithCliArgs(tt.config, tt.cliConfig)
|
||||
if !reflect.DeepEqual(tt.config, tt.expected) {
|
||||
t.Errorf("overrideWithCliArgs() = %+v, want %+v", tt.config, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loadConfigFile should handle WhiteDownloadDirs", func(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tempDir, err := os.MkdirTemp("", "flags_test_whitedirs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configData string
|
||||
fileName string
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "config with WhiteDownloadDirs array",
|
||||
configData: `{
|
||||
"address": "test.example.com",
|
||||
"whiteDownloadDirs": ["/path/to/dir1", "/path/to/dir2", "/path/to/dir3"]
|
||||
}`,
|
||||
fileName: "whitedirs_config.json",
|
||||
expected: &args{
|
||||
Address: stringPtr("test.example.com"),
|
||||
WhiteDownloadDirs: []string{"/path/to/dir1", "/path/to/dir2", "/path/to/dir3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with empty WhiteDownloadDirs array",
|
||||
configData: `{
|
||||
"address": "test.example.com",
|
||||
"whiteDownloadDirs": []
|
||||
}`,
|
||||
fileName: "empty_whitedirs_config.json",
|
||||
expected: &args{
|
||||
Address: stringPtr("test.example.com"),
|
||||
WhiteDownloadDirs: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config without WhiteDownloadDirs",
|
||||
configData: `{
|
||||
"address": "test.example.com",
|
||||
"port": 8080
|
||||
}`,
|
||||
fileName: "no_whitedirs_config.json",
|
||||
expected: &args{
|
||||
Address: stringPtr("test.example.com"),
|
||||
Port: intPtr(8080),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test config file
|
||||
configPath := filepath.Join(tempDir, tt.fileName)
|
||||
err := os.WriteFile(configPath, []byte(tt.configData), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg := &args{}
|
||||
loadConfigFile(cfg, configPath)
|
||||
|
||||
if !reflect.DeepEqual(cfg, tt.expected) {
|
||||
t.Errorf("loadConfigFile() = %+v, want %+v", cfg, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loadEnvVars should handle WhiteDownloadDirs", func(t *testing.T) {
|
||||
// Save original environment
|
||||
originalEnv := os.Getenv("GOPEED_WHITEDOWNLOADDIRS")
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("GOPEED_WHITEDOWNLOADDIRS", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("GOPEED_WHITEDOWNLOADDIRS")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expected *args
|
||||
}{
|
||||
{
|
||||
name: "comma-separated directories",
|
||||
envValue: "/env/dir1,/env/dir2,/env/dir3",
|
||||
expected: &args{
|
||||
WhiteDownloadDirs: []string{"/env/dir1", "/env/dir2", "/env/dir3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comma-separated with spaces",
|
||||
envValue: " /env/dir1 , /env/dir2 , /env/dir3 ",
|
||||
expected: &args{
|
||||
WhiteDownloadDirs: []string{"/env/dir1", "/env/dir2", "/env/dir3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single directory",
|
||||
envValue: "/single/dir",
|
||||
expected: &args{
|
||||
WhiteDownloadDirs: []string{"/single/dir"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty string should create empty slice",
|
||||
envValue: "",
|
||||
expected: &args{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clear environment variable first
|
||||
os.Unsetenv("GOPEED_WHITEDOWNLOADDIRS")
|
||||
|
||||
if tt.envValue != "" {
|
||||
os.Setenv("GOPEED_WHITEDOWNLOADDIRS", tt.envValue)
|
||||
}
|
||||
|
||||
cfg := &args{}
|
||||
loadEnvVars(cfg)
|
||||
|
||||
if !reflect.DeepEqual(cfg, tt.expected) {
|
||||
t.Errorf("loadEnvVars() = %+v, want %+v", cfg, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
+17
-15
@@ -6,11 +6,12 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
@@ -31,28 +32,29 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
var storageDir string
|
||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||
dir = *args.StorageDir
|
||||
storageDir = *args.StorageDir
|
||||
} else {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dir = filepath.Dir(exe)
|
||||
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||
}
|
||||
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: storageDir,
|
||||
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
}
|
||||
|
||||
@@ -941,9 +941,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
||||
}
|
||||
|
||||
// if enable white download directory, check if the download directory is in the white list
|
||||
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
||||
if len(d.cfg.WhiteDownloadDirs) > 0 {
|
||||
inWhiteList := false
|
||||
for _, dir := range d.cfg.DownloadDirWhiteList {
|
||||
for _, dir := range d.cfg.WhiteDownloadDirs {
|
||||
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||
inWhiteList = true
|
||||
break
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
|
||||
defer listener.Close()
|
||||
|
||||
downloader := NewDownloader(&DownloaderConfig{
|
||||
DownloadDirWhiteList: []string{"./downloads"},
|
||||
WhiteDownloadDirs: []string{"./downloads"},
|
||||
})
|
||||
if err := downloader.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
|
||||
Controller *controller.Controller
|
||||
FetchManagers []fetcher.FetcherManager
|
||||
|
||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
StorageDir string
|
||||
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
||||
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
StorageDir string
|
||||
WhiteDownloadDirs []string
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
|
||||
+3
-2
@@ -83,8 +83,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
startCfg.Init()
|
||||
|
||||
downloadCfg := &download.DownloaderConfig{
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
|
||||
}
|
||||
if startCfg.Storage == model.StorageBolt {
|
||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||
|
||||
Reference in new issue
Block a user