//go:build linux || freebsd // +build linux freebsd package config import ( "fmt" "os" "path/filepath" "strings" "time" "codeberg.org/petrbalvin/goget/internal/auth" "codeberg.org/petrbalvin/goget/internal/core" "codeberg.org/petrbalvin/interpres" ) // AuthConfig holds authentication configuration type AuthConfig struct { Username string `json:"username,omitempty" toml:"username,omitempty"` Password string `json:"password,omitempty" toml:"password,omitempty"` PasswordFile string `json:"password_file,omitempty" toml:"password_file,omitempty"` AuthType string `json:"auth_type,omitempty" toml:"auth_type,omitempty"` // basic, digest, auto OAuth auth.OAuthConfig `json:"oauth,omitempty" toml:"oauth,omitempty"` } // RecursiveConfig holds recursive download configuration type RecursiveConfig struct { Enabled bool `json:"enabled,omitempty" toml:"enabled,omitempty"` MaxDepth int `json:"max_depth,omitempty" toml:"max_depth,omitempty"` FollowExternal bool `json:"follow_external,omitempty" toml:"follow_external,omitempty"` ExcludePatterns []string `json:"exclude_patterns,omitempty" toml:"exclude_patterns,omitempty"` IncludePatterns []string `json:"include_patterns,omitempty" toml:"include_patterns,omitempty"` Delay string `json:"delay,omitempty" toml:"delay,omitempty"` Parallel int `json:"parallel,omitempty" toml:"parallel,omitempty"` } // ExtractConfig holds archive extraction configuration type ExtractConfig struct { Enabled bool `json:"enabled,omitempty" toml:"enabled,omitempty"` OutputDir string `json:"output_dir,omitempty" toml:"output_dir,omitempty"` StripComponents int `json:"strip_components,omitempty" toml:"strip_components,omitempty"` ExcludePatterns []string `json:"exclude_patterns,omitempty" toml:"exclude_patterns,omitempty"` PreservePermissions bool `json:"preserve_permissions,omitempty" toml:"preserve_permissions,omitempty"` AutoDetect bool `json:"auto_detect,omitempty" toml:"auto_detect,omitempty"` } // Config represents the application configuration type Config struct { Timeout time.Duration `json:"timeout,omitempty" toml:"timeout,omitempty"` Parallel int `json:"parallel,omitempty" toml:"parallel,omitempty"` OutputDir string `json:"output_dir,omitempty" toml:"output_dir,omitempty"` Verbose bool `json:"verbose,omitempty" toml:"verbose,omitempty"` Debug bool `json:"debug,omitempty" toml:"debug,omitempty"` IPv4Fallback bool `json:"ipv4_fallback,omitempty" toml:"ipv4_fallback,omitempty"` Proxy string `json:"proxy,omitempty" toml:"proxy,omitempty"` UserAgent string `json:"user_agent,omitempty" toml:"user_agent,omitempty"` AutoDecompress bool `json:"auto_decompress,omitempty" toml:"auto_decompress,omitempty"` InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty" toml:"insecure_skip_verify,omitempty"` ChecksumAlgo string `json:"checksum_algo,omitempty" toml:"checksum_algo,omitempty"` Auth AuthConfig `json:"auth,omitempty" toml:"auth,omitempty"` MaxSpeed int64 `json:"max_speed,omitempty" toml:"max_speed,omitempty"` AutoResume bool `json:"auto_resume,omitempty" toml:"auto_resume,omitempty"` KeepMetadata bool `json:"keep_metadata,omitempty" toml:"keep_metadata,omitempty"` Color string `json:"color,omitempty" toml:"color,omitempty"` ProgressStyle string `json:"progress_style,omitempty" toml:"progress_style,omitempty"` ShowETA bool `json:"show_eta,omitempty" toml:"show_eta,omitempty"` Recursive RecursiveConfig `json:"recursive,omitempty" toml:"recursive,omitempty"` Extract ExtractConfig `json:"extract,omitempty" toml:"extract,omitempty"` CookieJar string `json:"cookie_jar,omitempty" toml:"cookie_jar,omitempty"` DNSServers []string `json:"dns_servers,omitempty" toml:"dns_servers,omitempty"` MaxRetries int `json:"max_retries,omitempty" toml:"max_retries,omitempty"` configPath string } // DefaultConfig returns the default configuration func DefaultConfig() *Config { return &Config{ Timeout: 30 * time.Minute, Parallel: 0, OutputDir: ".", Verbose: true, Debug: false, IPv4Fallback: true, Proxy: "", UserAgent: core.Name + "/" + core.Version, AutoDecompress: true, InsecureSkipVerify: false, ChecksumAlgo: "sha256", Auth: AuthConfig{}, MaxSpeed: 0, AutoResume: true, KeepMetadata: false, Color: "auto", ProgressStyle: "unicode", ShowETA: true, Recursive: RecursiveConfig{ Enabled: false, MaxDepth: 3, FollowExternal: false, ExcludePatterns: []string{}, IncludePatterns: []string{}, Delay: "100ms", }, Extract: ExtractConfig{ Enabled: false, OutputDir: "", StripComponents: 0, ExcludePatterns: []string{}, PreservePermissions: true, AutoDetect: true, }, CookieJar: "", } } // Load loads the configuration from file func Load(customPath string) (*Config, error) { cfg := DefaultConfig() configPath := customPath if configPath == "" { configPath = getConfigPath() } if configPath == "" { return cfg, nil } cfg.configPath = configPath data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { return cfg, nil } return nil, fmt.Errorf("failed to read config: %w", err) } if err := interpres.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("failed to parse config %s: %w", configPath, err) } if cfg.Auth.PasswordFile != "" && cfg.Auth.Password == "" { passwordPath := expandTilde(cfg.Auth.PasswordFile) if data, err := os.ReadFile(passwordPath); err == nil { cfg.Auth.Password = strings.TrimSpace(string(data)) } } return cfg, nil } // Save writes the configuration to disk func (c *Config) Save() error { if c.configPath == "" { c.configPath = getConfigPath() if c.configPath == "" { return fmt.Errorf("cannot determine config path") } } if err := os.MkdirAll(filepath.Dir(c.configPath), 0755); err != nil { return err } cfgCopy := *c cfgCopy.Auth.Password = "" cfgCopy.Auth.OAuth.ClientSecret = "" cfgCopy.Auth.OAuth.AccessToken = "" cfgCopy.Auth.OAuth.RefreshToken = "" data, err := interpres.Marshal(cfgCopy) if err != nil { return err } return os.WriteFile(c.configPath, data, 0600) } // Path returns the config file path func (c *Config) Path() string { return c.configPath } // CLIFlags holds CLI overrides to merge into the configuration. type CLIFlags struct { Timeout time.Duration Parallel int Verbose bool Debug bool MaxSpeed int64 Proxy string NoIPv4 bool NoDecompress bool Username string Password string CookieJar string Recursive bool MaxDepth int FollowExternal bool ExcludePatterns []string Extract bool ExtractDir string StripComponents int } // Merge merges CLI overrides into the configuration. func (c *Config) Merge(cli *CLIFlags) { if cli.Timeout > 0 { c.Timeout = cli.Timeout } if cli.Parallel >= 0 { c.Parallel = cli.Parallel } if cli.Verbose { c.Verbose = true } if cli.Debug { c.Debug = true } if cli.MaxSpeed >= 0 { c.MaxSpeed = cli.MaxSpeed } if cli.Proxy != "" { c.Proxy = cli.Proxy } if cli.NoIPv4 { c.IPv4Fallback = false } if cli.NoDecompress { c.AutoDecompress = false } if cli.Username != "" { c.Auth.Username = cli.Username c.Auth.Password = cli.Password } if cli.CookieJar != "" { c.CookieJar = cli.CookieJar } if cli.Recursive { c.Recursive.Enabled = true } if cli.MaxDepth > 0 { c.Recursive.MaxDepth = cli.MaxDepth } if cli.FollowExternal { c.Recursive.FollowExternal = true } if len(cli.ExcludePatterns) > 0 { c.Recursive.ExcludePatterns = cli.ExcludePatterns } if cli.Extract { c.Extract.Enabled = true } if cli.ExtractDir != "" { c.Extract.OutputDir = cli.ExtractDir } if cli.StripComponents >= 0 { c.Extract.StripComponents = cli.StripComponents } } func getConfigPath() string { if path := os.Getenv("GOGET_CONFIG"); path != "" { return path } if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { return filepath.Join(xdg, "goget", "config.toml") } if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".config", "goget", "config.toml") } return "" } func expandTilde(path string) string { if strings.HasPrefix(path, "~/") { if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, path[2:]) } } return path } func (c *Config) GetEffectiveTimeout(fileSize int64, userOverride time.Duration) time.Duration { tc := core.DefaultTimeoutConfig() return tc.CalculateTimeout(fileSize, userOverride) } func (c *Config) GetParallelConnections(fileSize int64) int { if c.Parallel > 0 { return c.Parallel } if fileSize >= 100*1024*1024 { return 4 } return 1 } func (c *Config) ShouldUseColors() bool { switch c.Color { case "always": return true case "never": return false default: if os.Getenv("NO_COLOR") != "" { return false } return true } } func (c *Config) IsExcluded(url string) bool { for _, pattern := range c.Recursive.ExcludePatterns { if strings.Contains(url, pattern) { return true } } return false } func (c *Config) GetRecursiveDelay() time.Duration { if c.Recursive.Delay == "" { return 100 * time.Millisecond } d, err := time.ParseDuration(c.Recursive.Delay) if err != nil { return 100 * time.Millisecond } return d } func (c *Config) GetExtractOutputDir(defaultDir string) string { if c.Extract.OutputDir != "" { return c.Extract.OutputDir } return defaultDir } func ParseSpeed(s string) int64 { if s == "" || s == "0" { return 0 } s = strings.TrimSpace(strings.ToLower(s)) var multiplier int64 = 1 switch { case strings.HasSuffix(s, "tb/s"): multiplier = 1000 * 1000 * 1000 * 1000 s = strings.TrimSuffix(s, "tb/s") case strings.HasSuffix(s, "gb/s"): multiplier = 1000 * 1000 * 1000 s = strings.TrimSuffix(s, "gb/s") case strings.HasSuffix(s, "mb/s"): multiplier = 1000 * 1000 s = strings.TrimSuffix(s, "mb/s") case strings.HasSuffix(s, "kb/s"): multiplier = 1000 s = strings.TrimSuffix(s, "kb/s") case strings.HasSuffix(s, "/s"): s = strings.TrimSuffix(s, "/s") } var value float64 fmt.Sscanf(s, "%f", &value) return int64(value * float64(multiplier)) }