Better logging system
This commit is contained in:
parent
94f4359946
commit
506070edcc
28
internal/backup/core/error.go
Normal file
28
internal/backup/core/error.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// BackupError represents an error that occurred during backup operations
|
||||||
|
type BackupError struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface
|
||||||
|
func (e *BackupError) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying cause of the error
|
||||||
|
func (e *BackupError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackupError creates a new BackupError
|
||||||
|
func NewBackupError(message string, cause error) *BackupError {
|
||||||
|
return &BackupError{
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,9 @@ package core
|
|||||||
import (
|
import (
|
||||||
"backea/internal/backup/models"
|
"backea/internal/backup/models"
|
||||||
"backea/internal/backup/strategy"
|
"backea/internal/backup/strategy"
|
||||||
|
"backea/internal/logging"
|
||||||
"backea/internal/mail"
|
"backea/internal/mail"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,6 +15,7 @@ type Executor struct {
|
|||||||
Factory strategy.Factory
|
Factory strategy.Factory
|
||||||
Mailer *mail.Mailer
|
Mailer *mail.Mailer
|
||||||
concurrency int
|
concurrency int
|
||||||
|
Logger logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExecutor creates a new backup executor
|
// NewExecutor creates a new backup executor
|
||||||
@ -25,6 +25,7 @@ func NewExecutor(config *models.Configuration, factory strategy.Factory) *Execut
|
|||||||
Factory: factory,
|
Factory: factory,
|
||||||
// Mailer: mailer,
|
// Mailer: mailer,
|
||||||
concurrency: 5,
|
concurrency: 5,
|
||||||
|
Logger: *logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +70,8 @@ func (e *Executor) processAllServiceGroups(ctx context.Context) error {
|
|||||||
defer func() { <-sem }() // Release semaphore
|
defer func() { <-sem }() // Release semaphore
|
||||||
|
|
||||||
if err := e.processServiceGroup(ctx, group); err != nil {
|
if err := e.processServiceGroup(ctx, group); err != nil {
|
||||||
log.Printf("Failed to backup service group %s: %v", group, err)
|
e.Logger.Error("Failed to backup service group %s: %v", group, err)
|
||||||
errs <- fmt.Errorf("backup failed for group %s: %w", group, err)
|
errs <- NewBackupError("backup failed for group "+group, err)
|
||||||
}
|
}
|
||||||
}(groupName)
|
}(groupName)
|
||||||
}
|
}
|
||||||
@ -92,8 +93,8 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
|||||||
// Get service group configuration
|
// Get service group configuration
|
||||||
serviceGroup, exists := e.Config.Services[groupName]
|
serviceGroup, exists := e.Config.Services[groupName]
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Printf("Service group not found: %s", groupName)
|
e.Logger.Error("Service group not found: %s", groupName)
|
||||||
return fmt.Errorf("service group not found: %s", groupName)
|
return NewBackupError("service group not found: "+groupName, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create hook runner
|
// Create hook runner
|
||||||
@ -101,8 +102,8 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
|||||||
|
|
||||||
// Execute the before hook once for the entire group
|
// Execute the before hook once for the entire group
|
||||||
if err := hooks.RunBeforeHook(); err != nil {
|
if err := hooks.RunBeforeHook(); err != nil {
|
||||||
log.Printf("Failed to execute before hook for group %s: %v", groupName, err)
|
e.Logger.Error("Failed to execute before hook for group %s: %v", groupName, err)
|
||||||
return fmt.Errorf("before hook failed: %w", err)
|
return NewBackupError("before hook failed for group "+groupName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all services in the group in parallel
|
// Process all services in the group in parallel
|
||||||
@ -120,8 +121,8 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
|||||||
defer func() { <-sem }() // Release semaphore
|
defer func() { <-sem }() // Release semaphore
|
||||||
|
|
||||||
if err := e.processSpecificService(ctx, group, index); err != nil {
|
if err := e.processSpecificService(ctx, group, index); err != nil {
|
||||||
log.Printf("Failed to backup service %s.%s: %v", group, index, err)
|
e.Logger.Error("Failed to backup service %s.%s: %v", group, index, err)
|
||||||
errs <- fmt.Errorf("backup failed for %s.%s: %w", group, index, err)
|
errs <- NewBackupError("backup failed for "+group+"."+index, err)
|
||||||
}
|
}
|
||||||
}(groupName, configIndex)
|
}(groupName, configIndex)
|
||||||
}
|
}
|
||||||
@ -130,17 +131,21 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(errs)
|
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
|
var lastErr error
|
||||||
for err := range errs {
|
for err := range errs {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute the after hook once for the entire group
|
||||||
|
if err := hooks.RunAfterHook(); err != nil {
|
||||||
|
e.Logger.Error("Failed to execute after hook for group %s: %v", groupName, err)
|
||||||
|
// If no backup errors occurred, return the after hook error,
|
||||||
|
// otherwise prioritize the backup errors
|
||||||
|
if lastErr == nil {
|
||||||
|
lastErr = NewBackupError("after hook failed for group "+groupName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,28 +154,33 @@ func (e *Executor) processSpecificService(ctx context.Context, groupName string,
|
|||||||
// Get service configuration
|
// Get service configuration
|
||||||
serviceGroup, exists := e.Config.Services[groupName]
|
serviceGroup, exists := e.Config.Services[groupName]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("service group not found: %s", groupName)
|
return NewBackupError("service group not found: "+groupName, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the config index exists
|
// Check if the config index exists
|
||||||
_, exists = serviceGroup.BackupConfigs[configIndex]
|
_, exists = serviceGroup.BackupConfigs[configIndex]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("service index not found: %s.%s", groupName, configIndex)
|
return NewBackupError("service index not found: "+groupName+"."+configIndex, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the appropriate backup strategy using the factory
|
// Create the appropriate backup strategy using the factory
|
||||||
backupStrategy, err := e.Factory.CreateBackupStrategyForService(groupName, configIndex)
|
backupStrategy, err := e.Factory.CreateBackupStrategyForService(groupName, configIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create backup strategy for service %s.%s: %v", groupName, configIndex, err)
|
e.Logger.Error("Failed to create backup strategy for service %s.%s: %v", groupName, configIndex, err)
|
||||||
return fmt.Errorf("failed to create backup strategy: %w", err)
|
return NewBackupError("failed to create backup strategy for "+groupName+"."+configIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and run service
|
// Create and run service
|
||||||
service := NewService(
|
service := NewService(
|
||||||
fmt.Sprintf("%s.%s", groupName, configIndex),
|
groupName+"."+configIndex,
|
||||||
serviceGroup.Source.Path,
|
serviceGroup.Source.Path,
|
||||||
backupStrategy,
|
backupStrategy,
|
||||||
e.Mailer,
|
e.Mailer,
|
||||||
)
|
)
|
||||||
return service.Backup(ctx)
|
|
||||||
|
if err := service.Backup(ctx); err != nil {
|
||||||
|
return NewBackupError("backup execution failed for "+groupName+"."+configIndex, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,56 +1,64 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"backea/internal/logging"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookRunner runs hooks before and after backups
|
|
||||||
type HookRunner struct {
|
type HookRunner struct {
|
||||||
Directory string
|
Directory string
|
||||||
BeforeHook string
|
BeforeHook string
|
||||||
AfterHook string
|
AfterHook string
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHookRunner creates a new hook runner
|
|
||||||
func NewHookRunner(directory, beforeHook, afterHook string) *HookRunner {
|
func NewHookRunner(directory, beforeHook, afterHook string) *HookRunner {
|
||||||
return &HookRunner{
|
return &HookRunner{
|
||||||
Directory: directory,
|
Directory: directory,
|
||||||
BeforeHook: beforeHook,
|
BeforeHook: beforeHook,
|
||||||
AfterHook: afterHook,
|
AfterHook: afterHook,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunBeforeHook executes the before hook if defined
|
|
||||||
func (h *HookRunner) RunBeforeHook() error {
|
func (h *HookRunner) RunBeforeHook() error {
|
||||||
if h.BeforeHook == "" {
|
if h.BeforeHook == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
h.logger.Info("Running before hook: %s", h.BeforeHook)
|
||||||
log.Printf("Running before hook: %s", h.BeforeHook)
|
err := h.runCommand(h.BeforeHook)
|
||||||
return h.runCommand(h.BeforeHook)
|
if err != nil {
|
||||||
|
return NewBackupError("before hook execution failed", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunAfterHook executes the after hook if defined
|
|
||||||
func (h *HookRunner) RunAfterHook() error {
|
func (h *HookRunner) RunAfterHook() error {
|
||||||
if h.AfterHook == "" {
|
if h.AfterHook == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
h.logger.Info("Running after hook: %s", h.AfterHook)
|
||||||
log.Printf("Running after hook: %s", h.AfterHook)
|
err := h.runCommand(h.AfterHook)
|
||||||
return h.runCommand(h.AfterHook)
|
if err != nil {
|
||||||
|
return NewBackupError("after hook execution failed", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runCommand executes a shell command in the specified directory
|
|
||||||
func (h *HookRunner) runCommand(command string) error {
|
func (h *HookRunner) runCommand(command string) error {
|
||||||
cmd := exec.Command("sh", "-c", command)
|
cmd := exec.Command("sh", "-c", command)
|
||||||
if h.Directory != "" {
|
if h.Directory != "" {
|
||||||
cmd.Dir = h.Directory
|
cmd.Dir = h.Directory
|
||||||
}
|
}
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
log.Printf("Executing command in %s: %s", h.Directory, command)
|
h.logger.Info("Executing command in %s: %s", h.Directory, command)
|
||||||
return cmd.Run()
|
|
||||||
|
outputBytes, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.ErrorWithStack(err, "Command execution failed: %s", string(outputBytes))
|
||||||
|
return NewBackupError("command execution failed: "+string(outputBytes), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Debug("Command output: %s", string(outputBytes))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -3,74 +3,76 @@ package core
|
|||||||
import (
|
import (
|
||||||
"backea/internal/backup/models"
|
"backea/internal/backup/models"
|
||||||
"backea/internal/backup/strategy"
|
"backea/internal/backup/strategy"
|
||||||
|
"backea/internal/logging"
|
||||||
"backea/internal/mail"
|
"backea/internal/mail"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service represents a service to be backed up
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string
|
Name string
|
||||||
Directory string
|
Directory string
|
||||||
Strategy strategy.Strategy
|
Strategy strategy.Strategy
|
||||||
Mailer *mail.Mailer
|
Mailer *mail.Mailer
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new backup service
|
|
||||||
func NewService(name string, directory string, strategy strategy.Strategy, mailer *mail.Mailer) *Service {
|
func NewService(name string, directory string, strategy strategy.Strategy, mailer *mail.Mailer) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
Name: name,
|
Name: name,
|
||||||
Directory: directory,
|
Directory: directory,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
Mailer: mailer,
|
Mailer: mailer,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup performs the backup for this service
|
|
||||||
func (s *Service) Backup(ctx context.Context) error {
|
func (s *Service) Backup(ctx context.Context) error {
|
||||||
log.Printf("Backing up service: %s", s.Name)
|
s.logger.Info("Backing up service: %s", s.Name)
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
if _, err := os.Stat(s.Directory); os.IsNotExist(err) {
|
if _, err := os.Stat(s.Directory); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("directory does not exist: %s", s.Directory)
|
return NewBackupError("directory does not exist: "+s.Directory, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the backup strategy
|
|
||||||
if err := s.Strategy.Execute(ctx, s.Name, s.Directory); err != nil {
|
if err := s.Strategy.Execute(ctx, s.Name, s.Directory); err != nil {
|
||||||
if s.Mailer != nil {
|
if s.Mailer != nil {
|
||||||
s.Mailer.SendErrorNotification(s.Name, err)
|
s.Mailer.SendErrorNotification(s.Name, err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("backup failed: %w", err)
|
return NewBackupError("backup failed for "+s.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record backup completion
|
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
log.Printf("Backup completed for %s in %v", s.Name, duration)
|
s.logger.Info("Backup completed for %s in %v", s.Name, duration)
|
||||||
|
|
||||||
// Send success notification
|
|
||||||
if s.Mailer != nil {
|
if s.Mailer != nil {
|
||||||
s.Mailer.SendSuccessNotification(s.Name, duration)
|
s.Mailer.SendSuccessNotification(s.Name, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBackupInfos retrieves information about existing backups
|
|
||||||
func (s *Service) GetBackupInfos(ctx context.Context) ([]models.BackupInfo, error) {
|
func (s *Service) GetBackupInfos(ctx context.Context) ([]models.BackupInfo, error) {
|
||||||
return s.Strategy.ListBackups(ctx, s.Name)
|
infos, err := s.Strategy.ListBackups(ctx, s.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewBackupError("failed to list backups for "+s.Name, err)
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStorageUsage retrieves information about storage usage
|
|
||||||
func (s *Service) GetStorageUsage(ctx context.Context) (*models.StorageUsageInfo, error) {
|
func (s *Service) GetStorageUsage(ctx context.Context) (*models.StorageUsageInfo, error) {
|
||||||
return s.Strategy.GetStorageUsage(ctx, s.Name)
|
usage, err := s.Strategy.GetStorageUsage(ctx, s.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewBackupError("failed to get storage usage for "+s.Name, err)
|
||||||
|
}
|
||||||
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreBackup restores a backup by ID
|
|
||||||
func (s *Service) RestoreBackup(ctx context.Context, backupID string) error {
|
func (s *Service) RestoreBackup(ctx context.Context, backupID string) error {
|
||||||
log.Printf("Restoring backup %s for service %s", backupID, s.Name)
|
s.logger.Info("Restoring backup %s for service %s", backupID, s.Name)
|
||||||
return s.Strategy.RestoreBackup(ctx, backupID, s.Name)
|
|
||||||
|
if err := s.Strategy.RestoreBackup(ctx, backupID, s.Name); err != nil {
|
||||||
|
return NewBackupError("failed to restore backup "+backupID+" for "+s.Name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"backea/internal/logging"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -9,10 +9,17 @@ import (
|
|||||||
|
|
||||||
// Configuration holds the complete application configuration
|
// Configuration holds the complete application configuration
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
Defaults DefaultsConfig `mapstructure:"defaults"`
|
Defaults DefaultsConfig `mapstructure:"defaults"`
|
||||||
Services map[string]ServiceGroup `mapstructure:"services"`
|
Services map[string]ServiceGroup `mapstructure:"services"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoggingConfig holds logging configuration
|
||||||
|
type LoggingConfig struct {
|
||||||
|
LogLevel string `mapstructure:"log_level"`
|
||||||
|
LogFile string `mapstructure:"log_file"`
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultsConfig holds the default settings for all services
|
// DefaultsConfig holds the default settings for all services
|
||||||
type DefaultsConfig struct {
|
type DefaultsConfig struct {
|
||||||
Retention RetentionConfig `mapstructure:"retention"`
|
Retention RetentionConfig `mapstructure:"retention"`
|
||||||
@ -71,27 +78,61 @@ type DestConfig struct {
|
|||||||
Username string `mapstructure:"username"`
|
Username string `mapstructure:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseLogLevel converts string log level to LogLevel
|
||||||
|
func parseLogLevel(level string) logging.LogLevel {
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
return logging.DEBUG
|
||||||
|
case "info":
|
||||||
|
return logging.INFO
|
||||||
|
case "warn":
|
||||||
|
return logging.WARN
|
||||||
|
case "error":
|
||||||
|
return logging.ERROR
|
||||||
|
case "fatal":
|
||||||
|
return logging.FATAL
|
||||||
|
default:
|
||||||
|
return logging.INFO // Default to INFO if not specified or invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads the application configuration from a file
|
// LoadConfig loads the application configuration from a file
|
||||||
func LoadConfig(configPath string) (*Configuration, error) {
|
func LoadConfig(configPath string) (*Configuration, error) {
|
||||||
|
// Create a logger first with default settings
|
||||||
|
logger := logging.GetLogger()
|
||||||
|
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
log.Printf("Warning: Error loading .env file: %v", err)
|
logger.Warn("Error loading .env file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetConfigFile(configPath)
|
viper.SetConfigFile(configPath)
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
logger.Error("Failed to read config file: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Configuration
|
var config Configuration
|
||||||
if err := viper.Unmarshal(&config); err != nil {
|
if err := viper.Unmarshal(&config); err != nil {
|
||||||
|
logger.Error("Failed to unmarshal config: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize logger with config settings
|
||||||
|
logLevel := parseLogLevel(config.Logging.LogLevel)
|
||||||
|
logging.InitLogger(logLevel, config.Logging.LogFile)
|
||||||
|
|
||||||
|
// Get the potentially reconfigured logger
|
||||||
|
logger = logging.GetLogger()
|
||||||
|
logger.Info("Initialized logger with level %v and output file %s",
|
||||||
|
config.Logging.LogLevel, config.Logging.LogFile)
|
||||||
|
|
||||||
// Apply default retention settings where needed
|
// Apply default retention settings where needed
|
||||||
for serviceName, service := range config.Services {
|
for serviceName, service := range config.Services {
|
||||||
for configID, backupConfig := range service.BackupConfigs {
|
for configID, backupConfig := range service.BackupConfigs {
|
||||||
// If retention is not set, use defaults
|
// If retention is not set, use defaults
|
||||||
if isRetentionEmpty(backupConfig.BackupStrategy.Retention) {
|
if isRetentionEmpty(backupConfig.BackupStrategy.Retention) {
|
||||||
|
logger.Debug("Applying default retention settings for service %s, config %s",
|
||||||
|
serviceName, configID)
|
||||||
backupConfig.BackupStrategy.Retention = config.Defaults.Retention
|
backupConfig.BackupStrategy.Retention = config.Defaults.Retention
|
||||||
service.BackupConfigs[configID] = backupConfig
|
service.BackupConfigs[configID] = backupConfig
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,48 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backea/internal/logging"
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetOrCreatePassword retrieves a password from kopia.env or creates a new one if it doesn't exist
|
type SecurityError struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SecurityError) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SecurityError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecurityError(message string, cause error) *SecurityError {
|
||||||
|
return &SecurityError{
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
||||||
// Define the expected key in the env file
|
logger := logging.GetLogger()
|
||||||
// Replace dots with underscores for environment variable name
|
|
||||||
safeServiceName := strings.ReplaceAll(serviceName, ".", "_")
|
safeServiceName := strings.ReplaceAll(serviceName, ".", "_")
|
||||||
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(safeServiceName))
|
passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(safeServiceName))
|
||||||
|
|
||||||
// Try to read from kopia.env first
|
|
||||||
kopiaEnvPath := "kopia.env"
|
kopiaEnvPath := "kopia.env"
|
||||||
if _, err := os.Stat(kopiaEnvPath); err == nil {
|
if _, err := os.Stat(kopiaEnvPath); err == nil {
|
||||||
// File exists, check if the password is already there
|
|
||||||
file, err := os.Open(kopiaEnvPath)
|
file, err := os.Open(kopiaEnvPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to open kopia.env: %w", err)
|
return "", NewSecurityError("failed to open kopia.env", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
@ -32,7 +52,6 @@ func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
|||||||
if strings.HasPrefix(line, passwordKey+"=") {
|
if strings.HasPrefix(line, passwordKey+"=") {
|
||||||
parts := strings.SplitN(line, "=", 2)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
// Found the password, remove quotes if present
|
|
||||||
password := parts[1]
|
password := parts[1]
|
||||||
password = strings.Trim(password, "\"")
|
password = strings.Trim(password, "\"")
|
||||||
return password, nil
|
return password, nil
|
||||||
@ -41,71 +60,56 @@ func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return "", fmt.Errorf("error reading kopia.env: %w", err)
|
return "", NewSecurityError("error reading kopia.env", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password not found or file doesn't exist, generate a new password
|
|
||||||
password, err := GenerateSecurePassword(length)
|
password, err := GenerateSecurePassword(length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to generate password: %w", err)
|
return "", NewSecurityError("failed to generate password", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or append to kopia.env
|
|
||||||
file, err := os.OpenFile(kopiaEnvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
file, err := os.OpenFile(kopiaEnvPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to open kopia.env for writing: %w", err)
|
return "", NewSecurityError("failed to open kopia.env for writing", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Write the new password entry with quotes
|
|
||||||
passwordEntry := fmt.Sprintf("%s=\"%s\"\n", passwordKey, password)
|
passwordEntry := fmt.Sprintf("%s=\"%s\"\n", passwordKey, password)
|
||||||
if _, err := file.WriteString(passwordEntry); err != nil {
|
if _, err := file.WriteString(passwordEntry); err != nil {
|
||||||
return "", fmt.Errorf("failed to write to kopia.env: %w", err)
|
return "", NewSecurityError("failed to write to kopia.env", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Created new password for service %s and stored in kopia.env", serviceName)
|
logger.Info("Created new password for service %s and stored in kopia.env", serviceName)
|
||||||
return password, nil
|
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) {
|
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"
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
result := make([]byte, length)
|
result := make([]byte, length)
|
||||||
|
|
||||||
// Create a secure source of randomness
|
|
||||||
randomBytes := make([]byte, length)
|
randomBytes := make([]byte, length)
|
||||||
if _, err := rand.Read(randomBytes); err != nil {
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
return "", err
|
return "", NewSecurityError("failed to generate random bytes", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure minimum complexity requirements
|
|
||||||
// At least 2 uppercase, 2 lowercase, 2 digits
|
|
||||||
if length >= 6 {
|
if length >= 6 {
|
||||||
// Force first characters to meet minimum requirements
|
result[0] = 'A' + byte(randomBytes[0]%26)
|
||||||
result[0] = 'A' + byte(randomBytes[0]%26) // Uppercase
|
result[1] = 'A' + byte(randomBytes[1]%26)
|
||||||
result[1] = 'A' + byte(randomBytes[1]%26) // Uppercase
|
result[2] = 'a' + byte(randomBytes[2]%26)
|
||||||
result[2] = 'a' + byte(randomBytes[2]%26) // Lowercase
|
result[3] = 'a' + byte(randomBytes[3]%26)
|
||||||
result[3] = 'a' + byte(randomBytes[3]%26) // Lowercase
|
result[4] = '0' + byte(randomBytes[4]%10)
|
||||||
result[4] = '0' + byte(randomBytes[4]%10) // Digit
|
result[5] = '0' + byte(randomBytes[5]%10)
|
||||||
result[5] = '0' + byte(randomBytes[5]%10) // Digit
|
|
||||||
|
|
||||||
// Fill the rest with random chars
|
|
||||||
for i := 6; i < length; i++ {
|
for i := 6; i < length; i++ {
|
||||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle the result to avoid predictable pattern
|
|
||||||
for i := length - 1; i > 0; i-- {
|
for i := length - 1; i > 0; i-- {
|
||||||
j := int(randomBytes[i]) % (i + 1)
|
j := int(randomBytes[i]) % (i + 1)
|
||||||
result[i], result[j] = result[j], result[i]
|
result[i], result[j] = result[j], result[i]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For very short passwords just use random chars
|
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
result[i] = chars[int(randomBytes[i])%len(chars)]
|
result[i] = chars[int(randomBytes[i])%len(chars)]
|
||||||
}
|
}
|
||||||
|
@ -3,25 +3,55 @@ package strategy
|
|||||||
import (
|
import (
|
||||||
"backea/internal/backup/models"
|
"backea/internal/backup/models"
|
||||||
"backea/internal/backup/strategy/kopia"
|
"backea/internal/backup/strategy/kopia"
|
||||||
|
"backea/internal/logging"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StrategyError represents an error in strategy operations
|
||||||
|
type StrategyError struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StrategyError) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StrategyError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStrategyError(message string, cause error) *StrategyError {
|
||||||
|
return &StrategyError{
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Factory handles backup strategy creation
|
// Factory handles backup strategy creation
|
||||||
type Factory struct {
|
type Factory struct {
|
||||||
Config *models.Configuration
|
Config *models.Configuration
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFactory initializes and returns a Factory
|
// NewFactory initializes and returns a Factory
|
||||||
func NewFactory(configPath string) (*Factory, error) {
|
func NewFactory(configPath string) (*Factory, error) {
|
||||||
cfg, err := models.LoadConfig(configPath)
|
cfg, err := models.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
return nil, NewStrategyError("failed to load config", err)
|
||||||
}
|
}
|
||||||
return &Factory{Config: cfg, ConfigPath: configPath}, nil
|
return &Factory{
|
||||||
|
Config: cfg,
|
||||||
|
ConfigPath: configPath,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBackupStrategyForService returns a backup strategy based on config
|
// CreateBackupStrategyForService returns a backup strategy based on config
|
||||||
@ -30,6 +60,7 @@ func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcePath, err := f.getServicePath(groupName)
|
sourcePath, err := f.getServicePath(groupName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -39,7 +70,7 @@ func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string)
|
|||||||
case "kopia":
|
case "kopia":
|
||||||
return f.createKopiaStrategy(serviceConfig, sourcePath)
|
return f.createKopiaStrategy(serviceConfig, sourcePath)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown strategy type: %s", serviceConfig.BackupStrategy.Type)
|
return nil, NewStrategyError(fmt.Sprintf("unknown strategy type: %s", serviceConfig.BackupStrategy.Type), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +78,7 @@ func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string)
|
|||||||
func (f *Factory) createKopiaStrategy(serviceConfig *models.BackupConfig, sourcePath string) (Strategy, error) {
|
func (f *Factory) createKopiaStrategy(serviceConfig *models.BackupConfig, sourcePath string) (Strategy, error) {
|
||||||
provider, err := f.createKopiaProvider(serviceConfig.BackupStrategy)
|
provider, err := f.createKopiaProvider(serviceConfig.BackupStrategy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create kopia provider: %w", err)
|
return nil, NewStrategyError("failed to create kopia provider", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
retention := kopia.Retention(serviceConfig.BackupStrategy.Retention)
|
retention := kopia.Retention(serviceConfig.BackupStrategy.Retention)
|
||||||
@ -61,16 +92,19 @@ func (f *Factory) createKopiaProvider(strategyConfig models.StrategyConfig) (kop
|
|||||||
switch strategyConfig.Provider {
|
switch strategyConfig.Provider {
|
||||||
case "local":
|
case "local":
|
||||||
return kopia.NewLocalProvider(strategyConfig.Destination.Path), nil
|
return kopia.NewLocalProvider(strategyConfig.Destination.Path), nil
|
||||||
|
|
||||||
case "b2", "backblaze":
|
case "b2", "backblaze":
|
||||||
return kopia.NewB2Provider(), nil
|
return kopia.NewB2Provider(), nil
|
||||||
|
|
||||||
case "sftp":
|
case "sftp":
|
||||||
host := strategyConfig.Destination.Host
|
host := strategyConfig.Destination.Host
|
||||||
basepath := strategyConfig.Destination.Path
|
basepath := strategyConfig.Destination.Path
|
||||||
username := strategyConfig.Destination.Username
|
username := strategyConfig.Destination.Username
|
||||||
keyFile := strategyConfig.Destination.SSHKey
|
keyFile := strategyConfig.Destination.SSHKey
|
||||||
return kopia.NewSFTPProvider(host, basepath, username, keyFile), nil
|
return kopia.NewSFTPProvider(host, basepath, username, keyFile), nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown kopia provider type: %s", strategyConfig.Provider)
|
return nil, NewStrategyError(fmt.Sprintf("unknown kopia provider type: %s", strategyConfig.Provider), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +113,7 @@ func (f *Factory) getServicePath(groupName string) (string, error) {
|
|||||||
if serviceGroup, exists := f.Config.Services[groupName]; exists {
|
if serviceGroup, exists := f.Config.Services[groupName]; exists {
|
||||||
return serviceGroup.Source.Path, nil
|
return serviceGroup.Source.Path, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("service group not found: %s", groupName)
|
return "", NewStrategyError(fmt.Sprintf("service group not found: %s", groupName), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getServiceConfig returns a service's config
|
// getServiceConfig returns a service's config
|
||||||
@ -88,15 +122,16 @@ func (f *Factory) getServiceConfig(groupName, serviceIndex string) (*models.Back
|
|||||||
if backupConfig, exists := serviceGroup.BackupConfigs[serviceIndex]; exists {
|
if backupConfig, exists := serviceGroup.BackupConfigs[serviceIndex]; exists {
|
||||||
return &backupConfig, nil
|
return &backupConfig, nil
|
||||||
}
|
}
|
||||||
|
return nil, NewStrategyError(fmt.Sprintf("service index not found: %s.%s", groupName, serviceIndex), nil)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("backup config not found: %s.%s", groupName, serviceIndex)
|
return nil, NewStrategyError(fmt.Sprintf("service group not found: %s", groupName), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadConfig reloads configuration
|
// ReloadConfig reloads configuration
|
||||||
func (f *Factory) ReloadConfig() error {
|
func (f *Factory) ReloadConfig() error {
|
||||||
cfg, err := models.LoadConfig(f.ConfigPath)
|
cfg, err := models.LoadConfig(f.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not reload config: %w", err)
|
return NewStrategyError("could not reload config", err)
|
||||||
}
|
}
|
||||||
f.Config = cfg
|
f.Config = cfg
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package kopia
|
package kopia
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backea/internal/logging"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@ -11,42 +11,71 @@ import (
|
|||||||
b2_client "backea/internal/client"
|
b2_client "backea/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// KopiaError represents an error in Kopia operations
|
||||||
|
type KopiaError struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *KopiaError) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *KopiaError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKopiaError(message string, cause error) *KopiaError {
|
||||||
|
return &KopiaError{
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// B2Provider implements the Provider interface for Backblaze B2
|
// B2Provider implements the Provider interface for Backblaze B2
|
||||||
type B2Provider struct{}
|
type B2Provider struct {
|
||||||
|
logger *logging.Logger
|
||||||
|
}
|
||||||
|
|
||||||
// NewB2Provider creates a new B2 provider
|
// NewB2Provider creates a new B2 provider
|
||||||
func NewB2Provider() *B2Provider {
|
func NewB2Provider() *B2Provider {
|
||||||
return &B2Provider{}
|
return &B2Provider{
|
||||||
|
logger: logging.GetLogger(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect connects to a B2 repository
|
// Connect connects to a B2 repository
|
||||||
func (p *B2Provider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
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")
|
keyID := os.Getenv("B2_KEY_ID")
|
||||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||||
if keyID == "" || applicationKey == "" {
|
if keyID == "" || applicationKey == "" {
|
||||||
return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
return NewKopiaError("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketName := p.GetBucketName(serviceName)
|
bucketName := p.GetBucketName(serviceName)
|
||||||
|
|
||||||
// Create B2 client and check if bucket exists, create if not
|
B2Client, err := b2_client.NewClientFromEnv()
|
||||||
B2Client, _ := b2_client.NewClientFromEnv()
|
if err != nil {
|
||||||
if B2Client == nil {
|
return NewKopiaError("failed to initialize B2 client", err)
|
||||||
return fmt.Errorf("B2 client not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := B2Client.GetBucket(ctx, bucketName)
|
if B2Client == nil {
|
||||||
|
return NewKopiaError("B2 client not initialized", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = B2Client.GetBucket(ctx, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Bucket %s not found, creating...", bucketName)
|
p.logger.Info("Bucket %s not found, creating...", bucketName)
|
||||||
_, err = B2Client.CreateBucket(ctx, bucketName, false)
|
_, err = B2Client.CreateBucket(ctx, bucketName, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create bucket: %w", err)
|
return NewKopiaError("failed to create bucket", err)
|
||||||
}
|
}
|
||||||
log.Printf("Created bucket: %s", bucketName)
|
p.logger.Info("Created bucket: %s", bucketName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to connect to existing repository with config file
|
|
||||||
connectCmd := exec.CommandContext(
|
connectCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -57,10 +86,10 @@ func (p *B2Provider) Connect(ctx context.Context, serviceName string, password s
|
|||||||
"--key", applicationKey,
|
"--key", applicationKey,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = connectCmd.Run()
|
err = connectCmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Connection failed, create new repository
|
p.logger.Info("Creating new B2 repository for %s", serviceName)
|
||||||
log.Printf("Creating new B2 repository for %s", serviceName)
|
|
||||||
createCmd := exec.CommandContext(
|
createCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -71,9 +100,10 @@ func (p *B2Provider) Connect(ctx context.Context, serviceName string, password s
|
|||||||
"--key", applicationKey,
|
"--key", applicationKey,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
createOutput, err := createCmd.CombinedOutput()
|
createOutput, err := createCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
return NewKopiaError(fmt.Sprintf("failed to create repository: %s", string(createOutput)), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,11 +115,10 @@ func (p *B2Provider) GetRepositoryParams(serviceName string) ([]string, error) {
|
|||||||
keyID := os.Getenv("B2_KEY_ID")
|
keyID := os.Getenv("B2_KEY_ID")
|
||||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||||
if keyID == "" || applicationKey == "" {
|
if keyID == "" || applicationKey == "" {
|
||||||
return nil, fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
return nil, NewKopiaError("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketName := p.GetBucketName(serviceName)
|
bucketName := p.GetBucketName(serviceName)
|
||||||
|
|
||||||
return []string{
|
return []string{
|
||||||
"b2",
|
"b2",
|
||||||
"--bucket", bucketName,
|
"--bucket", bucketName,
|
||||||
@ -100,10 +129,7 @@ func (p *B2Provider) GetRepositoryParams(serviceName string) ([]string, error) {
|
|||||||
|
|
||||||
// GetBucketName returns a bucket name for the service
|
// GetBucketName returns a bucket name for the service
|
||||||
func (p *B2Provider) GetBucketName(serviceName string) string {
|
func (p *B2Provider) GetBucketName(serviceName string) string {
|
||||||
// Backblaze doesn't allow dots in bucket names
|
|
||||||
sanitized := strings.ReplaceAll(serviceName, ".", "-")
|
sanitized := strings.ReplaceAll(serviceName, ".", "-")
|
||||||
|
|
||||||
// Add a prefix to make the bucket name unique
|
|
||||||
return fmt.Sprintf("backea-%s", sanitized)
|
return fmt.Sprintf("backea-%s", sanitized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package kopia
|
package kopia
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backea/internal/logging"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -12,29 +12,29 @@ import (
|
|||||||
// LocalProvider implements the Provider interface for local storage
|
// LocalProvider implements the Provider interface for local storage
|
||||||
type LocalProvider struct {
|
type LocalProvider struct {
|
||||||
BasePath string
|
BasePath string
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocalProvider creates a new local provider
|
// NewLocalProvider creates a new local provider
|
||||||
func NewLocalProvider(basePath string) *LocalProvider {
|
func NewLocalProvider(basePath string) *LocalProvider {
|
||||||
// If basePath is empty, use a default location
|
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos")
|
basePath = filepath.Join(os.Getenv("HOME"), ".backea", "repos")
|
||||||
}
|
}
|
||||||
return &LocalProvider{
|
return &LocalProvider{
|
||||||
BasePath: basePath,
|
BasePath: basePath,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect connects to a local repository
|
// Connect connects to a local repository
|
||||||
func (p *LocalProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
func (p *LocalProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||||
repoPath := filepath.Join(p.BasePath, serviceName)
|
repoPath := filepath.Join(p.BasePath, serviceName)
|
||||||
log.Printf("Connecting to local repository at %s with config: %s", repoPath, configPath)
|
p.logger.Info("Connecting to local repository at %s with config: %s", repoPath, configPath)
|
||||||
|
|
||||||
if err := os.MkdirAll(repoPath, 0755); err != nil {
|
if err := os.MkdirAll(repoPath, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create repository directory: %w", err)
|
return NewKopiaError("failed to create repository directory", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to connect to existing repository with config file
|
|
||||||
connectCmd := exec.CommandContext(
|
connectCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -43,10 +43,10 @@ func (p *LocalProvider) Connect(ctx context.Context, serviceName string, passwor
|
|||||||
"--path", repoPath,
|
"--path", repoPath,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
err := connectCmd.Run()
|
err := connectCmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Connection failed, create new repository
|
p.logger.Info("Creating new local repository for %s at destination: %s", serviceName, repoPath)
|
||||||
log.Printf("Creating new local repository for %s at destination: %s", serviceName, repoPath)
|
|
||||||
createCmd := exec.CommandContext(
|
createCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -55,9 +55,10 @@ func (p *LocalProvider) Connect(ctx context.Context, serviceName string, passwor
|
|||||||
"--path", repoPath,
|
"--path", repoPath,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
createOutput, err := createCmd.CombinedOutput()
|
createOutput, err := createCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
return NewKopiaError(fmt.Sprintf("failed to create repository: %s", string(createOutput)), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package kopia
|
package kopia
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backea/internal/logging"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,6 +15,7 @@ type SFTPProvider struct {
|
|||||||
BasePath string
|
BasePath string
|
||||||
Username string
|
Username string
|
||||||
KeyFile string
|
KeyFile string
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSFTPProvider creates a new SFTP provider
|
// NewSFTPProvider creates a new SFTP provider
|
||||||
@ -24,20 +25,14 @@ func NewSFTPProvider(host, basePath, username, keyFile string) *SFTPProvider {
|
|||||||
BasePath: basePath,
|
BasePath: basePath,
|
||||||
Username: username,
|
Username: username,
|
||||||
KeyFile: keyFile,
|
KeyFile: keyFile,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// kopia repository connect sftp \
|
|
||||||
// --path="/home/gevo/kopiatemp" \
|
|
||||||
// --host="maric.ro" \
|
|
||||||
// --username="gevo" \
|
|
||||||
// --keyfile="$(realpath ~/.ssh/kopia_key)" \
|
|
||||||
// --known-hosts="$(realpath ~/.ssh/known_hosts)"
|
|
||||||
func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password string, configPath string) error {
|
||||||
// Just use the path on the remote server, not the full URI
|
|
||||||
repoPath := fmt.Sprintf("%s/%s", p.BasePath, serviceName)
|
repoPath := fmt.Sprintf("%s/%s", p.BasePath, serviceName)
|
||||||
|
knownHostsPath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
||||||
|
|
||||||
// Try to connect to existing repository with config file
|
|
||||||
connectCmd := exec.CommandContext(
|
connectCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -47,14 +42,13 @@ func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password
|
|||||||
"--host", p.Host,
|
"--host", p.Host,
|
||||||
"--username", p.Username,
|
"--username", p.Username,
|
||||||
"--keyfile", p.KeyFile,
|
"--keyfile", p.KeyFile,
|
||||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
"--known-hosts", knownHostsPath,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
err := connectCmd.Run()
|
err := connectCmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Connection failed, create new repository
|
p.logger.Info("Creating new SFTP repository for %s at %s", serviceName, repoPath)
|
||||||
log.Printf("Creating new SFTP repository for %s at %s", serviceName, repoPath)
|
|
||||||
createCmd := exec.CommandContext(
|
createCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
"kopia",
|
"kopia",
|
||||||
@ -64,13 +58,13 @@ func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password
|
|||||||
"--host", p.Host,
|
"--host", p.Host,
|
||||||
"--username", p.Username,
|
"--username", p.Username,
|
||||||
"--keyfile", p.KeyFile,
|
"--keyfile", p.KeyFile,
|
||||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
"--known-hosts", knownHostsPath,
|
||||||
"--password", password,
|
"--password", password,
|
||||||
)
|
)
|
||||||
|
|
||||||
createOutput, err := createCmd.CombinedOutput()
|
createOutput, err := createCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create repository: %w\nOutput: %s", err, createOutput)
|
return NewKopiaError(fmt.Sprintf("failed to create SFTP repository: %s", string(createOutput)), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,11 +74,13 @@ func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password
|
|||||||
// GetRepositoryParams returns parameters for SFTP operations
|
// GetRepositoryParams returns parameters for SFTP operations
|
||||||
func (p *SFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
func (p *SFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||||
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
repoPath := fmt.Sprintf("%s@%s:%s/%s", p.Username, p.Host, p.BasePath, serviceName)
|
||||||
|
knownHostsPath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
||||||
|
|
||||||
return []string{
|
return []string{
|
||||||
"sftp",
|
"sftp",
|
||||||
"--path", repoPath,
|
"--path", repoPath,
|
||||||
"--keyfile", p.KeyFile,
|
"--keyfile", p.KeyFile,
|
||||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
"--known-hosts", knownHostsPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -16,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"backea/internal/backup/models"
|
"backea/internal/backup/models"
|
||||||
"backea/internal/backup/security"
|
"backea/internal/backup/security"
|
||||||
|
"backea/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strategy implements the backup strategy using Kopia
|
// Strategy implements the backup strategy using Kopia
|
||||||
@ -24,6 +24,7 @@ type Strategy struct {
|
|||||||
Provider Provider
|
Provider Provider
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
SourcePath string
|
SourcePath string
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retention represents retention policy for backups
|
// Retention represents retention policy for backups
|
||||||
@ -43,41 +44,42 @@ func NewStrategy(retention Retention, provider Provider, configPath string, sour
|
|||||||
Provider: provider,
|
Provider: provider,
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
SourcePath: sourcePath,
|
SourcePath: sourcePath,
|
||||||
|
logger: logging.GetLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute performs the kopia backup
|
// Execute performs the kopia backup
|
||||||
func (s *Strategy) 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)
|
s.logger.Info("Performing kopia backup for service %s: %s", serviceName, directory)
|
||||||
|
|
||||||
// Get or create password for this service
|
// Get or create password for this service
|
||||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get or create password: %w", err)
|
return NewKopiaError("failed to get or create password", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure Kopia config directory exists
|
// Ensure Kopia config directory exists
|
||||||
kopiaConfigDir := filepath.Join(os.Getenv("HOME"), ".kopia")
|
kopiaConfigDir := filepath.Join(os.Getenv("HOME"), ".kopia")
|
||||||
if err := os.MkdirAll(kopiaConfigDir, 0755); err != nil {
|
if err := os.MkdirAll(kopiaConfigDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create kopia config directory: %w", err)
|
return NewKopiaError("failed to create kopia config directory", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository not connected, connect or create via provider
|
// Repository not connected, connect or create via provider
|
||||||
log.Printf("Connecting to repository for %s", serviceName)
|
s.logger.Info("Connecting to repository for %s", serviceName)
|
||||||
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
||||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
return NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create snapshot
|
// Create snapshot
|
||||||
log.Printf("Creating snapshot for directory: %s", directory)
|
s.logger.Info("Creating snapshot for directory: %s", directory)
|
||||||
snapshotCmd := exec.Command("kopia", "--config-file", s.ConfigPath, "snapshot", "create", directory)
|
snapshotCmd := exec.Command("kopia", "--config-file", s.ConfigPath, "snapshot", "create", directory)
|
||||||
snapshotOutput, err := snapshotCmd.CombinedOutput()
|
snapshotOutput, err := snapshotCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create snapshot: %w\nOutput: %s", err, snapshotOutput)
|
return NewKopiaError(fmt.Sprintf("failed to create snapshot: %s", string(snapshotOutput)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set retention policy
|
// Set retention policy
|
||||||
log.Printf("Setting retention policy for %s", serviceName)
|
s.logger.Info("Setting retention policy for %s", serviceName)
|
||||||
args := []string{
|
args := []string{
|
||||||
"--config-file", s.ConfigPath,
|
"--config-file", s.ConfigPath,
|
||||||
"policy", "set",
|
"policy", "set",
|
||||||
@ -92,10 +94,10 @@ func (s *Strategy) Execute(ctx context.Context, serviceName, directory string) e
|
|||||||
policyCmd := exec.Command("kopia", args...)
|
policyCmd := exec.Command("kopia", args...)
|
||||||
policyOutput, err := policyCmd.CombinedOutput()
|
policyOutput, err := policyCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set policy: %w\nOutput: %s", err, policyOutput)
|
return NewKopiaError(fmt.Sprintf("failed to set policy: %s", string(policyOutput)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Snapshot and policy set successfully for %s", serviceName)
|
s.logger.Info("Snapshot and policy set successfully for %s", serviceName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,13 +106,13 @@ func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]model
|
|||||||
// Parse service group and index from service name
|
// Parse service group and index from service name
|
||||||
_, _, err := parseServiceName(serviceName)
|
_, _, err := parseServiceName(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid service name format: %w", err)
|
return nil, NewKopiaError("invalid service name format", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the source path from the Strategy instead of trying to get it from the factory
|
// Use the source path from the Strategy instead of trying to get it from the factory
|
||||||
directoryPath := s.SourcePath
|
directoryPath := s.SourcePath
|
||||||
if directoryPath == "" {
|
if directoryPath == "" {
|
||||||
return nil, fmt.Errorf("source path not specified")
|
return nil, NewKopiaError("source path not specified", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim trailing slash if any
|
// Trim trailing slash if any
|
||||||
@ -119,7 +121,7 @@ func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]model
|
|||||||
// Ensure we're connected to the repository
|
// Ensure we're connected to the repository
|
||||||
err = s.EnsureRepositoryConnected(ctx, serviceName)
|
err = s.EnsureRepositoryConnected(ctx, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to repository: %w", err)
|
return nil, NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run kopia snapshot list command with JSON output
|
// Run kopia snapshot list command with JSON output
|
||||||
@ -134,13 +136,13 @@ func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]model
|
|||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list snapshots: %w", err)
|
return nil, NewKopiaError("failed to list snapshots", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the JSON output
|
// Parse the JSON output
|
||||||
var snapshots []map[string]interface{}
|
var snapshots []map[string]interface{}
|
||||||
if err := json.Unmarshal(output, &snapshots); err != nil {
|
if err := json.Unmarshal(output, &snapshots); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse snapshot list: %w", err)
|
return nil, NewKopiaError("failed to parse snapshot list", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to BackupInfo
|
// Convert to BackupInfo
|
||||||
@ -209,7 +211,7 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
|||||||
// Ensure we're connected to the repository
|
// Ensure we're connected to the repository
|
||||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to repository: %w", err)
|
return nil, NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provider type (b2, local, sftp)
|
// Get provider type (b2, local, sftp)
|
||||||
@ -245,7 +247,7 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// Parse the text output
|
// Parse the text output
|
||||||
outputStr := string(blobStatsOutput)
|
outputStr := string(blobStatsOutput)
|
||||||
log.Printf("Blob stats output: %s", outputStr)
|
s.logger.Debug("Blob stats output: %s", outputStr)
|
||||||
|
|
||||||
// Look for the line with "Total:"
|
// Look for the line with "Total:"
|
||||||
lines := strings.Split(outputStr, "\n")
|
lines := strings.Split(outputStr, "\n")
|
||||||
@ -258,42 +260,42 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
|||||||
size, err := parseHumanSize(sizeStr)
|
size, err := parseHumanSize(sizeStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
info.TotalBytes = size
|
info.TotalBytes = size
|
||||||
log.Printf("Got physical size from blob stats: %d bytes", size)
|
s.logger.Debug("Got physical size from blob stats: %d bytes", size)
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Failed to parse size '%s': %v", sizeStr, err)
|
s.logger.Warn("Failed to parse size '%s': %v", sizeStr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Blob stats command failed: %v - %s", err, string(blobStatsOutput))
|
s.logger.Warn("Blob stats command failed: %v - %s", err, string(blobStatsOutput))
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreBackup restores a backup with the given ID
|
// RestoreBackup restores a backup by ID
|
||||||
func (s *Strategy) 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
|
// Ensure repository is connected
|
||||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
return NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get source directory from strategy instead of importing factory
|
// Get source directory from strategy instead of importing factory
|
||||||
targetDir := s.SourcePath
|
targetDir := s.SourcePath
|
||||||
if targetDir == "" {
|
if targetDir == "" {
|
||||||
return fmt.Errorf("source path not specified")
|
return NewKopiaError("source path not specified", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary directory for restore
|
// Create a temporary directory for restore
|
||||||
restoreDir := filepath.Join(os.TempDir(), fmt.Sprintf("backea-restore-%s-%d", serviceName, time.Now().Unix()))
|
restoreDir := filepath.Join(os.TempDir(), fmt.Sprintf("backea-restore-%s-%d", serviceName, time.Now().Unix()))
|
||||||
if err := os.MkdirAll(restoreDir, 0755); err != nil {
|
if err := os.MkdirAll(restoreDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create restore directory: %w", err)
|
return NewKopiaError("failed to create restore directory", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Restoring backup %s to temporary directory %s", backupID, restoreDir)
|
s.logger.Info("Restoring backup %s to temporary directory %s", backupID, restoreDir)
|
||||||
|
|
||||||
// Run kopia restore command to restore the snapshot to the temporary directory
|
// Run kopia restore command to restore the snapshot to the temporary directory
|
||||||
restoreCmd := exec.CommandContext(
|
restoreCmd := exec.CommandContext(
|
||||||
@ -308,11 +310,11 @@ func (s *Strategy) RestoreBackup(ctx context.Context, backupID string, serviceNa
|
|||||||
|
|
||||||
restoreOutput, err := restoreCmd.CombinedOutput()
|
restoreOutput, err := restoreCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to restore snapshot: %w\nOutput: %s", err, restoreOutput)
|
return NewKopiaError(fmt.Sprintf("failed to restore snapshot: %s", string(restoreOutput)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we need to sync the restored data to the original directory
|
// Now we need to sync the restored data to the original directory
|
||||||
log.Printf("Syncing restored data from %s to %s", restoreDir, targetDir)
|
s.logger.Info("Syncing restored data from %s to %s", restoreDir, targetDir)
|
||||||
|
|
||||||
// Use rsync for the final transfer to avoid permissions issues
|
// Use rsync for the final transfer to avoid permissions issues
|
||||||
syncCmd := exec.CommandContext(
|
syncCmd := exec.CommandContext(
|
||||||
@ -326,17 +328,17 @@ func (s *Strategy) RestoreBackup(ctx context.Context, backupID string, serviceNa
|
|||||||
|
|
||||||
syncOutput, err := syncCmd.CombinedOutput()
|
syncOutput, err := syncCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to sync restored data: %w\nOutput: %s", err, syncOutput)
|
return NewKopiaError(fmt.Sprintf("failed to sync restored data: %s", string(syncOutput)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the temporary directory
|
// Clean up the temporary directory
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(5 * time.Minute) // Wait 5 minutes before cleaning up
|
time.Sleep(5 * time.Minute) // Wait 5 minutes before cleaning up
|
||||||
log.Printf("Cleaning up temporary restore directory %s", restoreDir)
|
s.logger.Info("Cleaning up temporary restore directory %s", restoreDir)
|
||||||
os.RemoveAll(restoreDir)
|
os.RemoveAll(restoreDir)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("Successfully restored backup %s to %s", backupID, targetDir)
|
s.logger.Info("Successfully restored backup %s to %s", backupID, targetDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,20 +347,19 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
// Ensure repository is connected
|
// Ensure repository is connected
|
||||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to repository: %w", err)
|
return nil, NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a single temporary directory for restore and ZIP creation
|
// Create a single temporary directory for restore and ZIP creation
|
||||||
// Note: We're not creating a nested "restore" subdirectory anymore
|
|
||||||
tempDir, err := os.MkdirTemp("", "backea-download-*")
|
tempDir, err := os.MkdirTemp("", "backea-download-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create temporary directory: %w", err)
|
return nil, NewKopiaError("failed to create temporary directory", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zipFile := filepath.Join(tempDir, "backup.zip")
|
zipFile := filepath.Join(tempDir, "backup.zip")
|
||||||
|
|
||||||
// Restore the snapshot directly to the temporary directory
|
// Restore the snapshot directly to the temporary directory
|
||||||
log.Printf("Restoring snapshot %s to temporary directory %s", backupID, tempDir)
|
s.logger.Info("Restoring snapshot %s to temporary directory %s", backupID, tempDir)
|
||||||
|
|
||||||
restoreCmd := exec.CommandContext(
|
restoreCmd := exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
@ -367,23 +368,23 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
"snapshot",
|
"snapshot",
|
||||||
"restore",
|
"restore",
|
||||||
backupID,
|
backupID,
|
||||||
tempDir, // Restore directly to tempDir instead of a subdirectory
|
tempDir,
|
||||||
)
|
)
|
||||||
|
|
||||||
restoreOutput, err := restoreCmd.CombinedOutput()
|
restoreOutput, err := restoreCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(tempDir)
|
os.RemoveAll(tempDir)
|
||||||
return nil, fmt.Errorf("failed to restore snapshot: %w\nOutput: %s", err, restoreOutput)
|
return nil, NewKopiaError(fmt.Sprintf("failed to restore snapshot: %s", string(restoreOutput)), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ZIP archive of the restored files
|
// Create ZIP archive of the restored files
|
||||||
log.Printf("Creating ZIP archive at %s", zipFile)
|
s.logger.Info("Creating ZIP archive at %s", zipFile)
|
||||||
|
|
||||||
// Use Go's zip package instead of command line tools
|
// Use Go's zip package instead of command line tools
|
||||||
zipWriter, err := os.Create(zipFile)
|
zipWriter, err := os.Create(zipFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(tempDir)
|
os.RemoveAll(tempDir)
|
||||||
return nil, fmt.Errorf("failed to create zip file: %w", err)
|
return nil, NewKopiaError("failed to create zip file", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
@ -396,7 +397,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||||
// Handle errors accessing files/directories
|
// Handle errors accessing files/directories
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Error accessing path %s: %v", path, err)
|
s.logger.Warn("Error accessing path %s: %v", path, err)
|
||||||
return nil // Skip this file but continue walking
|
return nil // Skip this file but continue walking
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,14 +414,14 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
// Create ZIP header
|
// Create ZIP header
|
||||||
header, err := zip.FileInfoHeader(info)
|
header, err := zip.FileInfoHeader(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Couldn't create header for %s: %v", path, err)
|
s.logger.Warn("Couldn't create header for %s: %v", path, err)
|
||||||
return nil // Skip this file but continue walking
|
return nil // Skip this file but continue walking
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the path relative to the temp directory
|
// Make the path relative to the temp directory
|
||||||
relPath, err := filepath.Rel(tempDir, path)
|
relPath, err := filepath.Rel(tempDir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Couldn't get relative path for %s: %v", path, err)
|
s.logger.Warn("Couldn't get relative path for %s: %v", path, err)
|
||||||
return nil // Skip this file but continue walking
|
return nil // Skip this file but continue walking
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,14 +431,14 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
// Create the file in the ZIP
|
// Create the file in the ZIP
|
||||||
writer, err := archive.CreateHeader(header)
|
writer, err := archive.CreateHeader(header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Couldn't create zip entry for %s: %v", path, err)
|
s.logger.Warn("Couldn't create zip entry for %s: %v", path, err)
|
||||||
return nil // Skip this file but continue walking
|
return nil // Skip this file but continue walking
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the file
|
// Open the file
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Couldn't open file %s: %v", path, err)
|
s.logger.Warn("Couldn't open file %s: %v", path, err)
|
||||||
return nil // Skip this file but continue walking
|
return nil // Skip this file but continue walking
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
@ -445,7 +446,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
// Copy the file content to the ZIP
|
// Copy the file content to the ZIP
|
||||||
_, err = io.Copy(writer, file)
|
_, err = io.Copy(writer, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Error copying content from %s: %v", path, err)
|
s.logger.Warn("Error copying content from %s: %v", path, err)
|
||||||
}
|
}
|
||||||
return nil // Continue to next file regardless of error
|
return nil // Continue to next file regardless of error
|
||||||
})
|
})
|
||||||
@ -453,7 +454,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
// Even if we had some file errors, don't fail the whole process
|
// Even if we had some file errors, don't fail the whole process
|
||||||
// as long as we created the zip file
|
// as long as we created the zip file
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Some files may have been skipped during zip creation: %v", err)
|
s.logger.Warn("Some files may have been skipped during zip creation: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the ZIP writer before opening it for reading
|
// Close the ZIP writer before opening it for reading
|
||||||
@ -464,7 +465,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
zipReader, err := os.Open(zipFile)
|
zipReader, err := os.Open(zipFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(tempDir)
|
os.RemoveAll(tempDir)
|
||||||
return nil, fmt.Errorf("failed to open zip archive: %w", err)
|
return nil, NewKopiaError("failed to open zip archive", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a reader that will clean up when closed
|
// Return a reader that will clean up when closed
|
||||||
@ -481,7 +482,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
|||||||
func (s *Strategy) GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error) {
|
func (s *Strategy) GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error) {
|
||||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to repository: %w", err)
|
return nil, NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run kopia snapshot describe command with JSON output
|
// Run kopia snapshot describe command with JSON output
|
||||||
@ -497,13 +498,13 @@ func (s *Strategy) GetBackupInfo(ctx context.Context, backupID string, serviceNa
|
|||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to describe snapshot: %w", err)
|
return nil, NewKopiaError("failed to describe snapshot", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the JSON output
|
// Parse the JSON output
|
||||||
var snap map[string]interface{}
|
var snap map[string]interface{}
|
||||||
if err := json.Unmarshal(output, &snap); err != nil {
|
if err := json.Unmarshal(output, &snap); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse snapshot info: %w", err)
|
return nil, NewKopiaError("failed to parse snapshot info", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the relevant information
|
// Extract the relevant information
|
||||||
@ -566,12 +567,12 @@ func (s *Strategy) EnsureRepositoryConnected(ctx context.Context, serviceName st
|
|||||||
// Repository not connected, try to connect
|
// Repository not connected, try to connect
|
||||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get password: %w", err)
|
return NewKopiaError("failed to get password", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect using provider
|
// Connect using provider
|
||||||
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
if err := s.Provider.Connect(ctx, serviceName, password, s.ConfigPath); err != nil {
|
||||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
return NewKopiaError("failed to connect to repository", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,7 +583,7 @@ func (s *Strategy) EnsureRepositoryConnected(ctx context.Context, serviceName st
|
|||||||
func parseServiceName(serviceName string) (string, string, error) {
|
func parseServiceName(serviceName string) (string, string, error) {
|
||||||
parts := strings.SplitN(serviceName, ".", 2)
|
parts := strings.SplitN(serviceName, ".", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return "", "", fmt.Errorf("service name must be in format 'group.index', got '%s'", serviceName)
|
return "", "", NewKopiaError(fmt.Sprintf("service name must be in format 'group.index', got '%s'", serviceName), nil)
|
||||||
}
|
}
|
||||||
return parts[0], parts[1], nil
|
return parts[0], parts[1], nil
|
||||||
}
|
}
|
||||||
@ -591,12 +592,12 @@ func parseServiceName(serviceName string) (string, string, error) {
|
|||||||
func parseHumanSize(sizeStr string) (int64, error) {
|
func parseHumanSize(sizeStr string) (int64, error) {
|
||||||
parts := strings.Fields(sizeStr)
|
parts := strings.Fields(sizeStr)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return 0, fmt.Errorf("invalid size format: %s", sizeStr)
|
return 0, NewKopiaError(fmt.Sprintf("invalid size format: %s", sizeStr), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := strconv.ParseFloat(parts[0], 64)
|
value, err := strconv.ParseFloat(parts[0], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("invalid size value: %w", err)
|
return 0, NewKopiaError("invalid size value", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unit := strings.ToUpper(parts[1])
|
unit := strings.ToUpper(parts[1])
|
||||||
@ -612,7 +613,7 @@ func parseHumanSize(sizeStr string) (int64, error) {
|
|||||||
case "TB", "TIB":
|
case "TB", "TIB":
|
||||||
return int64(value * 1024 * 1024 * 1024 * 1024), nil
|
return int64(value * 1024 * 1024 * 1024 * 1024), nil
|
||||||
default:
|
default:
|
||||||
return 0, fmt.Errorf("unknown size unit: %s", unit)
|
return 0, NewKopiaError(fmt.Sprintf("unknown size unit: %s", unit), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,20 +26,20 @@ type StorageUsageInfo struct {
|
|||||||
|
|
||||||
// Strategy defines the backup/restore strategy interface
|
// Strategy defines the backup/restore strategy interface
|
||||||
type Strategy interface {
|
type Strategy interface {
|
||||||
// Execute performs a backup
|
// Execute performs the backup operation
|
||||||
Execute(ctx context.Context, serviceName, directory string) error
|
Execute(ctx context.Context, serviceName, directory string) error
|
||||||
|
|
||||||
// ListBackups gets information about existing backups
|
// ListBackups returns information about existing backups
|
||||||
ListBackups(ctx context.Context, serviceName string) ([]models.BackupInfo, error)
|
ListBackups(ctx context.Context, serviceName string) ([]models.BackupInfo, error)
|
||||||
|
|
||||||
// GetStorageUsage returns information about the total storage used
|
// GetStorageUsage returns information about storage usage
|
||||||
GetStorageUsage(ctx context.Context, serviceName string) (*models.StorageUsageInfo, error)
|
GetStorageUsage(ctx context.Context, serviceName string) (*models.StorageUsageInfo, error)
|
||||||
|
|
||||||
// RestoreBackup restores a backup to its original location
|
// RestoreBackup restores a backup by ID
|
||||||
RestoreBackup(ctx context.Context, backupID string, serviceName string) error
|
RestoreBackup(ctx context.Context, backupID string, serviceName string) error
|
||||||
|
|
||||||
// DownloadBackup provides a reader with backup data for download
|
// DownloadBackup provides a reader with backup summary information
|
||||||
DownloadBackup(ctx context.Context, backupID, serviceName string) (io.ReadCloser, error)
|
DownloadBackup(ctx context.Context, backupID string, serviceName string) (io.ReadCloser, error)
|
||||||
|
|
||||||
// GetBackupInfo returns detailed information about a specific backup
|
// GetBackupInfo returns detailed information about a specific backup
|
||||||
GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error)
|
GetBackupInfo(ctx context.Context, backupID string, serviceName string) (*models.BackupInfo, error)
|
||||||
@ -75,11 +75,3 @@ type RetentionConfig struct {
|
|||||||
KeepMonthly int // Number of monthly snapshots to keep
|
KeepMonthly int // Number of monthly snapshots to keep
|
||||||
KeepYearly int // Number of yearly 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,6 +1,7 @@
|
|||||||
package b2_client
|
package b2_client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backea/internal/logging"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@ -9,24 +10,50 @@ import (
|
|||||||
"gopkg.in/kothar/go-backblaze.v0"
|
"gopkg.in/kothar/go-backblaze.v0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// B2Error represents an error in B2 client operations
|
||||||
|
type B2Error struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *B2Error) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return e.Message + ": " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *B2Error) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewB2Error(message string, cause error) *B2Error {
|
||||||
|
return &B2Error{
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Client represents a B2 client
|
// Client represents a B2 client
|
||||||
type Client struct {
|
type Client struct {
|
||||||
b2 *backblaze.B2
|
b2 *backblaze.B2
|
||||||
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientFromEnv creates a new B2 client using credentials from .env
|
// NewClientFromEnv creates a new B2 client using credentials from .env
|
||||||
func NewClientFromEnv() (*Client, error) {
|
func NewClientFromEnv() (*Client, error) {
|
||||||
|
logger := logging.GetLogger()
|
||||||
|
|
||||||
// Load .env file
|
// Load .env file
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
return nil, fmt.Errorf("error loading .env file: %w", err)
|
return nil, NewB2Error("error loading .env file", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get credentials from environment
|
// Get credentials from environment
|
||||||
keyID := os.Getenv("B2_KEY_ID")
|
keyID := os.Getenv("B2_KEY_ID")
|
||||||
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
applicationKey := os.Getenv("B2_APPLICATION_KEY")
|
||||||
|
|
||||||
if keyID == "" || applicationKey == "" {
|
if keyID == "" || applicationKey == "" {
|
||||||
return nil, fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file")
|
return nil, NewB2Error("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create B2 client
|
// Create B2 client
|
||||||
@ -34,13 +61,13 @@ func NewClientFromEnv() (*Client, error) {
|
|||||||
AccountID: keyID,
|
AccountID: keyID,
|
||||||
ApplicationKey: applicationKey,
|
ApplicationKey: applicationKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating B2 client: %w", err)
|
return nil, NewB2Error("error creating B2 client", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
b2: b2,
|
b2: b2,
|
||||||
|
logger: logger,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,9 +76,8 @@ func (c *Client) ListBuckets(ctx context.Context) ([]*backblaze.Bucket, error) {
|
|||||||
// List all buckets
|
// List all buckets
|
||||||
buckets, err := c.b2.ListBuckets()
|
buckets, err := c.b2.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error listing buckets: %w", err)
|
return nil, NewB2Error("error listing buckets", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buckets, nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +86,7 @@ func (c *Client) GetBucket(ctx context.Context, name string) (*backblaze.Bucket,
|
|||||||
// List all buckets
|
// List all buckets
|
||||||
buckets, err := c.b2.ListBuckets()
|
buckets, err := c.b2.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error listing buckets: %w", err)
|
return nil, NewB2Error("error listing buckets", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find bucket by name
|
// Find bucket by name
|
||||||
@ -70,7 +96,7 @@ func (c *Client) GetBucket(ctx context.Context, name string) (*backblaze.Bucket,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("bucket not found: %s", name)
|
return nil, NewB2Error(fmt.Sprintf("bucket not found: %s", name), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBucket creates a new bucket
|
// CreateBucket creates a new bucket
|
||||||
@ -84,8 +110,9 @@ func (c *Client) CreateBucket(ctx context.Context, name string, public bool) (*b
|
|||||||
// Create bucket
|
// Create bucket
|
||||||
bucket, err := c.b2.CreateBucket(name, bucketType)
|
bucket, err := c.b2.CreateBucket(name, bucketType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating bucket: %w", err)
|
return nil, NewB2Error(fmt.Sprintf("error creating bucket '%s'", name), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.logger.Info("Created B2 bucket: %s", name)
|
||||||
return bucket, nil
|
return bucket, nil
|
||||||
}
|
}
|
||||||
|
147
internal/logging/logging.go
Normal file
147
internal/logging/logging.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// internal/logging/logging.go
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Global logger instance
|
||||||
|
logger *Logger
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogLevel represents the severity of a log message
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEBUG LogLevel = iota
|
||||||
|
INFO
|
||||||
|
WARN
|
||||||
|
ERROR
|
||||||
|
FATAL
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger provides logging functionality
|
||||||
|
type Logger struct {
|
||||||
|
logLevel LogLevel
|
||||||
|
output io.Writer
|
||||||
|
stdLog *log.Logger
|
||||||
|
errLog *log.Logger
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
func InitLogger(level LogLevel, logFile string) {
|
||||||
|
once.Do(func() {
|
||||||
|
var output io.Writer = os.Stdout
|
||||||
|
|
||||||
|
if logFile != "" {
|
||||||
|
logDir := filepath.Dir(logFile)
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create log directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to open log file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = io.MultiWriter(os.Stdout, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = &Logger{
|
||||||
|
logLevel: level,
|
||||||
|
output: output,
|
||||||
|
stdLog: log.New(output, "", log.LstdFlags),
|
||||||
|
errLog: log.New(output, "ERROR: ", log.LstdFlags),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
func GetLogger() *Logger {
|
||||||
|
if logger == nil {
|
||||||
|
InitLogger(INFO, "")
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) SetLogLevel(level LogLevel) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
l.logLevel = level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(level LogLevel, format string, v ...interface{}) {
|
||||||
|
if level < l.logLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
// Get caller information
|
||||||
|
_, file, line, ok := runtime.Caller(2)
|
||||||
|
if !ok {
|
||||||
|
file = "unknown"
|
||||||
|
line = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format message with source location
|
||||||
|
message := fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, fmt.Sprintf(format, v...))
|
||||||
|
|
||||||
|
if level >= ERROR {
|
||||||
|
l.errLog.Println(message)
|
||||||
|
} else {
|
||||||
|
l.stdLog.Println(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a debug message
|
||||||
|
func (l *Logger) Debug(format string, v ...interface{}) {
|
||||||
|
l.log(DEBUG, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs an info message
|
||||||
|
func (l *Logger) Info(format string, v ...interface{}) {
|
||||||
|
l.log(INFO, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a warning message
|
||||||
|
func (l *Logger) Warn(format string, v ...interface{}) {
|
||||||
|
l.log(WARN, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error message
|
||||||
|
func (l *Logger) Error(format string, v ...interface{}) {
|
||||||
|
l.log(ERROR, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a fatal message and exits the program
|
||||||
|
func (l *Logger) Fatal(format string, v ...interface{}) {
|
||||||
|
l.log(FATAL, format, v...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWithStack logs an error with its stack trace
|
||||||
|
func (l *Logger) ErrorWithStack(err error, format string, v ...interface{}) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf(format, v...)
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n := runtime.Stack(buf, false)
|
||||||
|
|
||||||
|
l.Error("%s: %v\n%s", message, err, buf[:n])
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user