264 lines
6.6 KiB
Go
264 lines
6.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config represents the configuration structure
|
|
type Config struct {
|
|
Name string `yaml:"name"`
|
|
Items []Item `yaml:"items"`
|
|
Options Options `yaml:"options"`
|
|
}
|
|
|
|
// Item represents a backup item
|
|
type Item struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"` // local, mysql, postgresql
|
|
Path string `yaml:"path,omitempty"`
|
|
Exclude []string `yaml:"exclude,omitempty"` // Patterns to exclude (for local type)
|
|
Host string `yaml:"host,omitempty"`
|
|
Port int `yaml:"port,omitempty"`
|
|
User string `yaml:"user,omitempty"`
|
|
Password string `yaml:"password,omitempty"`
|
|
Database string `yaml:"database,omitempty"`
|
|
}
|
|
|
|
// Options represents backup options
|
|
type Options struct {
|
|
Storage string `yaml:"storage"`
|
|
S3 S3Config `yaml:"s3"`
|
|
MaxBackups int `yaml:"max_backups"`
|
|
BackupInterval string `yaml:"backup_interval"`
|
|
}
|
|
|
|
// S3Config represents S3 storage configuration
|
|
type S3Config struct {
|
|
Endpoint string `yaml:"endpoint"`
|
|
AccessKey string `yaml:"access_key"`
|
|
SecretKey string `yaml:"secret_key"`
|
|
Bucket string `yaml:"bucket"`
|
|
Region string `yaml:"region"`
|
|
Path string `yaml:"path"`
|
|
Compression string `yaml:"compression"`
|
|
Encryption string `yaml:"encryption"`
|
|
EncryptionKey string `yaml:"encryption_key"`
|
|
EncryptionAlgorithm string `yaml:"encryption_algorithm"`
|
|
}
|
|
|
|
// ParseInterval parses the backup interval string
|
|
func (o *Options) ParseInterval() (time.Duration, error) {
|
|
switch o.BackupInterval {
|
|
case "1h":
|
|
return time.Hour, nil
|
|
case "6h":
|
|
return 6 * time.Hour, nil
|
|
case "12h":
|
|
return 12 * time.Hour, nil
|
|
case "1d":
|
|
return 24 * time.Hour, nil
|
|
case "7d":
|
|
return 7 * 24 * time.Hour, nil
|
|
default:
|
|
return time.ParseDuration(o.BackupInterval)
|
|
}
|
|
}
|
|
|
|
// LoadConfig loads configuration from a YAML file
|
|
func LoadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
var config Config
|
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
|
}
|
|
|
|
if err := config.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid config: %w", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// Validate validates the configuration
|
|
func (c *Config) Validate() error {
|
|
if c.Name == "" {
|
|
return fmt.Errorf("name is required")
|
|
}
|
|
|
|
if len(c.Items) == 0 {
|
|
return fmt.Errorf("at least one backup item is required")
|
|
}
|
|
|
|
for i, item := range c.Items {
|
|
if item.Name == "" {
|
|
return fmt.Errorf("item[%d]: name is required", i)
|
|
}
|
|
|
|
switch item.Type {
|
|
case "local":
|
|
if item.Path == "" {
|
|
return fmt.Errorf("item[%d]: path is required for local type", i)
|
|
}
|
|
case "mysql", "postgresql", "mariadb":
|
|
if item.Host == "" {
|
|
return fmt.Errorf("item[%d]: host is required for %s type", i, item.Type)
|
|
}
|
|
if item.Database == "" {
|
|
return fmt.Errorf("item[%d]: database is required for %s type", i, item.Type)
|
|
}
|
|
default:
|
|
return fmt.Errorf("item[%d]: unsupported type %s", i, item.Type)
|
|
}
|
|
}
|
|
|
|
if c.Options.Storage != "s3" {
|
|
return fmt.Errorf("only s3 storage is supported")
|
|
}
|
|
|
|
if c.Options.S3.Bucket == "" {
|
|
return fmt.Errorf("s3 bucket is required")
|
|
}
|
|
|
|
if c.Options.MaxBackups < 1 {
|
|
return fmt.Errorf("max_backups must be at least 1")
|
|
}
|
|
|
|
if _, err := c.Options.ParseInterval(); err != nil {
|
|
return fmt.Errorf("invalid backup_interval: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestItems tests if all backup items are accessible
|
|
func (c *Config) TestItems() error {
|
|
for i, item := range c.Items {
|
|
switch item.Type {
|
|
case "local":
|
|
if err := testLocalItem(item); err != nil {
|
|
return fmt.Errorf("item[%d] %s: %w", i, item.Name, err)
|
|
}
|
|
case "mysql":
|
|
if err := testMySQLItem(item); err != nil {
|
|
return fmt.Errorf("item[%d] %s: %w", i, item.Name, err)
|
|
}
|
|
case "postgresql":
|
|
if err := testPostgreSQLItem(item); err != nil {
|
|
return fmt.Errorf("item[%d] %s: %w", i, item.Name, err)
|
|
}
|
|
case "mariadb":
|
|
if err := testMariaDBItem(item); err != nil {
|
|
return fmt.Errorf("item[%d] %s: %w", i, item.Name, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// testLocalItem tests if a local path exists and is accessible
|
|
func testLocalItem(item Item) error {
|
|
info, err := os.Stat(item.Path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("path does not exist: %s", item.Path)
|
|
}
|
|
return fmt.Errorf("cannot access path: %w", err)
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("path is not a directory: %s", item.Path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// testMySQLItem tests if MySQL database is accessible
|
|
func testMySQLItem(item Item) error {
|
|
// Build mysql command to test connection
|
|
args := []string{
|
|
fmt.Sprintf("--host=%s", item.Host),
|
|
fmt.Sprintf("--user=%s", item.User),
|
|
}
|
|
|
|
if item.Port > 0 {
|
|
args = append(args, fmt.Sprintf("--port=%d", item.Port))
|
|
}
|
|
|
|
if item.Password != "" {
|
|
args = append(args, fmt.Sprintf("--password=%s", item.Password))
|
|
}
|
|
|
|
args = append(args, "--execute=SELECT 1", item.Database)
|
|
|
|
cmd := exec.Command("mysql", args...)
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("cannot connect to MySQL database: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// testPostgreSQLItem tests if PostgreSQL database is accessible
|
|
func testPostgreSQLItem(item Item) error {
|
|
// Build psql command to test connection
|
|
args := []string{
|
|
fmt.Sprintf("--host=%s", item.Host),
|
|
fmt.Sprintf("--username=%s", item.User),
|
|
fmt.Sprintf("--dbname=%s", item.Database),
|
|
"--command=SELECT 1",
|
|
"--no-password",
|
|
}
|
|
|
|
if item.Port > 0 {
|
|
args = append(args, fmt.Sprintf("--port=%d", item.Port))
|
|
}
|
|
|
|
cmd := exec.Command("psql", args...)
|
|
|
|
// Set password as environment variable
|
|
if item.Password != "" {
|
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", item.Password))
|
|
}
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("cannot connect to PostgreSQL database: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// testMariaDBItem tests if MariaDB database is accessible
|
|
func testMariaDBItem(item Item) error {
|
|
// Build mariadb command to test connection
|
|
args := []string{
|
|
fmt.Sprintf("--host=%s", item.Host),
|
|
fmt.Sprintf("--user=%s", item.User),
|
|
}
|
|
|
|
if item.Port > 0 {
|
|
args = append(args, fmt.Sprintf("--port=%d", item.Port))
|
|
}
|
|
|
|
if item.Password != "" {
|
|
args = append(args, fmt.Sprintf("--password=%s", item.Password))
|
|
}
|
|
|
|
args = append(args, "--execute=SELECT 1", item.Database)
|
|
|
|
cmd := exec.Command("mariadb", args...)
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("cannot connect to MariaDB database: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|