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 (
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/logging"
|
||||
"backea/internal/mail"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@ -16,6 +15,7 @@ type Executor struct {
|
||||
Factory strategy.Factory
|
||||
Mailer *mail.Mailer
|
||||
concurrency int
|
||||
Logger logging.Logger
|
||||
}
|
||||
|
||||
// NewExecutor creates a new backup executor
|
||||
@ -25,6 +25,7 @@ func NewExecutor(config *models.Configuration, factory strategy.Factory) *Execut
|
||||
Factory: factory,
|
||||
// Mailer: mailer,
|
||||
concurrency: 5,
|
||||
Logger: *logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,8 +70,8 @@ func (e *Executor) processAllServiceGroups(ctx context.Context) error {
|
||||
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)
|
||||
e.Logger.Error("Failed to backup service group %s: %v", group, err)
|
||||
errs <- NewBackupError("backup failed for group "+group, err)
|
||||
}
|
||||
}(groupName)
|
||||
}
|
||||
@ -92,8 +93,8 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
||||
// 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)
|
||||
e.Logger.Error("Service group not found: %s", groupName)
|
||||
return NewBackupError("service group not found: "+groupName, nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
e.Logger.Error("Failed to execute before hook for group %s: %v", groupName, err)
|
||||
return NewBackupError("before hook failed for group "+groupName, err)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
e.Logger.Error("Failed to backup service %s.%s: %v", group, index, err)
|
||||
errs <- NewBackupError("backup failed for "+group+"."+index, err)
|
||||
}
|
||||
}(groupName, configIndex)
|
||||
}
|
||||
@ -130,17 +131,21 @@ func (e *Executor) processServiceGroup(ctx context.Context, groupName string) er
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -149,28 +154,33 @@ func (e *Executor) processSpecificService(ctx context.Context, groupName string,
|
||||
// Get service configuration
|
||||
serviceGroup, exists := e.Config.Services[groupName]
|
||||
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
|
||||
_, exists = serviceGroup.BackupConfigs[configIndex]
|
||||
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
|
||||
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)
|
||||
e.Logger.Error("Failed to create backup strategy for service %s.%s: %v", groupName, configIndex, err)
|
||||
return NewBackupError("failed to create backup strategy for "+groupName+"."+configIndex, err)
|
||||
}
|
||||
|
||||
// Create and run service
|
||||
service := NewService(
|
||||
fmt.Sprintf("%s.%s", groupName, configIndex),
|
||||
groupName+"."+configIndex,
|
||||
serviceGroup.Source.Path,
|
||||
backupStrategy,
|
||||
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
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"backea/internal/logging"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// HookRunner runs hooks before and after backups
|
||||
type HookRunner struct {
|
||||
Directory string
|
||||
BeforeHook string
|
||||
AfterHook string
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewHookRunner creates a new hook runner
|
||||
func NewHookRunner(directory, beforeHook, afterHook string) *HookRunner {
|
||||
return &HookRunner{
|
||||
Directory: directory,
|
||||
BeforeHook: beforeHook,
|
||||
AfterHook: afterHook,
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
h.logger.Info("Running before hook: %s", h.BeforeHook)
|
||||
err := 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 {
|
||||
if h.AfterHook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Running after hook: %s", h.AfterHook)
|
||||
return h.runCommand(h.AfterHook)
|
||||
h.logger.Info("Running after hook: %s", h.AfterHook)
|
||||
err := 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 {
|
||||
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()
|
||||
h.logger.Info("Executing command in %s: %s", h.Directory, command)
|
||||
|
||||
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 (
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/strategy"
|
||||
"backea/internal/logging"
|
||||
"backea/internal/mail"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service represents a service to be backed up
|
||||
type Service struct {
|
||||
Name string
|
||||
Directory string
|
||||
Strategy strategy.Strategy
|
||||
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 {
|
||||
return &Service{
|
||||
Name: name,
|
||||
Directory: directory,
|
||||
Strategy: strategy,
|
||||
Mailer: mailer,
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Backup performs the backup for this service
|
||||
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()
|
||||
|
||||
// Ensure directory exists
|
||||
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 s.Mailer != nil {
|
||||
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)
|
||||
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 {
|
||||
s.Mailer.SendSuccessNotification(s.Name, duration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBackupInfos retrieves information about existing backups
|
||||
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) {
|
||||
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 {
|
||||
log.Printf("Restoring backup %s for service %s", backupID, s.Name)
|
||||
return s.Strategy.RestoreBackup(ctx, backupID, s.Name)
|
||||
s.logger.Info("Restoring backup %s for service %s", 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
|
||||
|
||||
import (
|
||||
"log"
|
||||
"backea/internal/logging"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
@ -9,10 +9,17 @@ import (
|
||||
|
||||
// Configuration holds the complete application configuration
|
||||
type Configuration struct {
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Defaults DefaultsConfig `mapstructure:"defaults"`
|
||||
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
|
||||
type DefaultsConfig struct {
|
||||
Retention RetentionConfig `mapstructure:"retention"`
|
||||
@ -71,27 +78,61 @@ type DestConfig struct {
|
||||
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
|
||||
func LoadConfig(configPath string) (*Configuration, error) {
|
||||
// Create a logger first with default settings
|
||||
logger := logging.GetLogger()
|
||||
|
||||
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)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
logger.Error("Failed to read config file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Configuration
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
logger.Error("Failed to unmarshal config: %v", 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
|
||||
for serviceName, service := range config.Services {
|
||||
for configID, backupConfig := range service.BackupConfigs {
|
||||
// If retention is not set, use defaults
|
||||
if isRetentionEmpty(backupConfig.BackupStrategy.Retention) {
|
||||
logger.Debug("Applying default retention settings for service %s, config %s",
|
||||
serviceName, configID)
|
||||
backupConfig.BackupStrategy.Retention = config.Defaults.Retention
|
||||
service.BackupConfigs[configID] = backupConfig
|
||||
}
|
||||
|
@ -1,28 +1,48 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"backea/internal/logging"
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"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) {
|
||||
// Define the expected key in the env file
|
||||
// Replace dots with underscores for environment variable name
|
||||
logger := logging.GetLogger()
|
||||
|
||||
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)
|
||||
return "", NewSecurityError("failed to open kopia.env", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@ -32,7 +52,6 @@ func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
||||
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
|
||||
@ -41,71 +60,56 @@ func GetOrCreatePassword(serviceName string, length int) (string, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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()
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
return "", NewSecurityError("failed to generate random bytes", 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
|
||||
result[0] = 'A' + byte(randomBytes[0]%26)
|
||||
result[1] = 'A' + byte(randomBytes[1]%26)
|
||||
result[2] = 'a' + byte(randomBytes[2]%26)
|
||||
result[3] = 'a' + byte(randomBytes[3]%26)
|
||||
result[4] = '0' + byte(randomBytes[4]%10)
|
||||
result[5] = '0' + byte(randomBytes[5]%10)
|
||||
|
||||
// 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)]
|
||||
}
|
||||
|
@ -3,25 +3,55 @@ package strategy
|
||||
import (
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/strategy/kopia"
|
||||
"backea/internal/logging"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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
|
||||
type Factory struct {
|
||||
Config *models.Configuration
|
||||
ConfigPath string
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewFactory initializes and returns a Factory
|
||||
func NewFactory(configPath string) (*Factory, error) {
|
||||
cfg, err := models.LoadConfig(configPath)
|
||||
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
|
||||
@ -30,6 +60,7 @@ func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourcePath, err := f.getServicePath(groupName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -39,7 +70,7 @@ func (f *Factory) CreateBackupStrategyForService(groupName, serviceIndex string)
|
||||
case "kopia":
|
||||
return f.createKopiaStrategy(serviceConfig, sourcePath)
|
||||
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) {
|
||||
provider, err := f.createKopiaProvider(serviceConfig.BackupStrategy)
|
||||
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)
|
||||
@ -61,16 +92,19 @@ func (f *Factory) createKopiaProvider(strategyConfig models.StrategyConfig) (kop
|
||||
switch strategyConfig.Provider {
|
||||
case "local":
|
||||
return kopia.NewLocalProvider(strategyConfig.Destination.Path), nil
|
||||
|
||||
case "b2", "backblaze":
|
||||
return kopia.NewB2Provider(), nil
|
||||
|
||||
case "sftp":
|
||||
host := strategyConfig.Destination.Host
|
||||
basepath := strategyConfig.Destination.Path
|
||||
username := strategyConfig.Destination.Username
|
||||
keyFile := strategyConfig.Destination.SSHKey
|
||||
return kopia.NewSFTPProvider(host, basepath, username, keyFile), nil
|
||||
|
||||
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 {
|
||||
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
|
||||
@ -88,15 +122,16 @@ func (f *Factory) getServiceConfig(groupName, serviceIndex string) (*models.Back
|
||||
if backupConfig, exists := serviceGroup.BackupConfigs[serviceIndex]; exists {
|
||||
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
|
||||
func (f *Factory) ReloadConfig() error {
|
||||
cfg, err := models.LoadConfig(f.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reload config: %w", err)
|
||||
return NewStrategyError("could not reload config", err)
|
||||
}
|
||||
f.Config = cfg
|
||||
return nil
|
||||
|
@ -1,9 +1,9 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"backea/internal/logging"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@ -11,42 +11,71 @@ import (
|
||||
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
|
||||
type B2Provider struct{}
|
||||
type B2Provider struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewB2Provider creates a new B2 provider
|
||||
func NewB2Provider() *B2Provider {
|
||||
return &B2Provider{}
|
||||
return &B2Provider{
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
return NewKopiaError("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||
}
|
||||
|
||||
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")
|
||||
B2Client, err := b2_client.NewClientFromEnv()
|
||||
if err != nil {
|
||||
return NewKopiaError("failed to initialize B2 client", err)
|
||||
}
|
||||
|
||||
_, err := B2Client.GetBucket(ctx, bucketName)
|
||||
if B2Client == nil {
|
||||
return NewKopiaError("B2 client not initialized", nil)
|
||||
}
|
||||
|
||||
_, err = B2Client.GetBucket(ctx, bucketName)
|
||||
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)
|
||||
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(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -57,10 +86,10 @@ func (p *B2Provider) Connect(ctx context.Context, serviceName string, password s
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
|
||||
err = connectCmd.Run()
|
||||
if err != nil {
|
||||
// Connection failed, create new repository
|
||||
log.Printf("Creating new B2 repository for %s", serviceName)
|
||||
p.logger.Info("Creating new B2 repository for %s", serviceName)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -71,9 +100,10 @@ func (p *B2Provider) Connect(ctx context.Context, serviceName string, password s
|
||||
"--key", applicationKey,
|
||||
"--password", password,
|
||||
)
|
||||
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
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")
|
||||
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")
|
||||
return nil, NewKopiaError("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||
}
|
||||
|
||||
bucketName := p.GetBucketName(serviceName)
|
||||
|
||||
return []string{
|
||||
"b2",
|
||||
"--bucket", bucketName,
|
||||
@ -100,10 +129,7 @@ func (p *B2Provider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"backea/internal/logging"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -12,29 +12,29 @@ import (
|
||||
// LocalProvider implements the Provider interface for local storage
|
||||
type LocalProvider struct {
|
||||
BasePath string
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// 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,
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
p.logger.Info("Connecting to local repository at %s with config: %s", repoPath, configPath)
|
||||
|
||||
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(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -43,10 +43,10 @@ func (p *LocalProvider) Connect(ctx context.Context, serviceName string, passwor
|
||||
"--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)
|
||||
p.logger.Info("Creating new local repository for %s at destination: %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -55,9 +55,10 @@ func (p *LocalProvider) Connect(ctx context.Context, serviceName string, passwor
|
||||
"--path", repoPath,
|
||||
"--password", password,
|
||||
)
|
||||
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
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
|
||||
|
||||
import (
|
||||
"backea/internal/logging"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -15,6 +15,7 @@ type SFTPProvider struct {
|
||||
BasePath string
|
||||
Username string
|
||||
KeyFile string
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewSFTPProvider creates a new SFTP provider
|
||||
@ -24,20 +25,14 @@ func NewSFTPProvider(host, basePath, username, keyFile string) *SFTPProvider {
|
||||
BasePath: basePath,
|
||||
Username: username,
|
||||
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 {
|
||||
// Just use the path on the remote server, not the full URI
|
||||
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(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -47,14 +42,13 @@ func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password
|
||||
"--host", p.Host,
|
||||
"--username", p.Username,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--known-hosts", knownHostsPath,
|
||||
"--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)
|
||||
p.logger.Info("Creating new SFTP repository for %s at %s", serviceName, repoPath)
|
||||
createCmd := exec.CommandContext(
|
||||
ctx,
|
||||
"kopia",
|
||||
@ -64,13 +58,13 @@ func (p *SFTPProvider) Connect(ctx context.Context, serviceName string, password
|
||||
"--host", p.Host,
|
||||
"--username", p.Username,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--known-hosts", knownHostsPath,
|
||||
"--password", password,
|
||||
)
|
||||
|
||||
createOutput, err := createCmd.CombinedOutput()
|
||||
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
|
||||
func (p *SFTPProvider) GetRepositoryParams(serviceName string) ([]string, error) {
|
||||
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{
|
||||
"sftp",
|
||||
"--path", repoPath,
|
||||
"--keyfile", p.KeyFile,
|
||||
"--known-hosts", filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"),
|
||||
"--known-hosts", knownHostsPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -16,6 +15,7 @@ import (
|
||||
|
||||
"backea/internal/backup/models"
|
||||
"backea/internal/backup/security"
|
||||
"backea/internal/logging"
|
||||
)
|
||||
|
||||
// Strategy implements the backup strategy using Kopia
|
||||
@ -24,6 +24,7 @@ type Strategy struct {
|
||||
Provider Provider
|
||||
ConfigPath string
|
||||
SourcePath string
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// Retention represents retention policy for backups
|
||||
@ -43,41 +44,42 @@ func NewStrategy(retention Retention, provider Provider, configPath string, sour
|
||||
Provider: provider,
|
||||
ConfigPath: configPath,
|
||||
SourcePath: sourcePath,
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute performs the kopia backup
|
||||
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
|
||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||
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
|
||||
kopiaConfigDir := filepath.Join(os.Getenv("HOME"), ".kopia")
|
||||
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
|
||||
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 {
|
||||
return fmt.Errorf("failed to connect to repository: %w", err)
|
||||
return NewKopiaError("failed to connect to repository", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
snapshotOutput, err := snapshotCmd.CombinedOutput()
|
||||
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
|
||||
log.Printf("Setting retention policy for %s", serviceName)
|
||||
s.logger.Info("Setting retention policy for %s", serviceName)
|
||||
args := []string{
|
||||
"--config-file", s.ConfigPath,
|
||||
"policy", "set",
|
||||
@ -92,10 +94,10 @@ func (s *Strategy) Execute(ctx context.Context, serviceName, directory string) e
|
||||
policyCmd := exec.Command("kopia", args...)
|
||||
policyOutput, err := policyCmd.CombinedOutput()
|
||||
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
|
||||
}
|
||||
|
||||
@ -104,13 +106,13 @@ func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]model
|
||||
// Parse service group and index from service name
|
||||
_, _, err := parseServiceName(serviceName)
|
||||
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
|
||||
directoryPath := s.SourcePath
|
||||
if directoryPath == "" {
|
||||
return nil, fmt.Errorf("source path not specified")
|
||||
return nil, NewKopiaError("source path not specified", nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
err = s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
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
|
||||
@ -134,13 +136,13 @@ func (s *Strategy) ListBackups(ctx context.Context, serviceName string) ([]model
|
||||
|
||||
output, err := cmd.Output()
|
||||
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
|
||||
var snapshots []map[string]interface{}
|
||||
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
|
||||
@ -209,7 +211,7 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
||||
// Ensure we're connected to the repository
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
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)
|
||||
@ -245,7 +247,7 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
||||
if err == nil {
|
||||
// Parse the text output
|
||||
outputStr := string(blobStatsOutput)
|
||||
log.Printf("Blob stats output: %s", outputStr)
|
||||
s.logger.Debug("Blob stats output: %s", outputStr)
|
||||
|
||||
// Look for the line with "Total:"
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
@ -258,42 +260,42 @@ func (s *Strategy) GetStorageUsage(ctx context.Context, serviceName string) (*mo
|
||||
size, err := parseHumanSize(sizeStr)
|
||||
if err == nil {
|
||||
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
|
||||
} else {
|
||||
log.Printf("Failed to parse size '%s': %v", sizeStr, err)
|
||||
s.logger.Warn("Failed to parse size '%s': %v", sizeStr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Ensure repository is connected
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
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
|
||||
targetDir := s.SourcePath
|
||||
if targetDir == "" {
|
||||
return fmt.Errorf("source path not specified")
|
||||
return NewKopiaError("source path not specified", nil)
|
||||
}
|
||||
|
||||
// Create a temporary directory for restore
|
||||
restoreDir := filepath.Join(os.TempDir(), fmt.Sprintf("backea-restore-%s-%d", serviceName, time.Now().Unix()))
|
||||
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
|
||||
restoreCmd := exec.CommandContext(
|
||||
@ -308,11 +310,11 @@ func (s *Strategy) RestoreBackup(ctx context.Context, backupID string, serviceNa
|
||||
|
||||
restoreOutput, err := restoreCmd.CombinedOutput()
|
||||
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
|
||||
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
|
||||
syncCmd := exec.CommandContext(
|
||||
@ -326,17 +328,17 @@ func (s *Strategy) RestoreBackup(ctx context.Context, backupID string, serviceNa
|
||||
|
||||
syncOutput, err := syncCmd.CombinedOutput()
|
||||
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
|
||||
go func() {
|
||||
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)
|
||||
}()
|
||||
|
||||
log.Printf("Successfully restored backup %s to %s", backupID, targetDir)
|
||||
s.logger.Info("Successfully restored backup %s to %s", backupID, targetDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -345,20 +347,19 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
// Ensure repository is connected
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
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
|
||||
// Note: We're not creating a nested "restore" subdirectory anymore
|
||||
tempDir, err := os.MkdirTemp("", "backea-download-*")
|
||||
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")
|
||||
|
||||
// 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(
|
||||
ctx,
|
||||
@ -367,23 +368,23 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
"snapshot",
|
||||
"restore",
|
||||
backupID,
|
||||
tempDir, // Restore directly to tempDir instead of a subdirectory
|
||||
tempDir,
|
||||
)
|
||||
|
||||
restoreOutput, err := restoreCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
zipWriter, err := os.Create(zipFile)
|
||||
if err != nil {
|
||||
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()
|
||||
@ -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 {
|
||||
// Handle errors accessing files/directories
|
||||
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
|
||||
}
|
||||
|
||||
@ -413,14 +414,14 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
// Create ZIP header
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
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
|
||||
}
|
||||
|
||||
// Make the path relative to the temp directory
|
||||
relPath, err := filepath.Rel(tempDir, path)
|
||||
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
|
||||
}
|
||||
|
||||
@ -430,14 +431,14 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
// Create the file in the ZIP
|
||||
writer, err := archive.CreateHeader(header)
|
||||
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
|
||||
}
|
||||
|
||||
// Open the file
|
||||
file, err := os.Open(path)
|
||||
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
|
||||
}
|
||||
defer file.Close()
|
||||
@ -445,7 +446,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
// Copy the file content to the ZIP
|
||||
_, err = io.Copy(writer, file)
|
||||
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
|
||||
})
|
||||
@ -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
|
||||
// as long as we created the zip file
|
||||
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
|
||||
@ -464,7 +465,7 @@ func (s *Strategy) DownloadBackup(ctx context.Context, backupID string, serviceN
|
||||
zipReader, err := os.Open(zipFile)
|
||||
if err != nil {
|
||||
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
|
||||
@ -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) {
|
||||
err := s.EnsureRepositoryConnected(ctx, serviceName)
|
||||
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
|
||||
@ -497,13 +498,13 @@ func (s *Strategy) GetBackupInfo(ctx context.Context, backupID string, serviceNa
|
||||
|
||||
output, err := cmd.Output()
|
||||
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
|
||||
var snap map[string]interface{}
|
||||
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
|
||||
@ -566,12 +567,12 @@ func (s *Strategy) EnsureRepositoryConnected(ctx context.Context, serviceName st
|
||||
// Repository not connected, try to connect
|
||||
password, err := security.GetOrCreatePassword(serviceName, 48)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get password: %w", err)
|
||||
return NewKopiaError("failed to get password", 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 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) {
|
||||
parts := strings.SplitN(serviceName, ".", 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
|
||||
}
|
||||
@ -591,12 +592,12 @@ func parseServiceName(serviceName string) (string, string, error) {
|
||||
func parseHumanSize(sizeStr string) (int64, error) {
|
||||
parts := strings.Fields(sizeStr)
|
||||
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)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid size value: %w", err)
|
||||
return 0, NewKopiaError("invalid size value", err)
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(parts[1])
|
||||
@ -612,7 +613,7 @@ func parseHumanSize(sizeStr string) (int64, error) {
|
||||
case "TB", "TIB":
|
||||
return int64(value * 1024 * 1024 * 1024 * 1024), nil
|
||||
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
|
||||
type Strategy interface {
|
||||
// Execute performs a backup
|
||||
// Execute performs the backup operation
|
||||
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)
|
||||
|
||||
// GetStorageUsage returns information about the total storage used
|
||||
// GetStorageUsage returns information about storage usage
|
||||
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
|
||||
|
||||
// DownloadBackup provides a reader with backup data for download
|
||||
DownloadBackup(ctx context.Context, backupID, serviceName string) (io.ReadCloser, error)
|
||||
// DownloadBackup provides a reader with backup summary information
|
||||
DownloadBackup(ctx context.Context, backupID string, serviceName string) (io.ReadCloser, error)
|
||||
|
||||
// GetBackupInfo returns detailed information about a specific backup
|
||||
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
|
||||
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
|
||||
|
||||
import (
|
||||
"backea/internal/logging"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -9,24 +10,50 @@ import (
|
||||
"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
|
||||
type Client struct {
|
||||
b2 *backblaze.B2
|
||||
b2 *backblaze.B2
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewClientFromEnv creates a new B2 client using credentials from .env
|
||||
func NewClientFromEnv() (*Client, error) {
|
||||
logger := logging.GetLogger()
|
||||
|
||||
// Load .env file
|
||||
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
|
||||
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")
|
||||
return nil, NewB2Error("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file", nil)
|
||||
}
|
||||
|
||||
// Create B2 client
|
||||
@ -34,13 +61,13 @@ func NewClientFromEnv() (*Client, error) {
|
||||
AccountID: keyID,
|
||||
ApplicationKey: applicationKey,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating B2 client: %w", err)
|
||||
return nil, NewB2Error("error creating B2 client", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
b2: b2,
|
||||
b2: b2,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -49,9 +76,8 @@ func (c *Client) ListBuckets(ctx context.Context) ([]*backblaze.Bucket, error) {
|
||||
// List all buckets
|
||||
buckets, err := c.b2.ListBuckets()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing buckets: %w", err)
|
||||
return nil, NewB2Error("error listing buckets", err)
|
||||
}
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
@ -60,7 +86,7 @@ func (c *Client) GetBucket(ctx context.Context, name string) (*backblaze.Bucket,
|
||||
// List all buckets
|
||||
buckets, err := c.b2.ListBuckets()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing buckets: %w", err)
|
||||
return nil, NewB2Error("error listing buckets", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -84,8 +110,9 @@ func (c *Client) CreateBucket(ctx context.Context, name string, public bool) (*b
|
||||
// Create bucket
|
||||
bucket, err := c.b2.CreateBucket(name, bucketType)
|
||||
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
|
||||
}
|
||||
|
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