package config import ( "fmt" "os" "strings" ) // Config is the top-level configuration. type Config struct { Global GlobalConfig `mapstructure:"global"` Projects []ProjectConfig `mapstructure:"projects"` Remote *RemoteConfig `mapstructure:"remote"` } // GlobalConfig holds global settings. type GlobalConfig struct { BackupDir string `mapstructure:"backup_dir"` TempDir string `mapstructure:"temp_dir"` } // ProjectConfig defines one Docker Compose project to back up. type ProjectConfig struct { Name string `mapstructure:"name"` Path string `mapstructure:"path"` ComposeFile string `mapstructure:"compose_file"` Cron string `mapstructure:"cron"` Exclude []string `mapstructure:"exclude"` Retention RetentionConfig `mapstructure:"retention"` } // RetentionConfig controls backup cleanup policy. type RetentionConfig struct { Count int `mapstructure:"count"` Days int `mapstructure:"days"` } // RemoteConfig holds optional remote upload targets. type RemoteConfig struct { S3 *S3Config `mapstructure:"s3"` WebDAV *WebDAVConfig `mapstructure:"webdav"` } // S3Config is the S3-compatible storage backend config. type S3Config struct { Endpoint string `mapstructure:"endpoint"` Bucket string `mapstructure:"bucket"` AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` Region string `mapstructure:"region"` PathPrefix string `mapstructure:"path_prefix"` } // WebDAVConfig is the WebDAV storage backend config. type WebDAVConfig struct { URL string `mapstructure:"url"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` } // DefaultRetentionCount is used when no retention count is specified. const DefaultRetentionCount = 7 // Validate checks the configuration and fills in defaults. func (c *Config) Validate() error { if c.Global.BackupDir == "" { return fmt.Errorf("global.backup_dir is required") } if c.Global.TempDir == "" { c.Global.TempDir = os.TempDir() } if len(c.Projects) == 0 { return fmt.Errorf("at least one project must be configured") } for i := range c.Projects { p := &c.Projects[i] if p.Name == "" { return fmt.Errorf("projects[%d]: name is required", i) } if p.Path == "" { return fmt.Errorf("projects[%d] (%s): path is required", i, p.Name) } if p.ComposeFile == "" { p.ComposeFile = "docker-compose.yml" } if p.Retention.Count <= 0 { p.Retention.Count = DefaultRetentionCount } } // Expand environment variable placeholders in remote configs and validate required fields. if c.Remote != nil { if c.Remote.S3 != nil { c.Remote.S3.AccessKey = expandEnv(c.Remote.S3.AccessKey) c.Remote.S3.SecretKey = expandEnv(c.Remote.S3.SecretKey) if strings.TrimSpace(c.Remote.S3.Bucket) == "" { return fmt.Errorf("remote.s3.bucket is required") } if strings.TrimSpace(c.Remote.S3.Region) == "" { return fmt.Errorf("remote.s3.region is required") } if strings.TrimSpace(c.Remote.S3.AccessKey) == "" { return fmt.Errorf("remote.s3.access_key is required") } if strings.TrimSpace(c.Remote.S3.SecretKey) == "" { return fmt.Errorf("remote.s3.secret_key is required") } } if c.Remote.WebDAV != nil { c.Remote.WebDAV.Username = expandEnv(c.Remote.WebDAV.Username) c.Remote.WebDAV.Password = expandEnv(c.Remote.WebDAV.Password) if strings.TrimSpace(c.Remote.WebDAV.URL) == "" { return fmt.Errorf("remote.webdav.url is required") } } } return nil } // expandEnv replaces ${VAR} or $VAR placeholders with environment values. func expandEnv(s string) string { return os.Expand(s, func(key string) string { return os.Getenv(key) }) } // HasRemote returns true if at least one remote backend is configured. func (c *Config) HasRemote() bool { return c.Remote != nil && (c.Remote.S3 != nil || c.Remote.WebDAV != nil) } // HasCron returns true if at least one project has a cron expression. func (c *Config) HasCron() bool { for _, p := range c.Projects { if strings.TrimSpace(p.Cron) != "" { return true } } return false }