This commit represents a significant architectural overhaul of vaultik: Database Schema Changes: - Switch files table to use UUID primary keys instead of path-based keys - Add UUID primary keys to blobs table for immediate chunk association - Update all foreign key relationships to use UUIDs - Add comprehensive schema documentation in DATAMODEL.md - Add SQLite busy timeout handling for concurrent operations Streaming and Performance Improvements: - Implement true streaming blob packing without intermediate storage - Add streaming chunk processing to reduce memory usage - Improve progress reporting with real-time metrics - Add upload metrics tracking in new uploads table CLI Refactoring: - Restructure CLI to use subcommands: snapshot create/list/purge/verify - Add store info command for S3 configuration display - Add custom duration parser supporting days/weeks/months/years - Remove old backup.go in favor of enhanced snapshot.go - Add --cron flag for silent operation Configuration Changes: - Remove unused index_prefix configuration option - Add support for snapshot pruning retention policies - Improve configuration validation and error messages Testing Improvements: - Add comprehensive repository tests with edge cases - Add cascade delete debugging tests - Fix concurrent operation tests to use SQLite busy timeout - Remove tolerance for SQLITE_BUSY errors in tests Documentation: - Add MIT LICENSE file - Update README with new command structure - Add comprehensive DATAMODEL.md explaining database schema - Update DESIGN.md with UUID-based architecture Other Changes: - Add test-config.yml for testing - Update Makefile with better test output formatting - Fix various race conditions in concurrent operations - Improve error handling throughout
175 lines
5.5 KiB
Go
175 lines
5.5 KiB
Go
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),
|
|
)
|