feat: add environment variables configuration support (#1088)

This commit is contained in:
Levi authored and GitHub committed 2025-08-28 08:21:02 +08:00
1 parent 2fd2596aa0
commit 7ce0d876b0
8 files changed
+908 -82

No files matched your search

+13 -7
View File
@@ -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
View File
@@ -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
}
+698
View File
@@ -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
View File
@@ -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)
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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
View File
@@ -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)