| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 | package backupimport (	"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")// BackupExecutionResult contains the result of a backup executiontype BackupExecutionResult 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 successfulfunc 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 failsfunc executeBackupByType(autoBackup *model.AutoBackup) (*BackupExecutionResult, 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 failsfunc createEncryptedBackup(autoBackup *model.AutoBackup) (*BackupExecutionResult, 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 &BackupExecutionResult{		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 failsfunc createCustomDirectoryBackup(autoBackup *model.AutoBackup) (*BackupExecutionResult, 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 &BackupExecutionResult{		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 failsfunc 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 failsfunc 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 failsfunc 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 failsfunc 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 failsfunc 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 foundfunc 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 validfunc 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 failsfunc handleBackupStorage(autoBackup *model.AutoBackup, result *BackupExecutionResult) 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 failfunc handleS3Storage(autoBackup *model.AutoBackup, result *BackupExecutionResult) 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 failsfunc cleanupLocalBackupFiles(result *BackupExecutionResult) 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 validfunc 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 validfunc 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 failsfunc 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}
 |