Some checks failed
CI/CD / Test (push) Has been cancelled
CI/CD / Integration Tests (push) Has been cancelled
CI/CD / Native Engine Tests (push) Has been cancelled
CI/CD / Lint (push) Has been cancelled
CI/CD / Build Binary (push) Has been cancelled
CI/CD / Test Release Build (push) Has been cancelled
CI/CD / Release Binaries (push) Has been cancelled
- Config now searches: ./ → ~/ → /etc/dbbackup.conf → /etc/dbbackup/dbbackup.conf - Works for postgres user with home at /var/lib/postgresql - Added ConfigSearchPaths() and LoadLocalConfigWithPath() - Log shows which config path was loaded
357 lines
9.4 KiB
Go
Executable File
357 lines
9.4 KiB
Go
Executable File
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const ConfigFileName = ".dbbackup.conf"
|
|
|
|
// LocalConfig represents a saved configuration in the current directory
|
|
type LocalConfig struct {
|
|
// Database settings
|
|
DBType string
|
|
Host string
|
|
Port int
|
|
User string
|
|
Database string
|
|
SSLMode string
|
|
|
|
// Backup settings
|
|
BackupDir string
|
|
WorkDir string // Working directory for large operations
|
|
Compression int
|
|
Jobs int
|
|
DumpJobs int
|
|
|
|
// Performance settings
|
|
CPUWorkload string
|
|
MaxCores int
|
|
ClusterTimeout int // Cluster operation timeout in minutes (default: 1440 = 24 hours)
|
|
ResourceProfile string
|
|
LargeDBMode bool // Enable large database mode (reduces parallelism, increases locks)
|
|
|
|
// Security settings
|
|
RetentionDays int
|
|
MinBackups int
|
|
MaxRetries int
|
|
}
|
|
|
|
// ConfigSearchPaths returns all paths where config files are searched, in order of priority
|
|
func ConfigSearchPaths() []string {
|
|
paths := []string{
|
|
filepath.Join(".", ConfigFileName), // Current directory (highest priority)
|
|
}
|
|
|
|
// User's home directory
|
|
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
|
paths = append(paths, filepath.Join(home, ConfigFileName))
|
|
}
|
|
|
|
// System-wide config locations
|
|
paths = append(paths,
|
|
"/etc/dbbackup.conf",
|
|
"/etc/dbbackup/dbbackup.conf",
|
|
)
|
|
|
|
return paths
|
|
}
|
|
|
|
// LoadLocalConfig loads configuration from .dbbackup.conf
|
|
// Search order: 1) current directory, 2) user's home directory, 3) /etc/dbbackup.conf, 4) /etc/dbbackup/dbbackup.conf
|
|
func LoadLocalConfig() (*LocalConfig, error) {
|
|
for _, path := range ConfigSearchPaths() {
|
|
cfg, err := LoadLocalConfigFromPath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg != nil {
|
|
return cfg, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// LoadLocalConfigWithPath loads configuration and returns the path it was loaded from
|
|
func LoadLocalConfigWithPath() (*LocalConfig, string, error) {
|
|
for _, path := range ConfigSearchPaths() {
|
|
cfg, err := LoadLocalConfigFromPath(path)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if cfg != nil {
|
|
return cfg, path, nil
|
|
}
|
|
}
|
|
return nil, "", nil
|
|
}
|
|
|
|
// LoadLocalConfigFromPath loads configuration from a specific path
|
|
func LoadLocalConfigFromPath(configPath string) (*LocalConfig, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil // No config file, not an error
|
|
}
|
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
cfg := &LocalConfig{}
|
|
lines := strings.Split(string(data), "\n")
|
|
currentSection := ""
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines and comments
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
// Section headers
|
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
currentSection = strings.Trim(line, "[]")
|
|
continue
|
|
}
|
|
|
|
// Key-value pairs
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch currentSection {
|
|
case "database":
|
|
switch key {
|
|
case "type":
|
|
cfg.DBType = value
|
|
case "host":
|
|
cfg.Host = value
|
|
case "port":
|
|
if p, err := strconv.Atoi(value); err == nil {
|
|
cfg.Port = p
|
|
}
|
|
case "user":
|
|
cfg.User = value
|
|
case "database":
|
|
cfg.Database = value
|
|
case "ssl_mode":
|
|
cfg.SSLMode = value
|
|
}
|
|
case "backup":
|
|
switch key {
|
|
case "backup_dir":
|
|
cfg.BackupDir = value
|
|
case "work_dir":
|
|
cfg.WorkDir = value
|
|
case "compression":
|
|
if c, err := strconv.Atoi(value); err == nil {
|
|
cfg.Compression = c
|
|
}
|
|
case "jobs":
|
|
if j, err := strconv.Atoi(value); err == nil {
|
|
cfg.Jobs = j
|
|
}
|
|
case "dump_jobs":
|
|
if dj, err := strconv.Atoi(value); err == nil {
|
|
cfg.DumpJobs = dj
|
|
}
|
|
}
|
|
case "performance":
|
|
switch key {
|
|
case "cpu_workload":
|
|
cfg.CPUWorkload = value
|
|
case "max_cores":
|
|
if mc, err := strconv.Atoi(value); err == nil {
|
|
cfg.MaxCores = mc
|
|
}
|
|
case "cluster_timeout":
|
|
if ct, err := strconv.Atoi(value); err == nil {
|
|
cfg.ClusterTimeout = ct
|
|
}
|
|
case "resource_profile":
|
|
cfg.ResourceProfile = value
|
|
case "large_db_mode":
|
|
cfg.LargeDBMode = value == "true" || value == "1"
|
|
}
|
|
case "security":
|
|
switch key {
|
|
case "retention_days":
|
|
if rd, err := strconv.Atoi(value); err == nil {
|
|
cfg.RetentionDays = rd
|
|
}
|
|
case "min_backups":
|
|
if mb, err := strconv.Atoi(value); err == nil {
|
|
cfg.MinBackups = mb
|
|
}
|
|
case "max_retries":
|
|
if mr, err := strconv.Atoi(value); err == nil {
|
|
cfg.MaxRetries = mr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// SaveLocalConfig saves configuration to .dbbackup.conf in current directory
|
|
func SaveLocalConfig(cfg *LocalConfig) error {
|
|
return SaveLocalConfigToPath(cfg, filepath.Join(".", ConfigFileName))
|
|
}
|
|
|
|
// SaveLocalConfigToPath saves configuration to a specific path
|
|
func SaveLocalConfigToPath(cfg *LocalConfig, configPath string) error {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("# dbbackup configuration\n")
|
|
sb.WriteString("# This file is auto-generated. Edit with care.\n")
|
|
sb.WriteString(fmt.Sprintf("# Saved: %s\n\n", time.Now().Format(time.RFC3339)))
|
|
|
|
// Database section - ALWAYS write all values
|
|
sb.WriteString("[database]\n")
|
|
sb.WriteString(fmt.Sprintf("type = %s\n", cfg.DBType))
|
|
sb.WriteString(fmt.Sprintf("host = %s\n", cfg.Host))
|
|
sb.WriteString(fmt.Sprintf("port = %d\n", cfg.Port))
|
|
sb.WriteString(fmt.Sprintf("user = %s\n", cfg.User))
|
|
sb.WriteString(fmt.Sprintf("database = %s\n", cfg.Database))
|
|
sb.WriteString(fmt.Sprintf("ssl_mode = %s\n", cfg.SSLMode))
|
|
sb.WriteString("\n")
|
|
|
|
// Backup section - ALWAYS write all values (including 0)
|
|
sb.WriteString("[backup]\n")
|
|
sb.WriteString(fmt.Sprintf("backup_dir = %s\n", cfg.BackupDir))
|
|
if cfg.WorkDir != "" {
|
|
sb.WriteString(fmt.Sprintf("work_dir = %s\n", cfg.WorkDir))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("compression = %d\n", cfg.Compression))
|
|
sb.WriteString(fmt.Sprintf("jobs = %d\n", cfg.Jobs))
|
|
sb.WriteString(fmt.Sprintf("dump_jobs = %d\n", cfg.DumpJobs))
|
|
sb.WriteString("\n")
|
|
|
|
// Performance section - ALWAYS write all values
|
|
sb.WriteString("[performance]\n")
|
|
sb.WriteString(fmt.Sprintf("cpu_workload = %s\n", cfg.CPUWorkload))
|
|
sb.WriteString(fmt.Sprintf("max_cores = %d\n", cfg.MaxCores))
|
|
sb.WriteString(fmt.Sprintf("cluster_timeout = %d\n", cfg.ClusterTimeout))
|
|
if cfg.ResourceProfile != "" {
|
|
sb.WriteString(fmt.Sprintf("resource_profile = %s\n", cfg.ResourceProfile))
|
|
}
|
|
sb.WriteString(fmt.Sprintf("large_db_mode = %t\n", cfg.LargeDBMode))
|
|
sb.WriteString("\n")
|
|
|
|
// Security section - ALWAYS write all values
|
|
sb.WriteString("[security]\n")
|
|
sb.WriteString(fmt.Sprintf("retention_days = %d\n", cfg.RetentionDays))
|
|
sb.WriteString(fmt.Sprintf("min_backups = %d\n", cfg.MinBackups))
|
|
sb.WriteString(fmt.Sprintf("max_retries = %d\n", cfg.MaxRetries))
|
|
|
|
// Use 0644 permissions for readability
|
|
if err := os.WriteFile(configPath, []byte(sb.String()), 0644); err != nil {
|
|
return fmt.Errorf("failed to write config file %s: %w", configPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyLocalConfig applies loaded local config to the main config.
|
|
// All non-empty/non-zero values from the config file are applied.
|
|
// CLI flag overrides are handled separately in root.go after this function.
|
|
func ApplyLocalConfig(cfg *Config, local *LocalConfig) {
|
|
if local == nil {
|
|
return
|
|
}
|
|
|
|
// Apply all non-empty values from config file
|
|
// CLI flags override these in root.go after ApplyLocalConfig is called
|
|
if local.DBType != "" {
|
|
cfg.DatabaseType = local.DBType
|
|
}
|
|
if local.Host != "" {
|
|
cfg.Host = local.Host
|
|
}
|
|
if local.Port != 0 {
|
|
cfg.Port = local.Port
|
|
}
|
|
if local.User != "" {
|
|
cfg.User = local.User
|
|
}
|
|
if local.Database != "" {
|
|
cfg.Database = local.Database
|
|
}
|
|
if local.SSLMode != "" {
|
|
cfg.SSLMode = local.SSLMode
|
|
}
|
|
if local.BackupDir != "" {
|
|
cfg.BackupDir = local.BackupDir
|
|
}
|
|
if local.WorkDir != "" {
|
|
cfg.WorkDir = local.WorkDir
|
|
}
|
|
if local.Compression != 0 {
|
|
cfg.CompressionLevel = local.Compression
|
|
}
|
|
if local.Jobs != 0 {
|
|
cfg.Jobs = local.Jobs
|
|
}
|
|
if local.DumpJobs != 0 {
|
|
cfg.DumpJobs = local.DumpJobs
|
|
}
|
|
if local.CPUWorkload != "" {
|
|
cfg.CPUWorkloadType = local.CPUWorkload
|
|
}
|
|
if local.MaxCores != 0 {
|
|
cfg.MaxCores = local.MaxCores
|
|
}
|
|
if local.ClusterTimeout != 0 {
|
|
cfg.ClusterTimeoutMinutes = local.ClusterTimeout
|
|
}
|
|
if local.ResourceProfile != "" {
|
|
cfg.ResourceProfile = local.ResourceProfile
|
|
}
|
|
if local.LargeDBMode {
|
|
cfg.LargeDBMode = true
|
|
}
|
|
if local.RetentionDays != 0 {
|
|
cfg.RetentionDays = local.RetentionDays
|
|
}
|
|
if local.MinBackups != 0 {
|
|
cfg.MinBackups = local.MinBackups
|
|
}
|
|
if local.MaxRetries != 0 {
|
|
cfg.MaxRetries = local.MaxRetries
|
|
}
|
|
}
|
|
|
|
// ConfigFromConfig creates a LocalConfig from a Config
|
|
func ConfigFromConfig(cfg *Config) *LocalConfig {
|
|
return &LocalConfig{
|
|
DBType: cfg.DatabaseType,
|
|
Host: cfg.Host,
|
|
Port: cfg.Port,
|
|
User: cfg.User,
|
|
Database: cfg.Database,
|
|
SSLMode: cfg.SSLMode,
|
|
BackupDir: cfg.BackupDir,
|
|
WorkDir: cfg.WorkDir,
|
|
Compression: cfg.CompressionLevel,
|
|
Jobs: cfg.Jobs,
|
|
DumpJobs: cfg.DumpJobs,
|
|
CPUWorkload: cfg.CPUWorkloadType,
|
|
MaxCores: cfg.MaxCores,
|
|
ClusterTimeout: cfg.ClusterTimeoutMinutes,
|
|
ResourceProfile: cfg.ResourceProfile,
|
|
LargeDBMode: cfg.LargeDBMode,
|
|
RetentionDays: cfg.RetentionDays,
|
|
MinBackups: cfg.MinBackups,
|
|
MaxRetries: cfg.MaxRetries,
|
|
}
|
|
}
|