Files
nginx-ui/internal/backup/auto_backup.go

529 lines
18 KiB
Go

package backup
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/0xJacky/Nginx-UI/internal/notification"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
// ExecutionResult contains the result of a backup execution
type ExecutionResult struct {
FilePath string // Path to the created backup file
KeyPath string // Path to the encryption key file (if applicable)
}
// ExecuteAutoBackup executes an automatic backup task based on the configuration.
// This function handles all types of backup operations and manages the backup status
// throughout the execution process.
//
// Parameters:
// - autoBackup: The auto backup configuration to execute
//
// Returns:
// - error: CosyError if backup execution fails, nil if successful
func ExecuteAutoBackup(autoBackup *model.AutoBackup) error {
logger.Infof("Starting auto backup task: %s (ID: %d, Type: %s, Storage: %s)",
autoBackup.GetName(), autoBackup.ID, autoBackup.BackupType, autoBackup.StorageType)
// Validate storage configuration before starting backup
if err := validateStorageConfiguration(autoBackup); err != nil {
logger.Errorf("Storage configuration validation failed for task %s: %v", autoBackup.Name, err)
_ = updateBackupStatus(autoBackup.ID, model.BackupStatusFailed, err.Error())
// Send validation failure notification
notification.Error("Auto Backup Configuration Error",
"Storage configuration validation failed for backup task %{backup_name}, error: %{error}",
map[string]interface{}{
"backup_id": autoBackup.ID,
"backup_name": autoBackup.Name,
"error": err.Error(),
},
)
return err
}
// Update backup status to pending
if err := updateBackupStatus(autoBackup.ID, model.BackupStatusPending, ""); err != nil {
logger.Errorf("Failed to update backup status to pending: %v", err)
return cosy.WrapErrorWithParams(ErrAutoBackupWriteFile, err.Error())
}
// Execute backup based on type
result, backupErr := executeBackupByType(autoBackup)
// Update backup status based on execution result
now := time.Now()
if backupErr != nil {
logger.Errorf("Auto backup task %s failed: %v", autoBackup.Name, backupErr)
if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusFailed, backupErr.Error(), &now); updateErr != nil {
logger.Errorf("Failed to update backup status to failed: %v", updateErr)
}
// Send failure notification
notification.Error("Auto Backup Failed",
"Backup task %{backup_name} failed to execute, error: %{error}",
map[string]interface{}{
"backup_id": autoBackup.ID,
"backup_name": autoBackup.Name,
"error": backupErr.Error(),
},
)
return backupErr
}
// Handle storage upload based on storage type
if uploadErr := handleBackupStorage(autoBackup, result); uploadErr != nil {
logger.Errorf("Auto backup storage upload failed for task %s: %v", autoBackup.Name, uploadErr)
if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusFailed, uploadErr.Error(), &now); updateErr != nil {
logger.Errorf("Failed to update backup status to failed: %v", updateErr)
}
// Send storage failure notification
notification.Error("Auto Backup Storage Failed",
"Backup task %{backup_name} failed during storage upload, error: %{error}",
map[string]interface{}{
"backup_id": autoBackup.ID,
"backup_name": autoBackup.Name,
"error": uploadErr.Error(),
"timestamp": now,
},
)
return uploadErr
}
logger.Infof("Auto backup task %s completed successfully, file: %s", autoBackup.Name, result.FilePath)
if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusSuccess, "", &now); updateErr != nil {
logger.Errorf("Failed to update backup status to success: %v", updateErr)
}
// Send success notification
notification.Success("Auto Backup Completed",
"Backup task %{backup_name} completed successfully, file: %{file_path}",
map[string]interface{}{
"backup_id": autoBackup.ID,
"backup_name": autoBackup.Name,
"file_path": result.FilePath,
},
)
return nil
}
// executeBackupByType executes the backup operation based on the backup type.
// This function centralizes the backup type routing logic.
//
// Parameters:
// - autoBackup: The auto backup configuration
//
// Returns:
// - BackupExecutionResult: Result containing file paths
// - error: CosyError if backup fails
func executeBackupByType(autoBackup *model.AutoBackup) (*ExecutionResult, error) {
switch autoBackup.BackupType {
case model.BackupTypeNginxAndNginxUI:
return createEncryptedBackup(autoBackup)
case model.BackupTypeCustomDir:
return createCustomDirectoryBackup(autoBackup)
default:
return nil, cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.BackupType))
}
}
// createEncryptedBackup creates an encrypted backup for Nginx/Nginx UI configurations.
// This function handles all configuration backup types that require encryption.
//
// Parameters:
// - autoBackup: The auto backup configuration
// - backupPrefix: Prefix for the backup filename
//
// Returns:
// - BackupExecutionResult: Result containing file paths
// - error: CosyError if backup creation fails
func createEncryptedBackup(autoBackup *model.AutoBackup) (*ExecutionResult, error) {
// Generate unique filename with timestamp
filename := fmt.Sprintf("%s_%d.zip", autoBackup.GetName(), time.Now().Unix())
// Determine output path based on storage type
var outputPath string
if autoBackup.StorageType == model.StorageTypeS3 {
// For S3 storage, create temporary file
tempDir := os.TempDir()
outputPath = filepath.Join(tempDir, filename)
} else {
// For local storage, use the configured storage path
outputPath = filepath.Join(autoBackup.StoragePath, filename)
}
// Create backup using the main backup function
backupResult, err := Backup()
if err != nil {
return nil, cosy.WrapErrorWithParams(ErrBackupNginx, err.Error())
}
// Write encrypted backup content to file
if err := writeBackupFile(outputPath, backupResult.BackupContent); err != nil {
return nil, err
}
// Create and write encryption key file
keyPath := outputPath + ".key"
if err := writeKeyFile(keyPath, backupResult.AESKey, backupResult.AESIv); err != nil {
return nil, err
}
return &ExecutionResult{
FilePath: outputPath,
KeyPath: keyPath,
}, nil
}
// createCustomDirectoryBackup creates an unencrypted backup of a custom directory.
// This function handles custom directory backups which are stored as plain ZIP files.
//
// Parameters:
// - autoBackup: The auto backup configuration
//
// Returns:
// - BackupExecutionResult: Result containing file paths
// - error: CosyError if backup creation fails
func createCustomDirectoryBackup(autoBackup *model.AutoBackup) (*ExecutionResult, error) {
// Validate that backup path is specified for custom directory backup
if autoBackup.BackupPath == "" {
return nil, ErrAutoBackupPathRequired
}
// Validate backup source path
if err := ValidateBackupPath(autoBackup.BackupPath); err != nil {
return nil, err
}
// Generate unique filename with timestamp
filename := fmt.Sprintf("custom_dir_%s_%d.zip", autoBackup.GetName(), time.Now().Unix())
// Determine output path based on storage type
var outputPath string
if autoBackup.StorageType == model.StorageTypeS3 {
// For S3 storage, create temporary file
tempDir := os.TempDir()
outputPath = filepath.Join(tempDir, filename)
} else {
// For local storage, use the configured storage path
outputPath = filepath.Join(autoBackup.StoragePath, filename)
}
// Create unencrypted ZIP archive of the custom directory
if err := createZipArchive(outputPath, autoBackup.BackupPath); err != nil {
return nil, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
}
return &ExecutionResult{
FilePath: outputPath,
KeyPath: "", // No key file for unencrypted backups
}, nil
}
// writeBackupFile writes backup content to the specified file path with proper permissions.
// This function ensures backup files are created with secure permissions.
//
// Parameters:
// - filePath: Destination file path
// - content: Backup content to write
//
// Returns:
// - error: CosyError if file writing fails
func writeBackupFile(filePath string, content []byte) error {
if err := os.WriteFile(filePath, content, 0600); err != nil {
return cosy.WrapErrorWithParams(ErrAutoBackupWriteFile, err.Error())
}
return nil
}
// writeKeyFile writes encryption key information to a key file.
// This function creates a key file containing AES key and IV for encrypted backups.
//
// Parameters:
// - keyPath: Path for the key file
// - aesKey: Base64 encoded AES key
// - aesIv: Base64 encoded AES initialization vector
//
// Returns:
// - error: CosyError if key file writing fails
func writeKeyFile(keyPath, aesKey, aesIv string) error {
keyContent := fmt.Sprintf("%s:%s", aesKey, aesIv)
if err := os.WriteFile(keyPath, []byte(keyContent), 0600); err != nil {
return cosy.WrapErrorWithParams(ErrAutoBackupWriteKeyFile, err.Error())
}
return nil
}
// updateBackupStatus updates the backup status in the database.
// This function provides a centralized way to update backup execution status.
//
// Parameters:
// - id: Auto backup configuration ID
// - status: New backup status
// - errorMsg: Error message (empty for successful backups)
//
// Returns:
// - error: Database error if update fails
func updateBackupStatus(id uint64, status model.BackupStatus, errorMsg string) error {
_, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
"last_backup_status": status,
"last_backup_error": errorMsg,
})
return err
}
// updateBackupStatusWithTime updates the backup status and timestamp in the database.
// This function updates both status and execution time for completed backup operations.
//
// Parameters:
// - id: Auto backup configuration ID
// - status: New backup status
// - errorMsg: Error message (empty for successful backups)
// - backupTime: Timestamp of the backup execution
//
// Returns:
// - error: Database error if update fails
func updateBackupStatusWithTime(id uint64, status model.BackupStatus, errorMsg string, backupTime *time.Time) error {
_, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
"last_backup_status": status,
"last_backup_error": errorMsg,
"last_backup_time": backupTime,
})
return err
}
// GetEnabledAutoBackups retrieves all enabled auto backup configurations from the database.
// This function is used by the cron scheduler to get active backup tasks.
//
// Returns:
// - []*model.AutoBackup: List of enabled auto backup configurations
// - error: Database error if query fails
func GetEnabledAutoBackups() ([]*model.AutoBackup, error) {
return query.AutoBackup.Where(query.AutoBackup.Enabled.Is(true)).Find()
}
// GetAutoBackupByID retrieves a specific auto backup configuration by its ID.
// This function provides access to individual backup configurations.
//
// Parameters:
// - id: Auto backup configuration ID
//
// Returns:
// - *model.AutoBackup: The auto backup configuration
// - error: Database error if query fails or record not found
func GetAutoBackupByID(id uint64) (*model.AutoBackup, error) {
return query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).First()
}
// validateStorageConfiguration validates the storage configuration based on storage type.
// This function centralizes storage validation logic for both local and S3 storage.
//
// Parameters:
// - autoBackup: The auto backup configuration to validate
//
// Returns:
// - error: CosyError if validation fails, nil if configuration is valid
func validateStorageConfiguration(autoBackup *model.AutoBackup) error {
switch autoBackup.StorageType {
case model.StorageTypeLocal:
// For local storage, validate the storage path
return ValidateStoragePath(autoBackup.StoragePath)
case model.StorageTypeS3:
// For S3 storage, test the connection
s3Client, err := NewS3Client(autoBackup)
if err != nil {
return err
}
return s3Client.TestS3Connection(context.Background())
default:
return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
}
}
// handleBackupStorage handles the storage of backup files based on storage type.
// This function routes backup storage to the appropriate handler (local or S3).
//
// Parameters:
// - autoBackup: The auto backup configuration
// - result: The backup execution result containing file paths
//
// Returns:
// - error: CosyError if storage operation fails
func handleBackupStorage(autoBackup *model.AutoBackup, result *ExecutionResult) error {
switch autoBackup.StorageType {
case model.StorageTypeLocal:
// For local storage, files are already written to the correct location
logger.Infof("Backup files stored locally: %s", result.FilePath)
return nil
case model.StorageTypeS3:
// For S3 storage, upload files to S3 and optionally clean up local files
return handleS3Storage(autoBackup, result)
default:
return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
}
}
// handleS3Storage handles S3 storage operations for backup files.
// This function uploads backup files to S3 and manages local file cleanup.
//
// Parameters:
// - autoBackup: The auto backup configuration
// - result: The backup execution result containing file paths
//
// Returns:
// - error: CosyError if S3 operations fail
func handleS3Storage(autoBackup *model.AutoBackup, result *ExecutionResult) error {
// Create S3 client
s3Client, err := NewS3Client(autoBackup)
if err != nil {
return err
}
// Upload backup files to S3
ctx := context.Background()
if err := s3Client.UploadBackupFiles(ctx, result, autoBackup); err != nil {
return err
}
// Clean up local files after successful S3 upload
if err := cleanupLocalBackupFiles(result); err != nil {
logger.Warnf("Failed to cleanup local backup files: %v", err)
// Don't return error for cleanup failure as the backup was successful
}
logger.Infof("Backup files successfully uploaded to S3 and local files cleaned up")
return nil
}
// cleanupLocalBackupFiles removes local backup files after successful S3 upload.
// This function helps manage disk space by removing temporary local files.
//
// Parameters:
// - result: The backup execution result containing file paths to clean up
//
// Returns:
// - error: Standard error if cleanup fails
func cleanupLocalBackupFiles(result *ExecutionResult) error {
// Remove backup file
if err := os.Remove(result.FilePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove backup file %s: %v", result.FilePath, err)
}
// Remove key file if it exists
if result.KeyPath != "" {
if err := os.Remove(result.KeyPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove key file %s: %v", result.KeyPath, err)
}
}
return nil
}
// ValidateAutoBackupConfig performs comprehensive validation of auto backup configuration.
// This function centralizes all validation logic for both creation and modification.
//
// Parameters:
// - config: Auto backup configuration to validate
//
// Returns:
// - error: CosyError if validation fails, nil if configuration is valid
func ValidateAutoBackupConfig(config *model.AutoBackup) error {
// Validate backup path for custom directory backup type
if config.BackupType == model.BackupTypeCustomDir {
if config.BackupPath == "" {
return ErrAutoBackupPathRequired
}
// Use centralized path validation from backup package
if err := ValidateBackupPath(config.BackupPath); err != nil {
return err
}
}
// Validate storage path using centralized validation
if config.StorageType == model.StorageTypeLocal && config.StoragePath != "" {
if err := ValidateStoragePath(config.StoragePath); err != nil {
return err
}
}
// Validate S3 configuration if storage type is S3
if config.StorageType == model.StorageTypeS3 {
if err := ValidateS3Config(config); err != nil {
return err
}
}
return nil
}
// ValidateS3Config validates S3 storage configuration completeness.
// This function ensures all required S3 fields are provided when S3 storage is selected.
//
// Parameters:
// - config: Auto backup configuration with S3 settings
//
// Returns:
// - error: CosyError if S3 configuration is incomplete, nil if valid
func ValidateS3Config(config *model.AutoBackup) error {
var missingFields []string
// Check required S3 fields
if config.S3Bucket == "" {
missingFields = append(missingFields, "bucket")
}
if config.S3AccessKeyID == "" {
missingFields = append(missingFields, "access_key_id")
}
if config.S3SecretAccessKey == "" {
missingFields = append(missingFields, "secret_access_key")
}
// Return error if any required fields are missing
if len(missingFields) > 0 {
return cosy.WrapErrorWithParams(ErrAutoBackupS3ConfigIncomplete, strings.Join(missingFields, ", "))
}
return nil
}
// RestoreAutoBackup restores a soft-deleted auto backup configuration.
// This function restores the backup configuration and re-registers the cron job if enabled.
//
// Parameters:
// - id: Auto backup configuration ID to restore
//
// Returns:
// - error: Database error if restore fails
func RestoreAutoBackup(id uint64) error {
// Restore the soft-deleted record
_, err := query.AutoBackup.Unscoped().Where(query.AutoBackup.ID.Eq(id)).Update(query.AutoBackup.DeletedAt, nil)
if err != nil {
return err
}
// Get the restored backup configuration
autoBackup, err := GetAutoBackupByID(id)
if err != nil {
return err
}
// Re-register cron job if the backup is enabled
if autoBackup.Enabled {
// Import cron package to register the job
// Note: This would require importing the cron package, which might create circular dependency
// The actual implementation should be handled at the API level
logger.Infof("Auto backup %d restored and needs cron job registration", id)
}
return nil
}