357 lines
11 KiB
Go
357 lines
11 KiB
Go
//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))
|
||
|
|
}
|