Files
docker-compose-backup/internal/config/config.go
T
2026-07-04 17:20:28 +08:00

143 lines
4.1 KiB
Go

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
}