Add backup models and refactor backup strategy implementation
This commit is contained in:
parent
d840597cb8
commit
b3578ab982
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM docker.io/library/golang:1.24-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache git
|
||||
@ -39,9 +39,8 @@ COPY --from=builder /app/backea-server .
|
||||
COPY --from=builder /app/backup-performer .
|
||||
COPY --from=builder /app/list-backups .
|
||||
|
||||
COPY --from=builder /app/static ./static
|
||||
COPY --from=builder /app/templates ./templates
|
||||
|
||||
RUN chmod +x /app/backea-server /app/backup-performer /app/list-backups
|
||||
|
||||
CMD ["/app/backea-server"]
|
||||
CMD ["/app/backea-server"]
|
||||
|
@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/config"
|
||||
"backea/internal/backup/core"
|
||||
"backea/internal/backup/strategy"
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
@ -19,19 +21,28 @@ func main() {
|
||||
log.Printf("Warning: Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse the service flag to extract group and index if provided
|
||||
// Parse service name and index
|
||||
var serviceName, serviceIndex string
|
||||
if *serviceFlag != "" {
|
||||
parts := strings.SplitN(*serviceFlag, "-", 2)
|
||||
parts := strings.SplitN(*serviceFlag, ".", 2)
|
||||
serviceName = parts[0]
|
||||
if len(parts) > 1 {
|
||||
serviceIndex = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if err := backup.PerformBackups(ctx, *configPath, serviceName, serviceIndex); err != nil {
|
||||
cfg, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
factory, _ := strategy.NewFactory(*configPath)
|
||||
|
||||
executor := core.NewExecutor(cfg, *factory)
|
||||
|
||||
// Perform backups
|
||||
ctx := context.Background()
|
||||
if err := executor.PerformBackups(ctx, serviceName, serviceIndex); err != nil {
|
||||
log.Fatalf("Backup failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/strategy"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -20,12 +20,14 @@ func main() {
|
||||
log.Printf("Warning: Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
// Create backup factory
|
||||
factory, err := backup.NewBackupFactory(configPath)
|
||||
factoryBuilder := &strategy.DefaultFactoryBuilder{}
|
||||
factory, err := factoryBuilder.BuildFactory(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create backup factory: %v", err)
|
||||
}
|
||||
|
||||
config := factory.Config
|
||||
|
||||
// Process each service group
|
||||
for groupName, serviceGroup := range factory.Config.Services {
|
||||
fmt.Printf("Service Group: %s (Directory: %s)\n", groupName, serviceGroup.Source.Path)
|
||||
|
16
compose.yaml
16
compose.yaml
@ -7,19 +7,17 @@ services:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# Configuration files
|
||||
- ./.env:/app/.env
|
||||
- ./config.yml:/app/config.yml
|
||||
- ./kopia.env:/app/kopia.env
|
||||
|
||||
- ./.env:/app/.env:Z
|
||||
- ./config.yml:/app/config.yml:Z
|
||||
- ./kopia.env:/app/kopia.env:Z
|
||||
# Source directories to backup - map to identical paths inside container
|
||||
- /home/gevo/Documents/backea2:/home/gevo/Documents/backea2
|
||||
- /home/gevo/Images:/home/gevo/Images
|
||||
- /home/sirir/Images:/home/sirir/Images
|
||||
|
||||
# Dedicated repository storage
|
||||
- /home/gevo/.kopia:/root/.kopia
|
||||
- ./kopiadata:/root/.kopia
|
||||
|
||||
# SSH keys if needed
|
||||
- /home/gevo/.ssh:/root/.ssh:ro
|
||||
- /home/sirir/.ssh:/root/.ssh:ro
|
||||
- ./config/kopia:/tmp/kopia_configs
|
||||
environment:
|
||||
- HOME=/root
|
||||
@ -31,4 +29,4 @@ services:
|
||||
- apparmor:unconfined
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
- ./.env
|
||||
|
21
config.yml
21
config.yml
@ -11,7 +11,7 @@ services:
|
||||
backealocal:
|
||||
source:
|
||||
host: "local"
|
||||
path: "/home/gevo/Documents/backea2"
|
||||
path: "/home/sirir/Images"
|
||||
hooks:
|
||||
before_hook: "ls"
|
||||
after_hook: ""
|
||||
@ -21,18 +21,18 @@ services:
|
||||
type: "kopia"
|
||||
provider: "local"
|
||||
destination:
|
||||
path: "/home/gevo/Documents/backea2"
|
||||
path: "/home/sirir/backup/image1"
|
||||
2:
|
||||
backup_strategy:
|
||||
type: "kopia"
|
||||
provider: "local"
|
||||
destination:
|
||||
path: "/home/gevo/Documents/backea2"
|
||||
path: "/home/sirir/backup/images2"
|
||||
|
||||
imageslocal:
|
||||
source:
|
||||
host: "local"
|
||||
path: "/home/gevo/Images/"
|
||||
path: "/home/sirir/Images/"
|
||||
hooks:
|
||||
before_hook: "ls"
|
||||
after_hook: ""
|
||||
@ -43,16 +43,3 @@ services:
|
||||
provider: "local"
|
||||
destination:
|
||||
host: "local"
|
||||
|
||||
b2backupstotozzeeeaaaeeee:
|
||||
source:
|
||||
host: "local"
|
||||
path: "/home/gevo/Images/"
|
||||
hooks:
|
||||
before_hook: "ls"
|
||||
after_hook: ""
|
||||
backup_configs:
|
||||
1:
|
||||
backup_strategy:
|
||||
type: "kopia"
|
||||
provider: "b2_backblaze"
|
||||
|
@ -1,131 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"backea/internal/mail"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PerformBackups executes backups for multiple services based on configuration
|
||||
func PerformBackups(ctx context.Context, configPath string, serviceName string, serviceIndex string) error {
|
||||
// Create backup factory
|
||||
factory, err := NewBackupFactory(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize mailer
|
||||
mailer := mail.NewMailer()
|
||||
|
||||
// Process services
|
||||
if serviceName != "" {
|
||||
// Process single service group or specific service
|
||||
if serviceIndex != "" {
|
||||
// Process specific service within a group
|
||||
return processSpecificService(ctx, factory, serviceName, serviceIndex, mailer)
|
||||
} else {
|
||||
// Process all services in the specified group
|
||||
return processServiceGroup(ctx, factory, serviceName, mailer)
|
||||
}
|
||||
} else {
|
||||
// Process all service groups in parallel
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, len(factory.Config.Services))
|
||||
for groupName := range factory.Config.Services {
|
||||
wg.Add(1)
|
||||
go func(group string) {
|
||||
defer wg.Done()
|
||||
if err := processServiceGroup(ctx, factory, group, mailer); err != nil {
|
||||
log.Printf("Failed to backup service group %s: %v", group, err)
|
||||
errs <- fmt.Errorf("backup failed for group %s: %w", group, err)
|
||||
}
|
||||
}(groupName)
|
||||
}
|
||||
// Wait for all backups to complete
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// Check if any errors occurred
|
||||
var lastErr error
|
||||
for err := range errs {
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
|
||||
// processServiceGroup handles the backup for all services in a group
|
||||
func processServiceGroup(ctx context.Context, factory *BackupFactory, groupName string, mailer *mail.Mailer) error {
|
||||
// Get service group configuration
|
||||
serviceGroup, exists := factory.Config.Services[groupName]
|
||||
if !exists {
|
||||
log.Printf("Service group not found: %s", groupName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute the before hook once for the entire group
|
||||
if serviceGroup.Hooks.BeforeHook != "" {
|
||||
log.Printf("Executing before hook for group %s: %s", groupName, serviceGroup.Hooks.BeforeHook)
|
||||
if err := RunCommand(serviceGroup.Source.Path, serviceGroup.Hooks.BeforeHook); err != nil {
|
||||
log.Printf("Failed to execute before hook for group %s: %v", groupName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Process all services in the group in parallel
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, len(serviceGroup.BackupConfigs))
|
||||
for configIndex := range serviceGroup.BackupConfigs {
|
||||
wg.Add(1)
|
||||
go func(group, index string) {
|
||||
defer wg.Done()
|
||||
if err := processSpecificService(ctx, factory, group, index, mailer); err != nil {
|
||||
log.Printf("Failed to backup service %s.%s: %v", group, index, err)
|
||||
errs <- fmt.Errorf("backup failed for %s.%s: %w", group, index, err)
|
||||
}
|
||||
}(groupName, configIndex)
|
||||
}
|
||||
// Wait for all backups to complete
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// Execute the after hook once for the entire group
|
||||
if serviceGroup.Hooks.AfterHook != "" {
|
||||
log.Printf("Executing after hook for group %s: %s", groupName, serviceGroup.Hooks.AfterHook)
|
||||
if err := RunCommand(serviceGroup.Source.Path, serviceGroup.Hooks.AfterHook); err != nil {
|
||||
log.Printf("Failed to execute after hook for group %s: %v", groupName, err)
|
||||
// We don't return here because we want to process the errors from the backups
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any errors occurred
|
||||
var lastErr error
|
||||
for err := range errs {
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// processSpecificService handles the backup for a specific service in a group
|
||||
func processSpecificService(ctx context.Context, factory *BackupFactory, groupName string, configIndex string, mailer *mail.Mailer) error {
|
||||
// Get service configuration
|
||||
serviceGroup := factory.Config.Services[groupName]
|
||||
|
||||
// Create the appropriate backup strategy using the factory
|
||||
strategy, err := factory.CreateBackupStrategyForService(groupName, configIndex)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create backup strategy for service %s.%s: %v", groupName, configIndex, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and run service
|
||||
service := NewService(
|
||||
fmt.Sprintf("%s.%s", groupName, configIndex),
|
||||
serviceGroup.Source.Path,
|
||||
strategy,
|
||||
mailer,
|
||||
)
|
||||
return service.Backup(ctx)
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackupFactory creates backup strategies and managers
|
||||
type BackupFactory struct {
|
||||
Config *Configuration
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// NewBackupFactory creates a new backup factory
|
||||
func NewBackupFactory(configPath string) (*BackupFactory, error) {
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load config: %w", err)
|
||||
}
|
||||
return &BackupFactory{
|
||||
Config: config,
|
||||
ConfigPath: configPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateKopiaStrategy creates a new Kopia backup strategy with specific retention settings and provider
|
||||
func (f *BackupFactory) CreateKopiaStrategy(retention Retention, provider KopiaProvider) *KopiaStrategy {
|
||||
// Create temp directory for Kopia configs if it doesn't exist
|
||||
tmpConfigDir := filepath.Join(os.TempDir(), "kopia_configs")
|
||||
if err := os.MkdirAll(tmpConfigDir, 0755); err != nil {
|
||||
log.Printf("Warning: failed to create temp config directory: %v", err)
|
||||
// Fall back to default config path
|
||||
tmpConfigDir = os.TempDir()
|
||||
}
|
||||
|
||||
// Generate a unique config file path for this instance
|
||||
configPath := filepath.Join(tmpConfigDir, fmt.Sprintf("kopia_%s_%d.config",
|
||||
provider.GetProviderType(), time.Now().UnixNano()))
|
||||
|
||||
// Need to update this line to pass configPath
|
||||
return NewKopiaStrategy(retention, provider, configPath)
|
||||
}
|
||||
|
||||
// CreateKopiaProvider creates the appropriate Kopia provider based on config
|
||||
func (f *BackupFactory) CreateKopiaProvider(strategyConfig StrategyConfig) (KopiaProvider, error) {
|
||||
switch strategyConfig.Provider {
|
||||
case "b2_backblaze":
|
||||
return NewKopiaB2Provider(), nil
|
||||
case "local":
|
||||
// Extract options for local path if specified
|
||||
basePath := ""
|
||||
if strategyConfig.Options != "" {
|
||||
basePath = strategyConfig.Options
|
||||
} else {
|
||||
// Default to ~/.backea/repos if not specified
|
||||
basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos")
|
||||
}
|
||||
return NewKopiaLocalProvider(basePath), nil
|
||||
case "sftp":
|
||||
// Parse options for SFTP - expected format: "user@host:/path/to/backups"
|
||||
host := strategyConfig.Destination.Host
|
||||
path := strategyConfig.Destination.Path
|
||||
sshKey := strategyConfig.Destination.SSHKey
|
||||
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("SFTP provider requires host in destination config")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("SFTP provider requires path in destination config")
|
||||
}
|
||||
|
||||
// Default SSH key if not specified
|
||||
if sshKey == "" {
|
||||
sshKey = filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa")
|
||||
}
|
||||
|
||||
// Extract username from host if in format user@host
|
||||
username := "root" // default
|
||||
if hostParts := strings.Split(host, "@"); len(hostParts) > 1 {
|
||||
username = hostParts[0]
|
||||
host = hostParts[1]
|
||||
}
|
||||
|
||||
return NewKopiaSFTPProvider(host, path, username, sshKey), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Kopia provider: %s", strategyConfig.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBackupStrategyForService creates a backup strategy for the specified service within a group
|
||||
func (f *BackupFactory) CreateBackupStrategyForService(groupName string, serviceIndex string) (Strategy, error) {
|
||||
// Find service group
|
||||
serviceGroup, exists := f.Config.Services[groupName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
// Find specific backup config within the group
|
||||
backupConfig, exists := serviceGroup.BackupConfigs[serviceIndex]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("backup config not found: %s.%s", groupName, serviceIndex)
|
||||
}
|
||||
|
||||
// Create appropriate strategy based on type
|
||||
strategyConfig := backupConfig.BackupStrategy
|
||||
|
||||
// Extract retention settings once
|
||||
retention := Retention{
|
||||
KeepLatest: strategyConfig.Retention.KeepLatest,
|
||||
KeepHourly: strategyConfig.Retention.KeepHourly,
|
||||
KeepDaily: strategyConfig.Retention.KeepDaily,
|
||||
KeepWeekly: strategyConfig.Retention.KeepWeekly,
|
||||
KeepMonthly: strategyConfig.Retention.KeepMonthly,
|
||||
KeepYearly: strategyConfig.Retention.KeepYearly,
|
||||
}
|
||||
|
||||
switch strategyConfig.Type {
|
||||
case "kopia":
|
||||
// Create appropriate provider based on configuration
|
||||
provider, err := f.CreateKopiaProvider(strategyConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create kopia provider: %w", err)
|
||||
}
|
||||
return f.CreateKopiaStrategy(retention, provider), nil
|
||||
case "rsync":
|
||||
// Uncomment when rsync implementation is ready
|
||||
// return NewRsyncStrategy(
|
||||
// strategyConfig.Options,
|
||||
// Destination{
|
||||
// Host: strategyConfig.Destination.Host,
|
||||
// Path: strategyConfig.Destination.Path,
|
||||
// SSHKey: strategyConfig.Destination.SSHKey,
|
||||
// },
|
||||
// ), nil
|
||||
fallthrough
|
||||
default:
|
||||
// Default to local kopia if type is unknown or rsync (temporarily)
|
||||
provider, _ := f.CreateKopiaProvider(StrategyConfig{Provider: "local"})
|
||||
return f.CreateKopiaStrategy(retention, provider), nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteGroupHooks runs the hooks for a service group
|
||||
func (f *BackupFactory) ExecuteGroupHooks(groupName string, isBeforeHook bool) error {
|
||||
serviceGroup, exists := f.Config.Services[groupName]
|
||||
if !exists {
|
||||
return fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
var hook string
|
||||
if isBeforeHook {
|
||||
hook = serviceGroup.Hooks.BeforeHook
|
||||
} else {
|
||||
hook = serviceGroup.Hooks.AfterHook
|
||||
}
|
||||
|
||||
if hook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute the hook
|
||||
cmd := exec.Command("sh", "-c", hook)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("hook execution failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
log.Printf("Hook executed successfully: %s, output: %s", hook, string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadConfig refreshes the configuration from the config file
|
||||
func (f *BackupFactory) ReloadConfig() error {
|
||||
config, err := LoadConfig(f.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reload config: %w", err)
|
||||
}
|
||||
f.Config = config
|
||||
return nil
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// RsyncStrategy implements the backup strategy using rsync
|
||||
type RsyncStrategy struct {
|
||||
Options string
|
||||
Destination Destination
|
||||
}
|
||||
|
||||
// NewRsyncStrategy creates a new rsync strategy
|
||||
func NewRsyncStrategy(options string, destination Destination) *RsyncStrategy {
|
||||
return &RsyncStrategy{
|
||||
Options: options,
|
||||
Destination: destination,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute performs the rsync backup
|
||||
func (s *RsyncStrategy) Execute(ctx context.Context, serviceName, directory string) error {
|
||||
log.Printf("Performing rsync backup for %s", serviceName)
|
||||
|
||||
// Build rsync command
|
||||
dst := s.Destination
|
||||
sshKeyOption := ""
|
||||
|
||||
// Without
|
||||
if dst.SSHKey != "" {
|
||||
sshKeyOption = fmt.Sprintf("-e 'ssh -i %s'", dst.SSHKey)
|
||||
}
|
||||
|
||||
destination := fmt.Sprintf("%s:%s", dst.Host, dst.Path)
|
||||
rsyncCmd := fmt.Sprintf("rsync %s %s %s %s",
|
||||
s.Options,
|
||||
sshKeyOption,
|
||||
directory,
|
||||
destination)
|
||||
|
||||
// Run rsync command
|
||||
log.Printf("Running: %s", rsyncCmd)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", rsyncCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Configuration holds the complete application configuration
|
||||
type Configuration struct {
|
||||
Defaults DefaultsConfig `mapstructure:"defaults"`
|
||||
Services map[string]ServiceGroup `mapstructure:"services"`
|
||||
}
|
||||
|
||||
// DefaultsConfig holds the default settings for all services
|
||||
type DefaultsConfig struct {
|
||||
Retention RetentionConfig `mapstructure:"retention"`
|
||||
}
|
||||
|
||||
// ServiceGroup represents a service group with a common directory and multiple backup configurations
|
||||
type ServiceGroup struct {
|
||||
Directory string `mapstructure:"directory,omitempty"`
|
||||
Source SourceConfig `mapstructure:"source"`
|
||||
Hooks HooksConfig `mapstructure:"hooks"`
|
||||
BackupConfigs map[string]BackupConfig `mapstructure:"backup_configs"`
|
||||
}
|
||||
|
||||
// SourceConfig represents source configuration for backups
|
||||
type SourceConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Path string `mapstructure:"path"`
|
||||
SSHKey string `mapstructure:"ssh_key"`
|
||||
}
|
||||
|
||||
// HooksConfig contains the hooks to run before and after the backup
|
||||
type HooksConfig struct {
|
||||
BeforeHook string `mapstructure:"before_hook"`
|
||||
AfterHook string `mapstructure:"after_hook"`
|
||||
}
|
||||
|
||||
// BackupConfig represents a specific backup configuration
|
||||
type BackupConfig struct {
|
||||
BackupStrategy StrategyConfig `mapstructure:"backup_strategy"`
|
||||
}
|
||||
|
||||
// StrategyConfig represents a backup strategy configuration
|
||||
type StrategyConfig struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Provider string `mapstructure:"provider"`
|
||||
Options string `mapstructure:"options,omitempty"`
|
||||
Retention RetentionConfig `mapstructure:"retention,omitempty"`
|
||||
Destination DestConfig `mapstructure:"destination"`
|
||||
}
|
||||
|
||||
// RetentionConfig represents retention policy configuration
|
||||
type RetentionConfig struct {
|
||||
KeepLatest int `mapstructure:"keep_latest"`
|
||||
KeepHourly int `mapstructure:"keep_hourly"`
|
||||
KeepDaily int `mapstructure:"keep_daily"`
|
||||
KeepWeekly int `mapstructure:"keep_weekly"`
|
||||
KeepMonthly int `mapstructure:"keep_monthly"`
|
||||
KeepYearly int `mapstructure:"keep_yearly"`
|
||||
}
|
||||
|
||||
// DestConfig represents destination configuration for remote backups
|
||||
type DestConfig struct {
|
||||
Host string `mapstructure:"host,omitempty"`
|
||||
Path string `mapstructure:"path"`
|
||||
SSHKey string `mapstructure:"ssh_key,omitempty"`
|
||||
}
|
||||
|
||||
// LoadConfig loads the application configuration from a file
|
||||
func LoadConfig(configPath string) (*Configuration, error) {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Printf("Warning: Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
viper.SetConfigFile(configPath)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Configuration
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply default retention settings where needed
|
||||
for serviceName, service := range config.Services {
|
||||
for configID, backupConfig := range service.BackupConfigs {
|
||||
// If retention is not set, use defaults
|
||||
if isRetentionEmpty(backupConfig.BackupStrategy.Retention) {
|
||||
backupConfig.BackupStrategy.Retention = config.Defaults.Retention
|
||||
service.BackupConfigs[configID] = backupConfig
|
||||
}
|
||||
}
|
||||
config.Services[serviceName] = service
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// isRetentionEmpty checks if a retention config is empty (all zeros)
|
||||
func isRetentionEmpty(retention RetentionConfig) bool {
|
||||
return retention.KeepLatest == 0 &&
|
||||
retention.KeepHourly == 0 &&
|
||||
retention.KeepDaily == 0 &&
|
||||
retention.KeepWeekly == 0 &&
|
||||
retention.KeepMonthly == 0 &&
|
||||
retention.KeepYearly == 0
|
||||
}
|
176
internal/backup/core/executor.go
Normal file
176
internal/backup/core/executor.go
Normal file
@ -0,0 +1,176 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"backea/internal/backup/config"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/mail"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Executor handles the execution of backups for multiple services
|
||||
type Executor struct {
|
||||
Config *config.Configuration
|
||||
Factory strategy.Factory
|
||||
Mailer *mail.Mailer
|
||||
concurrency int
|
||||
}
|
||||
|
||||
// NewExecutor creates a new backup executor
|
||||
func NewExecutor(config *config.Configuration, factory strategy.Factory) *Executor {
|
||||
return &Executor{
|
||||
Config: config,
|
||||
Factory: factory,
|
||||
// Mailer: mailer,
|
||||
concurrency: 5,
|
||||
}
|
||||
}
|
||||
|
||||
// SetConcurrency sets the maximum number of concurrent backups
|
||||
func (e *Executor) SetConcurrency(n int) {
|
||||
if n > 0 {
|
||||
e.concurrency = n
|
||||
}
|
||||
}
|
||||
|
||||
// PerformBackups executes backups for multiple services based on configuration
|
||||
func (e *Executor) PerformBackups(ctx context.Context, serviceName string, serviceIndex string) error {
|
||||
// Process services based on parameters
|
||||
if serviceName != "" {
|
||||
// Process single service group or specific service
|
||||
if serviceIndex != "" {
|
||||
// Process specific service within a group
|
||||
return e.processSpecificService(ctx, serviceName, serviceIndex)
|
||||
} else {
|
||||
// Process all services in the specified group
|
||||
return e.processServiceGroup(ctx, serviceName)
|
||||
}
|
||||
} else {
|
||||
// Process all service groups in parallel
|
||||
return e.processAllServiceGroups(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// processAllServiceGroups processes all service groups in parallel
|
||||
func (e *Executor) processAllServiceGroups(ctx context.Context) error {
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, len(e.Config.Services))
|
||||
|
||||
// Create a semaphore to limit concurrency
|
||||
sem := make(chan struct{}, e.concurrency)
|
||||
|
||||
for groupName := range e.Config.Services {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{} // Acquire semaphore
|
||||
go func(group string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }() // Release semaphore
|
||||
|
||||
if err := e.processServiceGroup(ctx, group); err != nil {
|
||||
log.Printf("Failed to backup service group %s: %v", group, err)
|
||||
errs <- fmt.Errorf("backup failed for group %s: %w", group, err)
|
||||
}
|
||||
}(groupName)
|
||||
}
|
||||
|
||||
// Wait for all backups to complete
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// Check if any errors occurred and return the last one
|
||||
var lastErr error
|
||||
for err := range errs {
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// processServiceGroup handles the backup for all services in a group
|
||||
func (e *Executor) processServiceGroup(ctx context.Context, groupName string) error {
|
||||
// Get service group configuration
|
||||
serviceGroup, exists := e.Config.Services[groupName]
|
||||
if !exists {
|
||||
log.Printf("Service group not found: %s", groupName)
|
||||
return fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
// Create hook runner
|
||||
hooks := NewHookRunner(serviceGroup.Source.Path, serviceGroup.Hooks.BeforeHook, serviceGroup.Hooks.AfterHook)
|
||||
|
||||
// Execute the before hook once for the entire group
|
||||
if err := hooks.RunBeforeHook(); err != nil {
|
||||
log.Printf("Failed to execute before hook for group %s: %v", groupName, err)
|
||||
return fmt.Errorf("before hook failed: %w", err)
|
||||
}
|
||||
|
||||
// Process all services in the group in parallel
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, len(serviceGroup.BackupConfigs))
|
||||
|
||||
// Create a semaphore to limit concurrency within the group
|
||||
sem := make(chan struct{}, e.concurrency)
|
||||
|
||||
for configIndex := range serviceGroup.BackupConfigs {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{} // Acquire semaphore
|
||||
go func(group, index string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }() // Release semaphore
|
||||
|
||||
if err := e.processSpecificService(ctx, group, index); err != nil {
|
||||
log.Printf("Failed to backup service %s.%s: %v", group, index, err)
|
||||
errs <- fmt.Errorf("backup failed for %s.%s: %w", group, index, err)
|
||||
}
|
||||
}(groupName, configIndex)
|
||||
}
|
||||
|
||||
// Wait for all backups to complete
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
// Execute the after hook once for the entire group
|
||||
if err := hooks.RunAfterHook(); err != nil {
|
||||
log.Printf("Failed to execute after hook for group %s: %v", groupName, err)
|
||||
// Don't return here because we want to report backup errors as well
|
||||
}
|
||||
|
||||
// Check if any errors occurred
|
||||
var lastErr error
|
||||
for err := range errs {
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// processSpecificService handles the backup for a specific service in a group
|
||||
func (e *Executor) processSpecificService(ctx context.Context, groupName string, configIndex string) error {
|
||||
// Get service configuration
|
||||
serviceGroup, exists := e.Config.Services[groupName]
|
||||
if !exists {
|
||||
return fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
// Check if the config index exists
|
||||
_, exists = serviceGroup.BackupConfigs[configIndex]
|
||||
if !exists {
|
||||
return fmt.Errorf("service index not found: %s.%s", groupName, configIndex)
|
||||
}
|
||||
|
||||
// Create the appropriate backup strategy using the factory
|
||||
backupStrategy, err := e.Factory.CreateBackupStrategyForService(groupName, configIndex)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create backup strategy for service %s.%s: %v", groupName, configIndex, err)
|
||||
return fmt.Errorf("failed to create backup strategy: %w", err)
|
||||
}
|
||||
|
||||
// Create and run service
|
||||
service := NewService(
|
||||
fmt.Sprintf("%s.%s", groupName, configIndex),
|
||||
serviceGroup.Source.Path,
|
||||
backupStrategy,
|
||||
e.Mailer,
|
||||
)
|
||||
return service.Backup(ctx)
|
||||
}
|
56
internal/backup/core/hooks.go
Normal file
56
internal/backup/core/hooks.go
Normal file
@ -0,0 +1,56 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// HookRunner runs hooks before and after backups
|
||||
type HookRunner struct {
|
||||
Directory string
|
||||
BeforeHook string
|
||||
AfterHook string
|
||||
}
|
||||
|
||||
// NewHookRunner creates a new hook runner
|
||||
func NewHookRunner(directory, beforeHook, afterHook string) *HookRunner {
|
||||
return &HookRunner{
|
||||
Directory: directory,
|
||||
BeforeHook: beforeHook,
|
||||
AfterHook: afterHook,
|
||||
}
|
||||
}
|
||||
|
||||
// RunBeforeHook executes the before hook if defined
|
||||
func (h *HookRunner) RunBeforeHook() error {
|
||||
if h.BeforeHook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Running before hook: %s", h.BeforeHook)
|
||||
return h.runCommand(h.BeforeHook)
|
||||
}
|
||||
|
||||
// RunAfterHook executes the after hook if defined
|
||||
func (h *HookRunner) RunAfterHook() error {
|
||||
if h.AfterHook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Running after hook: %s", h.AfterHook)
|
||||
return h.runCommand(h.AfterHook)
|
||||
}
|
||||
|
||||
// runCommand executes a shell command in the specified directory
|
||||
func (h *HookRunner) runCommand(command string) error {
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
if h.Directory != "" {
|
||||
cmd.Dir = h.Directory
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
log.Printf("Executing command in %s: %s", h.Directory, command)
|
||||
return cmd.Run()
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
package backup
|
||||
package core
|
||||
|
||||
import (
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/mail"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -14,12 +15,12 @@ import (
|
||||
type Service struct {
|
||||
Name string
|
||||
Directory string
|
||||
Strategy Strategy
|
||||
Strategy strategy.Strategy
|
||||
Mailer *mail.Mailer
|
||||
}
|
||||
|
||||
// NewService creates a new backup service
|
||||
func NewService(name string, directory string, strategy Strategy, mailer *mail.Mailer) *Service {
|
||||
func NewService(name string, directory string, strategy strategy.Strategy, mailer *mail.Mailer) *Service {
|
||||
return &Service{
|
||||
Name: name,
|
||||
Directory: directory,
|
||||
@ -58,14 +59,18 @@ func (s *Service) Backup(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunCommand executes a shell command in the specified directory
|
||||
// This is now a package function rather than a method on Service
|
||||
func RunCommand(dir, command string) error {
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
// GetBackupInfos retrieves information about existing backups
|
||||
func (s *Service) GetBackupInfos(ctx context.Context) ([]models.BackupInfo, error) {
|
||||
return s.Strategy.ListBackups(ctx, s.Name)
|
||||
}
|
||||
|
||||
// GetStorageUsage retrieves information about storage usage
|
||||
func (s *Service) GetStorageUsage(ctx context.Context) (*models.StorageUsageInfo, error) {
|
||||
return s.Strategy.GetStorageUsage(ctx, s.Name)
|
||||
}
|
||||
|
||||
// RestoreBackup restores a backup by ID
|
||||
func (s *Service) RestoreBackup(ctx context.Context, backupID string) error {
|
||||
log.Printf("Restoring backup %s for service %s", backupID, s.Name)
|
||||
return s.Strategy.RestoreBackup(ctx, backupID, s.Name)
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
b2_client "backea/internal/client"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KopiaProvider defines the interface for different Kopia storage backends
|
||||
type KopiaProvider interface {
|
||||
// Connect connects to an existing repository or creates a new one
|
||||
Connect(ctx context.Context, serviceName string, password string, configPath string) error
|
||||
// GetRepositoryParams returns the parameters needed for repository operations
|
||||
GetRepositoryParams(serviceName string) ([]string, error)
|
||||
// GetBucketName returns the storage identifier (bucket name or path)
|
||||
GetBucketName(serviceName string) string
|
||||
// GetProviderType returns a string identifying the provider type
|
||||
GetProviderType() string
|
||||
}
|
||||
|
||||
// KopiaB2Provider implements the KopiaProvider interface for Backblaze B2
|
||||
type KopiaB2Provider struct{}
|
||||
|
||||
// NewKopiaB2Provider creates a new B2 provider
|
||||
func NewKopiaB2Provider() *KopiaB2Provider {
|
||||
return &KopiaB2Provider{}
|
||||
}
|
||||
|
||||
// Connect connects to a B2 repository
|
||||
func (p *KopiaB2Provider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
// Load environment variables for B2 credentials
|
||||
keyID := os.Getenv("B2_KEY_ID")
|
||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||
if keyID == "" || applicationKey == "" {
|
||||
return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
||||
}
|
||||
|
||||
bucketName := p.GetBucketName(serviceName)
|
||||
|
||||
// Create B2 client and check if bucket exists, create if not
|
||||
B2Client, _ := b2_client.NewClientFromEnv()
|
||||
if B2Client == nil {
|
||||
return fmt.Errorf("B2 client not initialized")
|
||||
}
|
||||
|
||||
_, err := B2Client.GetBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
log.Printf("Bucket %s not found, creating...", bucketName)
|
||||
_, err = B2Client.CreateBucket(ctx, bucketName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bucket: %w", err)
|
||||
}
|
||||
log.Printf("Created bucket: %s", bucketName)
|
||||
}
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
err = connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new B2 repository for %s", serviceName)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for B2 operations
|
||||
func (p *KopiaB2Provider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
keyID := os.Getenv("B2_KEY_ID")
|
||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||
if keyID == "" || applicationKey == "" {
|
||||
return nil, fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
||||
}
|
||||
|
||||
bucketName := p.GetBucketName(serviceName)
|
||||
|
||||
return []string{
|
||||
"b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Modify the GetBucketName method in your KopiaB2Provider
|
||||
func (p *KopiaB2Provider) GetBucketName(serviceName string) string {
|
||||
// Backblaze dont allow .
|
||||
sanitized := strings.ReplaceAll(serviceName, ".", "-")
|
||||
|
||||
// Add a prefix to make the bucket name unique
|
||||
return fmt.Sprintf("backea-%s", sanitized)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *KopiaB2Provider) GetProviderType() string {
|
||||
return "b2"
|
||||
}
|
||||
|
||||
// KopiaLocalProvider implements the KopiaProvider interface for local storage
|
||||
type KopiaLocalProvider struct {
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// NewKopiaLocalProvider creates a new local provider
|
||||
func NewKopiaLocalProvider(basePath string) *KopiaLocalProvider {
|
||||
// If basePath is empty, use a default location
|
||||
if basePath == "" {
|
||||
basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos")
|
||||
}
|
||||
return &KopiaLocalProvider{
|
||||
BasePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects to a local repository
|
||||
func (p *KopiaLocalProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
repoPath := filepath.Join(p.BasePath, serviceName)
|
||||
log.Printf("Connecting to local repository at %s with config: %s", repoPath, configPath)
|
||||
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(repoPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create repository directory: %w", err)
|
||||
}
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "filesystem",
|
||||
"--path", repoPath,
|
||||
"--password", password,
|
||||
)
|
||||
err := connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new local repository for %s at destination: %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "filesystem",
|
||||
"--path", repoPath,
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for local filesystem operations
|
||||
func (p *KopiaLocalProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
repoPath := filepath.Join(p.BasePath, serviceName)
|
||||
return []string{
|
||||
"filesystem",
|
||||
"--path", repoPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBucketName returns the path for a service
|
||||
func (p *KopiaLocalProvider) GetBucketName(serviceName string) string {
|
||||
return filepath.Join(p.BasePath, serviceName)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *KopiaLocalProvider) GetProviderType() string {
|
||||
return "local"
|
||||
}
|
||||
|
||||
// KopiaSFTPProvider implements the KopiaProvider interface for SFTP remote storage
|
||||
type KopiaSFTPProvider struct {
|
||||
Host string
|
||||
BasePath string
|
||||
Username string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// NewKopiaSFTPProvider creates a new SFTP provider
|
||||
func NewKopiaSFTPProvider(host, basePath, username, keyFile string) *KopiaSFTPProvider {
|
||||
return &KopiaSFTPProvider{
|
||||
Host: host,
|
||||
BasePath: basePath,
|
||||
Username: username,
|
||||
KeyFile: keyFile,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects to an SFTP repository
|
||||
func (p *KopiaSFTPProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--password", password,
|
||||
)
|
||||
err := connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new SFTP repository for %s at %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for SFTP operations
|
||||
func (p *KopiaSFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
||||
return []string{
|
||||
"sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBucketName returns the path for a service
|
||||
func (p *KopiaSFTPProvider) GetBucketName(serviceName string) string {
|
||||
return fmt.Sprintf("%s/%s", p.BasePath, serviceName)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *KopiaSFTPProvider) GetProviderType() string {
|
||||
return "sftp"
|
||||
}
|
15
internal/backup/models/backup.go
Normal file
15
internal/backup/models/backup.go
Normal file
@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackupInfo contains information about a single backup
|
||||
type BackupInfo struct {
|
||||
ID string // Unique identifier for the backup
|
||||
CreationTime time.Time // When the backup was created
|
||||
Size int64 // Size in bytes
|
||||
Source string // Original source path
|
||||
Type string // Type of backup (e.g., "kopia", "restic")
|
||||
RetentionTag string // Retention policy applied (e.g., "latest", "daily", "weekly")
|
||||
}
|
8
internal/backup/models/storage.go
Normal file
8
internal/backup/models/storage.go
Normal file
@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
// StorageUsageInfo contains information about backup storage usage
|
||||
type StorageUsageInfo struct {
|
||||
TotalBytes int64 // Total bytes stored
|
||||
Provider string // Storage provider (e.g., "local", "b2", "s3")
|
||||
ProviderID string // Provider-specific ID (bucket name, path, etc.)
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
client "backea/internal/client"
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RestoreManager handles restoring files from Kopia backups
|
||||
type RestoreManager struct {
|
||||
B2Client *client.Client
|
||||
}
|
||||
|
||||
// NewRestoreManager creates a new restore manager
|
||||
func NewRestoreManager(b2Client *client.Client) *RestoreManager {
|
||||
return &RestoreManager{
|
||||
B2Client: b2Client,
|
||||
}
|
||||
}
|
||||
|
||||
// getStoredPassword retrieves a password from kopia.env file
|
||||
func (r *RestoreManager) getStoredPassword(serviceName string) (string, error) {
|
||||
// Define the expected key in the env file
|
||||
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(serviceName))
|
||||
|
||||
// Try to read from kopia.env
|
||||
kopiaEnvPath := "kopia.env"
|
||||
if _, err := os.Stat(kopiaEnvPath); err != nil {
|
||||
return "", fmt.Errorf("kopia.env file not found: %w", err)
|
||||
}
|
||||
|
||||
// File exists, check if the password is there
|
||||
file, err := os.Open(kopiaEnvPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open kopia.env: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, passwordKey+"=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
// Found the password, remove quotes if present
|
||||
password := parts[1]
|
||||
password = strings.Trim(password, "\"")
|
||||
return password, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("error reading kopia.env: %w", err)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("password for service %s not found in kopia.env", serviceName)
|
||||
}
|
||||
|
||||
// connectToRepository connects to an existing Kopia repository
|
||||
func (r *RestoreManager) connectToRepository(ctx context.Context, serviceName string) error {
|
||||
if r.B2Client == nil {
|
||||
return fmt.Errorf("B2 client not initialized")
|
||||
}
|
||||
|
||||
// Load environment variables for B2 credentials
|
||||
keyID := os.Getenv("B2_KEY_ID")
|
||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||
if keyID == "" || applicationKey == "" {
|
||||
return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
||||
}
|
||||
|
||||
// Get stored password for this service
|
||||
password, err := r.getStoredPassword(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stored password: %w", err)
|
||||
}
|
||||
|
||||
// Generate bucket name from service name
|
||||
bucketName := fmt.Sprintf("backea-%s", strings.ToLower(serviceName))
|
||||
|
||||
// Check if bucket exists
|
||||
_, err = r.B2Client.GetBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bucket %s not found: %w", bucketName, err)
|
||||
}
|
||||
|
||||
// Check if kopia repository is already connected
|
||||
cmd := exec.Command("kopia", "repository", "status")
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
// Already connected
|
||||
log.Printf("Already connected to a repository, disconnecting first")
|
||||
disconnectCmd := exec.Command("kopia", "repository", "disconnect")
|
||||
if err := disconnectCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to disconnect from current repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the repository
|
||||
log.Printf("Connecting to B2 repository for %s", serviceName)
|
||||
connectCmd := exec.Command(
|
||||
"kopia", "repository", "connect", "b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
connectOutput, err := connectCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to repository: %w\nOutput: %s", err, connectOutput)
|
||||
}
|
||||
|
||||
log.Printf("Successfully connected to repository for %s", serviceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSnapshots lists all snapshots in the repository for a given service
|
||||
func (r *RestoreManager) ListSnapshots(ctx context.Context, serviceName string) error {
|
||||
// Connect to the repository
|
||||
if err := r.connectToRepository(ctx, serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// List all snapshots
|
||||
log.Printf("Listing snapshots for %s", serviceName)
|
||||
listCmd := exec.Command("kopia", "snapshot", "list")
|
||||
listOutput, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list snapshots: %w\nOutput: %s", err, listOutput)
|
||||
}
|
||||
|
||||
// Print the output
|
||||
fmt.Println("Available snapshots:")
|
||||
fmt.Println(string(listOutput))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreFile restores a file or directory from a specific snapshot to a target location
|
||||
func (r *RestoreManager) RestoreFile(ctx context.Context, serviceName, snapshotID, sourcePath, targetPath string) error {
|
||||
// Connect to the repository
|
||||
if err := r.connectToRepository(ctx, serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a subdirectory with the snapshot ID name
|
||||
snapshotDirPath := filepath.Join(targetPath, snapshotID)
|
||||
if err := os.MkdirAll(snapshotDirPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create target directory: %w", err)
|
||||
}
|
||||
|
||||
// Construct the kopia restore command
|
||||
log.Printf("Restoring from snapshot %s to %s", snapshotID, snapshotDirPath)
|
||||
|
||||
// Build the command with all required parameters
|
||||
restoreCmd := exec.Command(
|
||||
"kopia", "snapshot", "restore",
|
||||
snapshotID, // Just the snapshot ID
|
||||
snapshotDirPath, // Target location where files will be restored
|
||||
)
|
||||
|
||||
// Execute the command and capture output
|
||||
restoreOutput, err := restoreCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restore files: %w\nOutput: %s", err, restoreOutput)
|
||||
}
|
||||
|
||||
log.Printf("Successfully restored files to %s", snapshotDirPath)
|
||||
log.Printf("Restore output: %s", string(restoreOutput))
|
||||
|
||||
return nil
|
||||
}
|
115
internal/backup/security/password.go
Normal file
115
internal/backup/security/password.go
Normal file
@ -0,0 +1,115 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetOrCreatePassword retrieves a password from kopia.env or creates a new one if it doesn't exist
|
||||
func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
||||
// Define the expected key in the env file
|
||||
// Replace dots with underscores for environment variable name
|
||||
safeServiceName := strings.ReplaceAll(serviceName, ".", "_")
|
||||
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(safeServiceName))
|
||||
|
||||
// Try to read from kopia.env first
|
||||
kopiaEnvPath := "kopia.env"
|
||||
if _, err := os.Stat(kopiaEnvPath); err == nil {
|
||||
// File exists, check if the password is already there
|
||||
file, err := os.Open(kopiaEnvPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open kopia.env: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, passwordKey+"=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
// Found the password, remove quotes if present
|
||||
password := parts[1]
|
||||
password = strings.Trim(password, "\"")
|
||||
return password, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("error reading kopia.env: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Password not found or file doesn't exist, generate a new password
|
||||
password, err := GenerateSecurePassword(length)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate password: %w", err)
|
||||
}
|
||||
|
||||
// Create or append to kopia.env
|
||||
file, err := os.OpenFile(kopiaEnvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open kopia.env for writing: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write the new password entry with quotes
|
||||
passwordEntry := fmt.Sprintf("%s=\"%s\"\n", passwordKey, password)
|
||||
if _, err := file.WriteString(passwordEntry); err != nil {
|
||||
return "", fmt.Errorf("failed to write to kopia.env: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Created new password for service %s and stored in kopia.env", serviceName)
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// GenerateSecurePassword creates a cryptographically secure random password
|
||||
// using only safe characters that work well with command line tools
|
||||
func GenerateSecurePassword(length int) (string, error) {
|
||||
// Use a more robust but safe character set
|
||||
// Avoiding characters that might cause shell interpretation issues
|
||||
// No quotes, backslashes, spaces, or common special chars that need escaping
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
result := make([]byte, length)
|
||||
|
||||
// Create a secure source of randomness
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ensure minimum complexity requirements
|
||||
// At least 2 uppercase, 2 lowercase, 2 digits
|
||||
if length >= 6 {
|
||||
// Force first characters to meet minimum requirements
|
||||
result[0] = 'A' + byte(randomBytes[0]%26) // Uppercase
|
||||
result[1] = 'A' + byte(randomBytes[1]%26) // Uppercase
|
||||
result[2] = 'a' + byte(randomBytes[2]%26) // Lowercase
|
||||
result[3] = 'a' + byte(randomBytes[3]%26) // Lowercase
|
||||
result[4] = '0' + byte(randomBytes[4]%10) // Digit
|
||||
result[5] = '0' + byte(randomBytes[5]%10) // Digit
|
||||
|
||||
// Fill the rest with random chars
|
||||
for i := 6; i < length; i++ {
|
||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||
}
|
||||
|
||||
// Shuffle the result to avoid predictable pattern
|
||||
for i := length - 1; i > 0; i-- {
|
||||
j := int(randomBytes[i]) % (i + 1)
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
} else {
|
||||
// For very short passwords just use random chars
|
||||
for i := 0; i < length; i++ {
|
||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||
}
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Strategy defines the backup/restore strategy interface
|
||||
type Strategy interface {
|
||||
// Execute performs a backup
|
||||
Execute(ctx context.Context, serviceName, directory string) error
|
||||
|
||||
ListBackups(ctx context.Context, serviceName string) ([]BackupInfo, error)
|
||||
|
||||
GetStorageUsage(ctx context.Context, serviceName string) (*StorageUsageInfo, error)
|
||||
|
||||
RestoreBackup(ctx context.Context, backupID string, serviceName string) error
|
||||
|
||||
DownloadBackup(ctx context.Context, backupID, serviceName string) (io.ReadCloser, error)
|
||||
|
||||
GetBackupInfo(context.Context, string, string) (*BackupInfo, error)
|
||||
}
|
||||
|
||||
// BackupInfo contains information about a single backup
|
||||
type BackupInfo struct {
|
||||
ID string // Unique identifier for the backup
|
||||
CreationTime time.Time // When the backup was created
|
||||
Size int64 // Size in bytes
|
||||
Source string // Original source path
|
||||
Type string // Type of backup (e.g., "kopia", "restic")
|
||||
RetentionTag string // Retention policy applied (e.g., "latest", "daily", "weekly")
|
||||
}
|
||||
|
||||
// StorageUsageInfo contains information about backup storage usage
|
||||
type StorageUsageInfo struct {
|
||||
TotalBytes int64 // Total bytes stored
|
||||
Provider string // Storage provider (e.g., "local", "b2", "s3")
|
||||
ProviderID string // Provider-specific ID (bucket name, path, etc.)
|
||||
}
|
||||
|
||||
// Retention represents retention policy for backups
|
||||
type Retention struct {
|
||||
KeepLatest int
|
||||
KeepHourly int
|
||||
KeepDaily int
|
||||
KeepWeekly int
|
||||
KeepMonthly int
|
||||
KeepYearly int
|
||||
}
|
||||
|
||||
// Destination represents rsync destination
|
||||
type Destination struct {
|
||||
Host string
|
||||
Path string
|
||||
SSHKey string
|
||||
}
|
150
internal/backup/strategy/factory.go
Normal file
150
internal/backup/strategy/factory.go
Normal file
@ -0,0 +1,150 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"backea/internal/backup/config"
|
||||
"backea/internal/backup/strategy/kopia"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Factory handles backup strategy creation
|
||||
type Factory struct {
|
||||
Config *config.Configuration
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// NewFactory initializes and returns a Factory
|
||||
func NewFactory(configPath string) (*Factory, error) {
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
return &Factory{Config: cfg, ConfigPath: configPath}, nil
|
||||
}
|
||||
|
||||
// CreateBackupStrategyForService returns a backup strategy based on config
|
||||
func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string) (Strategy, error) {
|
||||
serviceConfig, err := f.getServiceConfig(groupName, serviceIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sourcePath, err := f.getServicePath(groupName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch serviceConfig.BackupStrategy.Type {
|
||||
case "kopia":
|
||||
return f.createKopiaStrategy(serviceConfig, sourcePath)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown strategy type: %s", serviceConfig.BackupStrategy.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// createKopiaStrategy initializes a Kopia strategy
|
||||
func (f *Factory) createKopiaStrategy(serviceConfig *config.BackupConfig, sourcePath string) (Strategy, error) {
|
||||
provider, err := f.createKopiaProvider(serviceConfig.BackupStrategy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create kopia provider: %w", err)
|
||||
}
|
||||
|
||||
retention := kopia.Retention(serviceConfig.BackupStrategy.Retention)
|
||||
configPath := generateTempConfigPath(provider.GetProviderType())
|
||||
|
||||
return kopia.NewStrategy(retention, provider, configPath, sourcePath), nil
|
||||
}
|
||||
|
||||
// createKopiaProvider returns a Kopia storage provider
|
||||
func (f *Factory) createKopiaProvider(strategyConfig config.StrategyConfig) (kopia.Provider, error) {
|
||||
switch strategyConfig.Provider {
|
||||
case "local":
|
||||
return kopia.NewLocalProvider(getDefaultPath(strategyConfig.Destination.Path)), nil
|
||||
case "b2", "backblaze":
|
||||
return kopia.NewB2Provider(), nil
|
||||
// case "sftp":
|
||||
// return createSFTPProvider(strategyConfig.Destination)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown kopia provider type: %s", strategyConfig.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// createSFTPProvider handles SFTP provider creation
|
||||
func createSFTPProvider(dest DestinationConfig) (kopia.Provider, error) {
|
||||
host, username := parseSFTPHost(dest.Host)
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("SFTP host not specified")
|
||||
}
|
||||
return kopia.NewSFTPProvider(host, getDefaultPath(dest.Path), username, getSSHKeyPath(dest.SSHKey)), nil
|
||||
}
|
||||
|
||||
// parseSFTPHost extracts the username and host from "user@host"
|
||||
func parseSFTPHost(host string) (string, string) {
|
||||
if atIndex := strings.Index(host, "@"); atIndex > 0 {
|
||||
return host[atIndex+1:], host[:atIndex]
|
||||
}
|
||||
return host, getEnvUser()
|
||||
}
|
||||
|
||||
// getDefaultPath returns the given path or a default
|
||||
func getDefaultPath(path string) string {
|
||||
if path != "" {
|
||||
return path
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".backea", "repos")
|
||||
}
|
||||
|
||||
// getEnvUser returns the system username
|
||||
func getEnvUser() string {
|
||||
if user := os.Getenv("USER"); user != "" {
|
||||
return user
|
||||
}
|
||||
return "root"
|
||||
}
|
||||
|
||||
// getSSHKeyPath returns the SSH key path
|
||||
func getSSHKeyPath(key string) string {
|
||||
if key != "" {
|
||||
return key
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".ssh", "id_rsa")
|
||||
}
|
||||
|
||||
// getServicePath returns a service's path
|
||||
func (f *Factory) getServicePath(groupName string) (string, error) {
|
||||
if serviceGroup, exists := f.Config.Services[groupName]; exists {
|
||||
return serviceGroup.Source.Path, nil
|
||||
}
|
||||
return "", fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
// getServiceConfig returns a service's config
|
||||
func (f *Factory) getServiceConfig(groupName, serviceIndex string) (*config.BackupConfig, error) {
|
||||
if serviceGroup, exists := f.Config.Services[groupName]; exists {
|
||||
if backupConfig, exists := serviceGroup.BackupConfigs[serviceIndex]; exists {
|
||||
return &backupConfig, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("backup config not found: %s.%s", groupName, serviceIndex)
|
||||
}
|
||||
|
||||
// ReloadConfig reloads configuration
|
||||
func (f *Factory) ReloadConfig() error {
|
||||
cfg, err := config.LoadConfig(f.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reload config: %w", err)
|
||||
}
|
||||
f.Config = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTempConfigPath creates a temp Kopia config path
|
||||
func generateTempConfigPath(providerType string) string {
|
||||
tmpConfigDir := filepath.Join(os.TempDir(), "kopia_configs")
|
||||
_ = os.MkdirAll(tmpConfigDir, 0755) // Ensure directory exists
|
||||
return filepath.Join(tmpConfigDir, fmt.Sprintf("kopia_%s_%d.config", providerType, time.Now().UnixNano()))
|
||||
}
|
113
internal/backup/strategy/kopia/b2.go
Normal file
113
internal/backup/strategy/kopia/b2.go
Normal file
@ -0,0 +1,113 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
b2_client "backea/internal/client"
|
||||
)
|
||||
|
||||
// B2Provider implements the Provider interface for Backblaze B2
|
||||
type B2Provider struct{}
|
||||
|
||||
// NewB2Provider creates a new B2 provider
|
||||
func NewB2Provider() *B2Provider {
|
||||
return &B2Provider{}
|
||||
}
|
||||
|
||||
// Connect connects to a B2 repository
|
||||
func (p *B2Provider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
// Load environment variables for B2 credentials
|
||||
keyID := os.Getenv("B2_KEY_ID")
|
||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||
if keyID == "" || applicationKey == "" {
|
||||
return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
||||
}
|
||||
|
||||
bucketName := p.GetBucketName(serviceName)
|
||||
|
||||
// Create B2 client and check if bucket exists, create if not
|
||||
B2Client, _ := b2_client.NewClientFromEnv()
|
||||
if B2Client == nil {
|
||||
return fmt.Errorf("B2 client not initialized")
|
||||
}
|
||||
|
||||
_, err := B2Client.GetBucket(ctx, bucketName)
|
||||
if err != nil {
|
||||
log.Printf("Bucket %s not found, creating...", bucketName)
|
||||
_, err = B2Client.CreateBucket(ctx, bucketName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create bucket: %w", err)
|
||||
}
|
||||
log.Printf("Created bucket: %s", bucketName)
|
||||
}
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
err = connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new B2 repository for %s", serviceName)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for B2 operations
|
||||
func (p *B2Provider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
keyID := os.Getenv("B2_KEY_ID")
|
||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||
if keyID == "" || applicationKey == "" {
|
||||
return nil, fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
||||
}
|
||||
|
||||
bucketName := p.GetBucketName(serviceName)
|
||||
|
||||
return []string{
|
||||
"b2",
|
||||
"--bucket", bucketName,
|
||||
"--key-id", keyID,
|
||||
"--key", applicationKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBucketName returns a bucket name for the service
|
||||
func (p *B2Provider) GetBucketName(serviceName string) string {
|
||||
// Backblaze doesn't allow dots in bucket names
|
||||
sanitized := strings.ReplaceAll(serviceName, ".", "-")
|
||||
|
||||
// Add a prefix to make the bucket name unique
|
||||
return fmt.Sprintf("backea-%s", sanitized)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *B2Provider) GetProviderType() string {
|
||||
return "b2"
|
||||
}
|
85
internal/backup/strategy/kopia/local.go
Normal file
85
internal/backup/strategy/kopia/local.go
Normal file
@ -0,0 +1,85 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LocalProvider implements the Provider interface for local storage
|
||||
type LocalProvider struct {
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// NewLocalProvider creates a new local provider
|
||||
func NewLocalProvider(basePath string) *LocalProvider {
|
||||
// If basePath is empty, use a default location
|
||||
if basePath == "" {
|
||||
basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos")
|
||||
}
|
||||
return &LocalProvider{
|
||||
BasePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects to a local repository
|
||||
func (p *LocalProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
repoPath := filepath.Join(p.BasePath, serviceName)
|
||||
log.Printf("Connecting to local repository at %s with config: %s", repoPath, configPath)
|
||||
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(repoPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create repository directory: %w", err)
|
||||
}
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "filesystem",
|
||||
"--path", repoPath,
|
||||
"--password", password,
|
||||
)
|
||||
err := connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new local repository for %s at destination: %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "filesystem",
|
||||
"--path", repoPath,
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for local filesystem operations
|
||||
func (p *LocalProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
repoPath := filepath.Join(p.BasePath, serviceName)
|
||||
return []string{
|
||||
"filesystem",
|
||||
"--path", repoPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBucketName returns the path for a service
|
||||
func (p *LocalProvider) GetBucketName(serviceName string) string {
|
||||
return filepath.Join(p.BasePath, serviceName)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *LocalProvider) GetProviderType() string {
|
||||
return "local"
|
||||
}
|
20
internal/backup/strategy/kopia/providers.go
Normal file
20
internal/backup/strategy/kopia/providers.go
Normal file
@ -0,0 +1,20 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Provider defines the interface for different Kopia storage backends
|
||||
type Provider interface {
|
||||
// Connect connects to an existing repository or creates a new one
|
||||
Connect(ctx context.Context, serviceName string, password string, configPath string) error
|
||||
|
||||
// GetRepositoryParams returns the parameters needed for repository operations
|
||||
GetRepositoryParams(serviceName string) ([]string, error)
|
||||
|
||||
// GetBucketName returns the storage identifier (bucket name or path)
|
||||
GetBucketName(serviceName string) string
|
||||
|
||||
// GetProviderType returns a string identifying the provider type
|
||||
GetProviderType() string
|
||||
}
|
87
internal/backup/strategy/kopia/sftp.go
Normal file
87
internal/backup/strategy/kopia/sftp.go
Normal file
@ -0,0 +1,87 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// SFTPProvider implements the Provider interface for SFTP remote storage
|
||||
type SFTPProvider struct {
|
||||
Host string
|
||||
BasePath string
|
||||
Username string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// NewSFTPProvider creates a new SFTP provider
|
||||
func NewSFTPProvider(host, basePath, username, keyFile string) *SFTPProvider {
|
||||
return &SFTPProvider{
|
||||
Host: host,
|
||||
BasePath: basePath,
|
||||
Username: username,
|
||||
KeyFile: keyFile,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects to an SFTP repository
|
||||
func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
||||
|
||||
// Try to connect to existing repository with config file
|
||||
connectCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "connect", "sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--password", password,
|
||||
)
|
||||
err := connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new SFTP repository for %s at %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
"--config-file", configPath,
|
||||
"repository", "create", "sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--password", password,
|
||||
)
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryParams returns parameters for SFTP operations
|
||||
func (p *SFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
||||
return []string{
|
||||
"sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBucketName returns the path for a service
|
||||
func (p *SFTPProvider) GetBucketName(serviceName string) string {
|
||||
return fmt.Sprintf("%s/%s", p.BasePath, serviceName)
|
||||
}
|
||||
|
||||
// GetProviderType returns the provider type identifier
|
||||
func (p *SFTPProvider) GetProviderType() string {
|
||||
return "sftp"
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
package backup
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -15,30 +13,45 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/security"
|
||||
)
|
||||
|
||||
// KopiaStrategy implements the backup strategy using Kopia
|
||||
type KopiaStrategy struct {
|
||||
// Strategy implements the backup strategy using Kopia
|
||||
type Strategy struct {
|
||||
Retention Retention
|
||||
Provider KopiaProvider
|
||||
Provider Provider
|
||||
ConfigPath string
|
||||
SourcePath string
|
||||
}
|
||||
|
||||
// NewKopiaStrategy creates a new kopia strategy with specified provider
|
||||
func NewKopiaStrategy(retention Retention, provider KopiaProvider, configPath string) *KopiaStrategy {
|
||||
return &KopiaStrategy{
|
||||
// Retention represents retention policy for backups
|
||||
type Retention struct {
|
||||
KeepLatest int
|
||||
KeepHourly int
|
||||
KeepDaily int
|
||||
KeepWeekly int
|
||||
KeepMonthly int
|
||||
KeepYearly int
|
||||
}
|
||||
|
||||
// NewStrategy creates a new kopia strategy with specified provider
|
||||
func NewStrategy(retention Retention, provider Provider, configPath string, sourcePath string) *Strategy {
|
||||
return &Strategy{
|
||||
Retention: retention,
|
||||
Provider: provider,
|
||||
ConfigPath: configPath,
|
||||
SourcePath: sourcePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute performs the kopia backup
|
||||
func (s *KopiaStrategy) Execute(ctx context.Context, serviceName, directory string) error {
|
||||
func (s *Strategy) Execute(ctx context.Context, serviceName, directory string) error {
|
||||
log.Printf("Performing kopia backup for service %s: %s", serviceName, directory)
|
||||
|
||||
// Get or create password for this service
|
||||
password, err := getOrCreatePassword(serviceName, 48)
|
||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get or create password: %w", err)
|
||||
}
|
||||
@ -87,27 +100,21 @@ func (s *KopiaStrategy) Execute(ctx context.Context, serviceName, directory stri
|
||||
}
|
||||
|
||||
// ListBackups returns information about existing backups
|
||||
func (s *KopiaStrategy) ListBackups(ctx context.Context, serviceName string) ([]BackupInfo, error) {
|
||||
// Parse service group and index from service name (e.g., "backealocal.1")
|
||||
groupName, _, err := parseServiceName(serviceName)
|
||||
func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]models.BackupInfo, error) {
|
||||
// Parse service group and index from service name
|
||||
_, _, err := parseServiceName(serviceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid service name format: %w", err)
|
||||
}
|
||||
|
||||
// Get service directory from config
|
||||
factory, err := NewBackupFactory("config.yml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create factory: %w", err)
|
||||
// Use the source path from the Strategy instead of trying to get it from the factory
|
||||
directoryPath := s.SourcePath
|
||||
if directoryPath == "" {
|
||||
return nil, fmt.Errorf("source path not specified")
|
||||
}
|
||||
|
||||
// Find service group
|
||||
serviceGroup, exists := factory.Config.Services[groupName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("service group not found: %s", groupName)
|
||||
}
|
||||
|
||||
// Get directory from the service group level
|
||||
directoryPath := strings.TrimRight(serviceGroup.Source.Path, "/")
|
||||
// Trim trailing slash if any
|
||||
directoryPath = strings.TrimRight(directoryPath, "/")
|
||||
|
||||
// Ensure we're connected to the repository
|
||||
err = s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
@ -137,7 +144,7 @@ func (s *KopiaStrategy) ListBackups(ctx context.Context, serviceName string) ([]
|
||||
}
|
||||
|
||||
// Convert to BackupInfo
|
||||
var result []BackupInfo
|
||||
var result []models.BackupInfo
|
||||
for _, snap := range snapshots {
|
||||
// Get basic info
|
||||
id, _ := snap["id"].(string)
|
||||
@ -184,7 +191,7 @@ func (s *KopiaStrategy) ListBackups(ctx context.Context, serviceName string) ([]
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, BackupInfo{
|
||||
result = append(result, models.BackupInfo{
|
||||
ID: id,
|
||||
CreationTime: creationTime,
|
||||
Size: size,
|
||||
@ -198,7 +205,7 @@ func (s *KopiaStrategy) ListBackups(ctx context.Context, serviceName string) ([]
|
||||
}
|
||||
|
||||
// GetStorageUsage returns information about the total storage used by the repository
|
||||
func (s *KopiaStrategy) GetStorageUsage(ctx context.Context, serviceName string) (*StorageUsageInfo, error) {
|
||||
func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*models.StorageUsageInfo, error) {
|
||||
// Ensure we're connected to the repository
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
if err != nil {
|
||||
@ -207,17 +214,10 @@ func (s *KopiaStrategy) GetStorageUsage(ctx context.Context, serviceName string)
|
||||
|
||||
// Get provider type (b2, local, sftp)
|
||||
providerType := "unknown"
|
||||
switch s.Provider.(type) {
|
||||
case *KopiaB2Provider:
|
||||
providerType = "b2"
|
||||
case *KopiaLocalProvider:
|
||||
providerType = "local"
|
||||
case *KopiaSFTPProvider:
|
||||
providerType = "sftp"
|
||||
}
|
||||
providerType = s.Provider.GetProviderType()
|
||||
|
||||
// Initialize storage info
|
||||
info := &StorageUsageInfo{
|
||||
info := &models.StorageUsageInfo{
|
||||
Provider: providerType,
|
||||
ProviderID: s.Provider.GetBucketName(serviceName),
|
||||
}
|
||||
@ -273,195 +273,18 @@ func (s *KopiaStrategy) GetStorageUsage(ctx context.Context, serviceName string)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Helper method to ensure the repository is connected before operations
|
||||
func (s *KopiaStrategy) EnsureRepositoryConnected(ctx context.Context, serviceName string) error {
|
||||
// Check if kopia repository is already connected with config file
|
||||
cmd := exec.Command("kopia", "--config-file", s.ConfigPath, "repository", "status")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
// Repository not connected, try to connect
|
||||
password, err := getOrCreatePassword(serviceName, 48)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get password: %w", err)
|
||||
}
|
||||
|
||||
// Connect using provider
|
||||
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseServiceName splits a service name in the format "group.index" into its components
|
||||
func parseServiceName(serviceName string) (string, string, error) {
|
||||
parts := strings.SplitN(serviceName, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("service name must be in format 'group.index', got '%s'", serviceName)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
// getOrCreatePassword retrieves a password from kopia.env or creates a new one if it doesn't exist
|
||||
// Extracted to a package-level function since it's utility functionality
|
||||
func getOrCreatePassword(serviceName string, length int) (string, error) {
|
||||
// Define the expected key in the env file
|
||||
// Replace dots with underscores for environment variable name
|
||||
safeServiceName := strings.ReplaceAll(serviceName, ".", "_")
|
||||
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(safeServiceName))
|
||||
|
||||
// Try to read from kopia.env first
|
||||
kopiaEnvPath := "kopia.env"
|
||||
if _, err := os.Stat(kopiaEnvPath); err == nil {
|
||||
// File exists, check if the password is already there
|
||||
file, err := os.Open(kopiaEnvPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open kopia.env: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, passwordKey+"=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
// Found the password, remove quotes if present
|
||||
password := parts[1]
|
||||
password = strings.Trim(password, "\"")
|
||||
return password, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("error reading kopia.env: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Password not found or file doesn't exist, generate a new password
|
||||
password, err := generateSecurePassword(length)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate password: %w", err)
|
||||
}
|
||||
|
||||
// Create or append to kopia.env
|
||||
file, err := os.OpenFile(kopiaEnvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open kopia.env for writing: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Write the new password entry with quotes
|
||||
passwordEntry := fmt.Sprintf("%s=\"%s\"\n", passwordKey, password)
|
||||
if _, err := file.WriteString(passwordEntry); err != nil {
|
||||
return "", fmt.Errorf("failed to write to kopia.env: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Created new password for service %s and stored in kopia.env", serviceName)
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// generateSecurePassword creates a cryptographically secure random password
|
||||
// using only safe characters that work well with command line tools
|
||||
func generateSecurePassword(length int) (string, error) {
|
||||
// Use a more robust but safe character set
|
||||
// Avoiding characters that might cause shell interpretation issues
|
||||
// No quotes, backslashes, spaces, or common special chars that need escaping
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
result := make([]byte, length)
|
||||
|
||||
// Create a secure source of randomness
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ensure minimum complexity requirements
|
||||
// At least 2 uppercase, 2 lowercase, 2 digits
|
||||
if length >= 6 {
|
||||
// Force first characters to meet minimum requirements
|
||||
result[0] = 'A' + byte(randomBytes[0]%26) // Uppercase
|
||||
result[1] = 'A' + byte(randomBytes[1]%26) // Uppercase
|
||||
result[2] = 'a' + byte(randomBytes[2]%26) // Lowercase
|
||||
result[3] = 'a' + byte(randomBytes[3]%26) // Lowercase
|
||||
result[4] = '0' + byte(randomBytes[4]%10) // Digit
|
||||
result[5] = '0' + byte(randomBytes[5]%10) // Digit
|
||||
|
||||
// Fill the rest with random chars
|
||||
for i := 6; i < length; i++ {
|
||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||
}
|
||||
|
||||
// Shuffle the result to avoid predictable pattern
|
||||
for i := length - 1; i > 0; i-- {
|
||||
j := int(randomBytes[i]) % (i + 1)
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
} else {
|
||||
// For very short passwords just use random chars
|
||||
for i := 0; i < length; i++ {
|
||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||
}
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// parseHumanSize parses a human-readable size string (e.g., "32.8 MB") into bytes
|
||||
func parseHumanSize(sizeStr string) (int64, error) {
|
||||
parts := strings.Fields(sizeStr)
|
||||
if len(parts) != 2 {
|
||||
return 0, fmt.Errorf("invalid size format: %s", sizeStr)
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(parts[0], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid size value: %w", err)
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(parts[1])
|
||||
switch unit {
|
||||
case "B":
|
||||
return int64(value), nil
|
||||
case "KB", "KIB":
|
||||
return int64(value * 1024), nil
|
||||
case "MB", "MIB":
|
||||
return int64(value * 1024 * 1024), nil
|
||||
case "GB", "GIB":
|
||||
return int64(value * 1024 * 1024 * 1024), nil
|
||||
case "TB", "TIB":
|
||||
return int64(value * 1024 * 1024 * 1024 * 1024), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown size unit: %s", unit)
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreBackup restores a backup with the given ID
|
||||
func (s *KopiaStrategy) RestoreBackup(ctx context.Context, backupID string, serviceName string) error {
|
||||
func (s *Strategy) RestoreBackup(ctx context.Context, backupID string, serviceName string) error {
|
||||
// Ensure repository is connected
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
||||
}
|
||||
|
||||
// Parse service group and index from service name
|
||||
groupName, _, err := parseServiceName(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid service name format: %w", err)
|
||||
}
|
||||
|
||||
// Get service directory from config
|
||||
factory, err := NewBackupFactory("config.yml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create factory: %w", err)
|
||||
}
|
||||
|
||||
// Find service group
|
||||
serviceGroup, exists := factory.Config.Services[groupName]
|
||||
if !exists {
|
||||
return fmt.Errorf("service group not found: %s", groupName)
|
||||
// Get source directory from strategy instead of importing factory
|
||||
targetDir := s.SourcePath
|
||||
if targetDir == "" {
|
||||
return fmt.Errorf("source path not specified")
|
||||
}
|
||||
|
||||
// Create a temporary directory for restore
|
||||
@ -489,7 +312,6 @@ func (s *KopiaStrategy) RestoreBackup(ctx context.Context, backupID string, serv
|
||||
}
|
||||
|
||||
// Now we need to sync the restored data to the original directory
|
||||
targetDir := serviceGroup.Source.Path
|
||||
log.Printf("Syncing restored data from %s to %s", restoreDir, targetDir)
|
||||
|
||||
// Use rsync for the final transfer to avoid permissions issues
|
||||
@ -519,8 +341,7 @@ func (s *KopiaStrategy) RestoreBackup(ctx context.Context, backupID string, serv
|
||||
}
|
||||
|
||||
// DownloadBackup provides a reader with backup summary information in text format
|
||||
func (s *KopiaStrategy) DownloadBackup(ctx context.Context, backupID string, serviceName string) (io.ReadCloser, error) {
|
||||
|
||||
func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceName string) (io.ReadCloser, error) {
|
||||
// Ensure repository is connected
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
if err != nil {
|
||||
@ -647,8 +468,7 @@ func (s *KopiaStrategy) DownloadBackup(ctx context.Context, backupID string, ser
|
||||
}
|
||||
|
||||
// GetBackupInfo returns detailed information about a specific backup
|
||||
func (s *KopiaStrategy) GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*BackupInfo, error) {
|
||||
|
||||
func (s *Strategy) GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error) {
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to repository: %w", err)
|
||||
@ -717,7 +537,7 @@ func (s *KopiaStrategy) GetBackupInfo(ctx context.Context, backupID string, serv
|
||||
}
|
||||
}
|
||||
|
||||
return &BackupInfo{
|
||||
return &models.BackupInfo{
|
||||
ID: id,
|
||||
CreationTime: creationTime,
|
||||
Size: size,
|
||||
@ -727,6 +547,65 @@ func (s *KopiaStrategy) GetBackupInfo(ctx context.Context, backupID string, serv
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper method to ensure the repository is connected before operations
|
||||
func (s *Strategy) EnsureRepositoryConnected(ctx context.Context, serviceName string) error {
|
||||
// Check if kopia repository is already connected with config file
|
||||
cmd := exec.Command("kopia", "--config-file", s.ConfigPath, "repository", "status")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
// Repository not connected, try to connect
|
||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get password: %w", err)
|
||||
}
|
||||
|
||||
// Connect using provider
|
||||
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseServiceName splits a service name in the format "group.index" into its components
|
||||
func parseServiceName(serviceName string) (string, string, error) {
|
||||
parts := strings.SplitN(serviceName, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("service name must be in format 'group.index', got '%s'", serviceName)
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
// parseHumanSize parses a human-readable size string (e.g., "32.8 MB") into bytes
|
||||
func parseHumanSize(sizeStr string) (int64, error) {
|
||||
parts := strings.Fields(sizeStr)
|
||||
if len(parts) != 2 {
|
||||
return 0, fmt.Errorf("invalid size format: %s", sizeStr)
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(parts[0], 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid size value: %w", err)
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(parts[1])
|
||||
switch unit {
|
||||
case "B":
|
||||
return int64(value), nil
|
||||
case "KB", "KIB":
|
||||
return int64(value * 1024), nil
|
||||
case "MB", "MIB":
|
||||
return int64(value * 1024 * 1024), nil
|
||||
case "GB", "GIB":
|
||||
return int64(value * 1024 * 1024 * 1024), nil
|
||||
case "TB", "TIB":
|
||||
return int64(value * 1024 * 1024 * 1024 * 1024), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown size unit: %s", unit)
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupReadCloser is a wrapper around io.ReadCloser that performs cleanup when closed
|
||||
type cleanupReadCloser struct {
|
||||
io.ReadCloser
|
||||
@ -739,29 +618,3 @@ func (c *cleanupReadCloser) Close() error {
|
||||
c.cleanup()
|
||||
return err
|
||||
}
|
||||
|
||||
// extractServiceNameFromBackupID extracts the service name from a backup ID
|
||||
// This is an approximation as the exact format depends on your ID structure
|
||||
func extractServiceNameFromBackupID(backupID string) string {
|
||||
// Kopia snapshot IDs don't directly include the service name
|
||||
// You may need to adjust this based on your actual ID format
|
||||
|
||||
// Try to extract the pattern from your backups list
|
||||
// If you're using ListBackups to find all backups, you might have
|
||||
// a mapping of IDs to service names that you can use
|
||||
|
||||
// For this example, let's assume we're using environment variables to track this
|
||||
envVar := fmt.Sprintf("BACKEA_SNAPSHOT_%s", backupID)
|
||||
if serviceName := os.Getenv(envVar); serviceName != "" {
|
||||
return serviceName
|
||||
}
|
||||
|
||||
// Fallback: For testing, we'll return a default or parse from the first part
|
||||
// This should be replaced with your actual logic
|
||||
parts := strings.Split(backupID, "-")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
85
internal/backup/strategy/types.go
Normal file
85
internal/backup/strategy/types.go
Normal file
@ -0,0 +1,85 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"backea/internal/backup/models"
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackupInfo contains information about a single backup
|
||||
type BackupInfo struct {
|
||||
ID string // Unique identifier for the backup
|
||||
CreationTime time.Time // When the backup was created
|
||||
Size int64 // Size in bytes
|
||||
Source string // Original source path
|
||||
Type string // Type of backup (e.g., "kopia", "restic")
|
||||
RetentionTag string // Retention policy applied (e.g., "latest", "daily", "weekly")
|
||||
}
|
||||
|
||||
// StorageUsageInfo contains information about backup storage usage
|
||||
type StorageUsageInfo struct {
|
||||
TotalBytes int64 // Total bytes stored
|
||||
Provider string // Storage provider (e.g., "local", "b2", "s3")
|
||||
ProviderID string // Provider-specific ID (bucket name, path, etc.)
|
||||
}
|
||||
|
||||
// Strategy defines the backup/restore strategy interface
|
||||
type Strategy interface {
|
||||
// Execute performs a backup
|
||||
Execute(ctx context.Context, serviceName, directory string) error
|
||||
|
||||
// ListBackups gets information about existing backups
|
||||
ListBackups(ctx context.Context, serviceName string) ([]models.BackupInfo, error)
|
||||
|
||||
// GetStorageUsage returns information about the total storage used
|
||||
GetStorageUsage(ctx context.Context, serviceName string) (*models.StorageUsageInfo, error)
|
||||
|
||||
// RestoreBackup restores a backup to its original location
|
||||
RestoreBackup(ctx context.Context, backupID string, serviceName string) error
|
||||
|
||||
// DownloadBackup provides a reader with backup data for download
|
||||
DownloadBackup(ctx context.Context, backupID, serviceName string) (io.ReadCloser, error)
|
||||
|
||||
// GetBackupInfo returns detailed information about a specific backup
|
||||
GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error)
|
||||
}
|
||||
|
||||
// ServiceConfig represents the configuration for a service's backup
|
||||
type BackupConfig struct {
|
||||
BackupStrategy StrategyConfig // Strategy to use for backups
|
||||
}
|
||||
|
||||
// StrategyConfig defines configuration for a backup strategy
|
||||
type StrategyConfig struct {
|
||||
Type string // Type of backup strategy (e.g., "kopia", "rsync")
|
||||
Provider string // Storage provider (e.g., "local", "b2", "sftp")
|
||||
Destination DestinationConfig // Destination configuration
|
||||
Retention RetentionConfig // Retention policy
|
||||
}
|
||||
|
||||
// DestinationConfig defines backup destination settings
|
||||
type DestinationConfig struct {
|
||||
Host string // Host for remote destination
|
||||
Path string // Path for storage
|
||||
Username string // Username for authentication
|
||||
SSHKey string // SSH key for authentication
|
||||
}
|
||||
|
||||
// RetentionConfig defines backup retention policy
|
||||
type RetentionConfig struct {
|
||||
KeepLatest int // Number of latest snapshots to keep
|
||||
KeepHourly int // Number of hourly snapshots to keep
|
||||
KeepDaily int // Number of daily snapshots to keep
|
||||
KeepWeekly int // Number of weekly snapshots to keep
|
||||
KeepMonthly int // Number of monthly snapshots to keep
|
||||
KeepYearly int // Number of yearly snapshots to keep
|
||||
}
|
||||
|
||||
// Factory creates backup strategies
|
||||
// type Factory interface {
|
||||
// CreateBackupStrategyForService(groupName, serviceIndex string) (Strategy, error)
|
||||
// GetServicePath(groupName string) (string, error)
|
||||
// GetServiceConfig(groupName, serviceIndex string) (*BackupConfig, error)
|
||||
// ReloadConfig() error
|
||||
// }
|
@ -1,25 +1,21 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/web/routes"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
// New creates a new configured Echo server
|
||||
func New(factory *backup.BackupFactory) *echo.Echo {
|
||||
func New(factory strategy.Factory) *echo.Echo {
|
||||
e := echo.New()
|
||||
|
||||
// Add middleware
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// Setup static file serving
|
||||
e.Static("/static", "static")
|
||||
|
||||
// Register routes
|
||||
routes.RegisterRoutes(e, factory)
|
||||
|
||||
return e
|
||||
|
@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/strategy"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -18,11 +18,11 @@ import (
|
||||
|
||||
// BackupActionsHandler handles backup-related actions like restore and download
|
||||
type BackupActionsHandler struct {
|
||||
backupFactory *backup.BackupFactory
|
||||
backupFactory strategy.Factory
|
||||
}
|
||||
|
||||
// NewBackupActionsHandler creates a new backup actions handler
|
||||
func NewBackupActionsHandler(factory *backup.BackupFactory) *BackupActionsHandler {
|
||||
func NewBackupActionsHandler(factory strategy.Factory) *BackupActionsHandler {
|
||||
return &BackupActionsHandler{
|
||||
backupFactory: factory,
|
||||
}
|
||||
@ -185,7 +185,7 @@ func (h *BackupActionsHandler) RestoreBackupForm(c echo.Context) error {
|
||||
}
|
||||
|
||||
// restoreBackupToCustomDestination handles restoring a backup to a custom directory
|
||||
func (h *BackupActionsHandler) restoreBackupToCustomDestination(ctx context.Context, strategy backup.Strategy, backupID, serviceName, destinationPath string) error {
|
||||
func (h *BackupActionsHandler) restoreBackupToCustomDestination(ctx context.Context, strategy strategy.Strategy, backupID, serviceName, destinationPath string) error {
|
||||
// Validate destination path
|
||||
destinationPath = filepath.Clean(destinationPath)
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/config"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/templates"
|
||||
"context"
|
||||
"fmt"
|
||||
@ -16,10 +17,10 @@ import (
|
||||
)
|
||||
|
||||
type HomepageHandler struct {
|
||||
backupFactory *backup.BackupFactory
|
||||
backupFactory strategy.Factory
|
||||
}
|
||||
|
||||
func NewHomepageHandler(factory *backup.BackupFactory) *HomepageHandler {
|
||||
func NewHomepageHandler(factory strategy.Factory) *HomepageHandler {
|
||||
return &HomepageHandler{
|
||||
backupFactory: factory,
|
||||
}
|
||||
@ -28,14 +29,20 @@ func NewHomepageHandler(factory *backup.BackupFactory) *HomepageHandler {
|
||||
// Home handles the homepage request and displays latest backups by service
|
||||
func (h *HomepageHandler) Home(c echo.Context) error {
|
||||
// Create the data structures
|
||||
serviceBackups := make(map[string]map[string][]backup.BackupInfo)
|
||||
serviceBackups := make(map[string]map[string][]strategy.BackupInfo)
|
||||
serviceConfigs := make(map[string]map[string]templates.ServiceProviderInfo)
|
||||
groupDirectories := make(map[string]string) // Store directories by group name
|
||||
|
||||
// Get the configuration from the factory
|
||||
factoryConfig, err := h.getConfig()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get configuration: %v", err))
|
||||
}
|
||||
|
||||
// Process each service group
|
||||
for groupName, serviceGroup := range h.backupFactory.Config.Services {
|
||||
for groupName, serviceGroup := range factoryConfig.Services {
|
||||
// Initialize maps for this group
|
||||
serviceBackups[groupName] = make(map[string][]backup.BackupInfo)
|
||||
serviceBackups[groupName] = make(map[string][]strategy.BackupInfo)
|
||||
serviceConfigs[groupName] = make(map[string]templates.ServiceProviderInfo)
|
||||
|
||||
// Store the directory at the group level in a separate map
|
||||
@ -60,7 +67,6 @@ func (h *HomepageHandler) Home(c echo.Context) error {
|
||||
sort.Strings(sortedGroupNames)
|
||||
|
||||
// Render the template with sorted group names and directories
|
||||
// We don't load actual backup data on initial page load, it will be loaded via HTMX
|
||||
component := templates.Home(serviceBackups, serviceConfigs, sortedGroupNames, groupDirectories)
|
||||
return component.Render(c.Request().Context(), c.Response().Writer)
|
||||
}
|
||||
@ -69,14 +75,20 @@ func (h *HomepageHandler) Home(c echo.Context) error {
|
||||
func (h *HomepageHandler) ServiceGroupHeader(c echo.Context) error {
|
||||
groupName := c.Param("groupName")
|
||||
|
||||
// Get the configuration
|
||||
factoryConfig, err := h.getConfig()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get configuration: %v", err))
|
||||
}
|
||||
|
||||
// Check if the service group exists
|
||||
serviceGroup, exists := h.backupFactory.Config.Services[groupName]
|
||||
serviceGroup, exists := factoryConfig.Services[groupName]
|
||||
if !exists {
|
||||
return echo.NewHTTPError(404, "Service group not found")
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Service group not found")
|
||||
}
|
||||
|
||||
// Create data structures
|
||||
serviceBackups := make(map[string][]backup.BackupInfo)
|
||||
serviceBackups := make(map[string][]strategy.BackupInfo)
|
||||
serviceConfigs := make(map[string]templates.ServiceProviderInfo)
|
||||
|
||||
// Setup synchronization
|
||||
@ -140,14 +152,20 @@ func (h *HomepageHandler) ServiceGroupHeader(c echo.Context) error {
|
||||
func (h *HomepageHandler) ServiceGroupBackups(c echo.Context) error {
|
||||
groupName := c.Param("groupName")
|
||||
|
||||
// Get the configuration
|
||||
factoryConfig, err := h.getConfig()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get configuration: %v", err))
|
||||
}
|
||||
|
||||
// Check if the service group exists
|
||||
serviceGroup, exists := h.backupFactory.Config.Services[groupName]
|
||||
serviceGroup, exists := factoryConfig.Services[groupName]
|
||||
if !exists {
|
||||
return echo.NewHTTPError(404, "Service group not found")
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Service group not found")
|
||||
}
|
||||
|
||||
// Create data structures
|
||||
serviceBackups := make(map[string][]backup.BackupInfo)
|
||||
serviceBackups := make(map[string][]strategy.BackupInfo)
|
||||
serviceConfigs := make(map[string]templates.ServiceProviderInfo)
|
||||
|
||||
// Setup synchronization
|
||||
@ -198,7 +216,7 @@ func (h *HomepageHandler) ServiceGroupBackups(c echo.Context) error {
|
||||
wg.Wait()
|
||||
|
||||
// Create a map with just the group for the template
|
||||
groupServiceBackups := make(map[string]map[string][]backup.BackupInfo)
|
||||
groupServiceBackups := make(map[string]map[string][]strategy.BackupInfo)
|
||||
groupServiceBackups[groupName] = serviceBackups
|
||||
|
||||
// Create a map with just the group configs for the template
|
||||
@ -214,14 +232,20 @@ func (h *HomepageHandler) ServiceGroupBackups(c echo.Context) error {
|
||||
func (h *HomepageHandler) ServiceGroupAllBackups(c echo.Context) error {
|
||||
groupName := c.Param("groupName")
|
||||
|
||||
// Get the configuration
|
||||
factoryConfig, err := h.getConfig()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get configuration: %v", err))
|
||||
}
|
||||
|
||||
// Check if the service group exists
|
||||
serviceGroup, exists := h.backupFactory.Config.Services[groupName]
|
||||
serviceGroup, exists := factoryConfig.Services[groupName]
|
||||
if !exists {
|
||||
return echo.NewHTTPError(404, "Service group not found")
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Service group not found")
|
||||
}
|
||||
|
||||
// Create data structures
|
||||
serviceBackups := make(map[string][]backup.BackupInfo)
|
||||
serviceBackups := make(map[string][]strategy.BackupInfo)
|
||||
serviceConfigs := make(map[string]templates.ServiceProviderInfo)
|
||||
|
||||
// Setup synchronization
|
||||
@ -272,7 +296,7 @@ func (h *HomepageHandler) ServiceGroupAllBackups(c echo.Context) error {
|
||||
wg.Wait()
|
||||
|
||||
// Create a map with just the group for the template
|
||||
groupServiceBackups := make(map[string]map[string][]backup.BackupInfo)
|
||||
groupServiceBackups := make(map[string]map[string][]strategy.BackupInfo)
|
||||
groupServiceBackups[groupName] = serviceBackups
|
||||
|
||||
// Create a map with just the group configs for the template
|
||||
@ -315,3 +339,15 @@ func (h *HomepageHandler) ServiceGroupAllBackups(c echo.Context) error {
|
||||
component := templates.ServiceGroupAllBackupsTable(groupName, serviceBackups, groupServiceConfigs)
|
||||
return component.Render(c.Request().Context(), c.Response().Writer)
|
||||
}
|
||||
|
||||
// Helper method to get configuration from factory
|
||||
func (h *HomepageHandler) getConfig() (*config.Configuration, error) {
|
||||
// Type assert to get the concrete FactoryImpl type which has Config field
|
||||
if factoryImpl, ok := h.backupFactory.(*strategy.FactoryImpl); ok {
|
||||
return factoryImpl.Config, nil
|
||||
}
|
||||
|
||||
// If we can't access the Config directly, get it through factory methods
|
||||
// This is where we'd need to add methods to the Factory interface if not already there
|
||||
return nil, fmt.Errorf("cannot access configuration from factory")
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/web/handlers"
|
||||
"fmt"
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// RegisterRoutes sets up all the routes for the web application
|
||||
func RegisterRoutes(e *echo.Echo, factory *backup.BackupFactory) {
|
||||
func RegisterRoutes(e *echo.Echo, factory strategy.Factory) {
|
||||
// Create handlers with backup factory
|
||||
homeHandler := handlers.NewHomepageHandler(factory)
|
||||
actionsHandler := handlers.NewBackupActionsHandler(factory)
|
||||
|
@ -5,15 +5,16 @@ package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"backea/internal/backup"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/templates/layouts"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
templruntime "github.com/a-h/templ/runtime"
|
||||
)
|
||||
|
||||
// FormatSize formats byte size to human-readable format
|
||||
@ -156,7 +157,7 @@ func GetSortedBackups(serviceGroup map[string][]backup.BackupInfo) []BackupWithS
|
||||
}
|
||||
|
||||
// Home renders the homepage with lazy-loaded backup information
|
||||
func Home(serviceBackups map[string]map[string][]backup.BackupInfo, serviceConfigs map[string]map[string]ServiceProviderInfo, sortedGroupNames []string, groupDirectories map[string]string) templ.Component {
|
||||
func Home(serviceBackups map[string]map[string][]strategy.BackupInfo, serviceConfigs map[string]map[string]ServiceProviderInfo, sortedGroupNames []string, groupDirectories map[string]string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user