package config import ( "fmt" "os" "time" "go.uber.org/fx" "gopkg.in/yaml.v3" ) // Config represents the application configuration for Vaultik. // It defines all settings for backup operations, including source directories, // encryption recipients, S3 storage configuration, and performance tuning parameters. // Configuration is typically loaded from a YAML file. type Config struct { AgeRecipients []string `yaml:"age_recipients"` BackupInterval time.Duration `yaml:"backup_interval"` BlobSizeLimit Size `yaml:"blob_size_limit"` ChunkSize Size `yaml:"chunk_size"` Exclude []string `yaml:"exclude"` FullScanInterval time.Duration `yaml:"full_scan_interval"` Hostname string `yaml:"hostname"` IndexPath string `yaml:"index_path"` MinTimeBetweenRun time.Duration `yaml:"min_time_between_run"` S3 S3Config `yaml:"s3"` SourceDirs []string `yaml:"source_dirs"` CompressionLevel int `yaml:"compression_level"` } // S3Config represents S3 storage configuration for backup storage. // It supports both AWS S3 and S3-compatible storage services. // All fields except UseSSL and PartSize are required. type S3Config struct { Endpoint string `yaml:"endpoint"` Bucket string `yaml:"bucket"` Prefix string `yaml:"prefix"` AccessKeyID string `yaml:"access_key_id"` SecretAccessKey string `yaml:"secret_access_key"` Region string `yaml:"region"` UseSSL bool `yaml:"use_ssl"` PartSize Size `yaml:"part_size"` } // ConfigPath wraps the config file path for fx dependency injection. // This type allows the config file path to be injected as a distinct type // rather than a plain string, avoiding conflicts with other string dependencies. type ConfigPath string // New creates a new Config instance by loading from the specified path. // This function is used by the fx dependency injection framework. // Returns an error if the path is empty or if loading fails. func New(path ConfigPath) (*Config, error) { if path == "" { return nil, fmt.Errorf("config path not provided") } cfg, err := Load(string(path)) if err != nil { return nil, fmt.Errorf("failed to load config: %w", err) } return cfg, nil } // Load reads and parses the configuration file from the specified path. // It applies default values for optional fields, performs environment variable // substitution for certain fields (like IndexPath), and validates the configuration. // The configuration file should be in YAML format. Returns an error if the file // cannot be read, parsed, or if validation fails. func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } cfg := &Config{ // Set defaults BlobSizeLimit: Size(10 * 1024 * 1024 * 1024), // 10GB ChunkSize: Size(10 * 1024 * 1024), // 10MB BackupInterval: 1 * time.Hour, FullScanInterval: 24 * time.Hour, MinTimeBetweenRun: 15 * time.Minute, IndexPath: "/var/lib/vaultik/index.sqlite", CompressionLevel: 3, } if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("failed to parse config: %w", err) } // Check for environment variable override for IndexPath if envIndexPath := os.Getenv("VAULTIK_INDEX_PATH"); envIndexPath != "" { cfg.IndexPath = envIndexPath } // Get hostname if not set if cfg.Hostname == "" { hostname, err := os.Hostname() if err != nil { return nil, fmt.Errorf("failed to get hostname: %w", err) } cfg.Hostname = hostname } // Set default S3 settings if cfg.S3.Region == "" { cfg.S3.Region = "us-east-1" } if cfg.S3.PartSize == 0 { cfg.S3.PartSize = Size(5 * 1024 * 1024) // 5MB } if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } return cfg, nil } // Validate checks if the configuration is valid and complete. // It ensures all required fields are present and have valid values: // - At least one age recipient must be specified // - At least one source directory must be configured // - S3 credentials and endpoint must be provided // - Chunk size must be at least 1MB // - Blob size limit must be at least the chunk size // - Compression level must be between 1 and 19 // Returns an error describing the first validation failure encountered. func (c *Config) Validate() error { if len(c.AgeRecipients) == 0 { return fmt.Errorf("at least one age_recipient is required") } if len(c.SourceDirs) == 0 { return fmt.Errorf("at least one source directory is required") } if c.S3.Endpoint == "" { return fmt.Errorf("s3.endpoint is required") } if c.S3.Bucket == "" { return fmt.Errorf("s3.bucket is required") } if c.S3.AccessKeyID == "" { return fmt.Errorf("s3.access_key_id is required") } if c.S3.SecretAccessKey == "" { return fmt.Errorf("s3.secret_access_key is required") } if c.ChunkSize.Int64() < 1024*1024 { // 1MB minimum return fmt.Errorf("chunk_size must be at least 1MB") } if c.BlobSizeLimit.Int64() < c.ChunkSize.Int64() { return fmt.Errorf("blob_size_limit must be at least chunk_size") } if c.CompressionLevel < 1 || c.CompressionLevel > 19 { return fmt.Errorf("compression_level must be between 1 and 19") } return nil } // Module exports the config module for fx dependency injection. // It provides the Config type to other modules in the application. var Module = fx.Module("config", fx.Provide(New), )