auto_backup.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. package backup
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "time"
  9. "github.com/0xJacky/Nginx-UI/internal/notification"
  10. "github.com/0xJacky/Nginx-UI/model"
  11. "github.com/0xJacky/Nginx-UI/query"
  12. "github.com/uozi-tech/cosy"
  13. "github.com/uozi-tech/cosy/logger"
  14. )
  15. // BackupExecutionResult contains the result of a backup execution
  16. type BackupExecutionResult struct {
  17. FilePath string // Path to the created backup file
  18. KeyPath string // Path to the encryption key file (if applicable)
  19. }
  20. // ExecuteAutoBackup executes an automatic backup task based on the configuration.
  21. // This function handles all types of backup operations and manages the backup status
  22. // throughout the execution process.
  23. //
  24. // Parameters:
  25. // - autoBackup: The auto backup configuration to execute
  26. //
  27. // Returns:
  28. // - error: CosyError if backup execution fails, nil if successful
  29. func ExecuteAutoBackup(autoBackup *model.AutoBackup) error {
  30. logger.Infof("Starting auto backup task: %s (ID: %d, Type: %s, Storage: %s)",
  31. autoBackup.Name, autoBackup.ID, autoBackup.BackupType, autoBackup.StorageType)
  32. // Validate storage configuration before starting backup
  33. if err := validateStorageConfiguration(autoBackup); err != nil {
  34. logger.Errorf("Storage configuration validation failed for task %s: %v", autoBackup.Name, err)
  35. updateBackupStatus(autoBackup.ID, model.BackupStatusFailed, err.Error())
  36. // Send validation failure notification
  37. notification.Error(
  38. fmt.Sprintf("Auto Backup Configuration Error: %s", autoBackup.Name),
  39. fmt.Sprintf("Storage configuration validation failed for backup task '%s'", autoBackup.Name),
  40. map[string]interface{}{
  41. "backup_id": autoBackup.ID,
  42. "backup_name": autoBackup.Name,
  43. "error": err.Error(),
  44. "timestamp": time.Now(),
  45. },
  46. )
  47. return err
  48. }
  49. // Update backup status to pending
  50. if err := updateBackupStatus(autoBackup.ID, model.BackupStatusPending, ""); err != nil {
  51. logger.Errorf("Failed to update backup status to pending: %v", err)
  52. return cosy.WrapErrorWithParams(ErrAutoBackupWriteFile, err.Error())
  53. }
  54. // Execute backup based on type
  55. result, backupErr := executeBackupByType(autoBackup)
  56. // Update backup status based on execution result
  57. now := time.Now()
  58. if backupErr != nil {
  59. logger.Errorf("Auto backup task %s failed: %v", autoBackup.Name, backupErr)
  60. if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusFailed, backupErr.Error(), &now); updateErr != nil {
  61. logger.Errorf("Failed to update backup status to failed: %v", updateErr)
  62. }
  63. // Send failure notification
  64. notification.Error(
  65. fmt.Sprintf("Auto Backup Failed: %s", autoBackup.Name),
  66. fmt.Sprintf("Backup task '%s' failed to execute", autoBackup.Name),
  67. map[string]interface{}{
  68. "backup_id": autoBackup.ID,
  69. "backup_name": autoBackup.Name,
  70. "error": backupErr.Error(),
  71. "timestamp": now,
  72. },
  73. )
  74. return backupErr
  75. }
  76. // Handle storage upload based on storage type
  77. if uploadErr := handleBackupStorage(autoBackup, result); uploadErr != nil {
  78. logger.Errorf("Auto backup storage upload failed for task %s: %v", autoBackup.Name, uploadErr)
  79. if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusFailed, uploadErr.Error(), &now); updateErr != nil {
  80. logger.Errorf("Failed to update backup status to failed: %v", updateErr)
  81. }
  82. // Send storage failure notification
  83. notification.Error(
  84. fmt.Sprintf("Auto Backup Storage Failed: %s", autoBackup.Name),
  85. fmt.Sprintf("Backup task '%s' failed during storage upload", autoBackup.Name),
  86. map[string]interface{}{
  87. "backup_id": autoBackup.ID,
  88. "backup_name": autoBackup.Name,
  89. "error": uploadErr.Error(),
  90. "timestamp": now,
  91. },
  92. )
  93. return uploadErr
  94. }
  95. logger.Infof("Auto backup task %s completed successfully, file: %s", autoBackup.Name, result.FilePath)
  96. if updateErr := updateBackupStatusWithTime(autoBackup.ID, model.BackupStatusSuccess, "", &now); updateErr != nil {
  97. logger.Errorf("Failed to update backup status to success: %v", updateErr)
  98. }
  99. // Send success notification
  100. notification.Success(
  101. fmt.Sprintf("Auto Backup Completed: %s", autoBackup.Name),
  102. fmt.Sprintf("Backup task '%s' completed successfully", autoBackup.Name),
  103. map[string]interface{}{
  104. "backup_id": autoBackup.ID,
  105. "backup_name": autoBackup.Name,
  106. "file_path": result.FilePath,
  107. "timestamp": now,
  108. },
  109. )
  110. return nil
  111. }
  112. // executeBackupByType executes the backup operation based on the backup type.
  113. // This function centralizes the backup type routing logic.
  114. //
  115. // Parameters:
  116. // - autoBackup: The auto backup configuration
  117. //
  118. // Returns:
  119. // - BackupExecutionResult: Result containing file paths
  120. // - error: CosyError if backup fails
  121. func executeBackupByType(autoBackup *model.AutoBackup) (*BackupExecutionResult, error) {
  122. switch autoBackup.BackupType {
  123. case model.BackupTypeNginxConfig:
  124. return createEncryptedBackup(autoBackup, "nginx_config")
  125. case model.BackupTypeNginxUIConfig:
  126. return createEncryptedBackup(autoBackup, "nginx_ui_config")
  127. case model.BackupTypeBothConfig:
  128. return createEncryptedBackup(autoBackup, "both_config")
  129. case model.BackupTypeCustomDir:
  130. return createCustomDirectoryBackup(autoBackup)
  131. default:
  132. return nil, cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.BackupType))
  133. }
  134. }
  135. // createEncryptedBackup creates an encrypted backup for Nginx/Nginx UI configurations.
  136. // This function handles all configuration backup types that require encryption.
  137. //
  138. // Parameters:
  139. // - autoBackup: The auto backup configuration
  140. // - backupPrefix: Prefix for the backup filename
  141. //
  142. // Returns:
  143. // - BackupExecutionResult: Result containing file paths
  144. // - error: CosyError if backup creation fails
  145. func createEncryptedBackup(autoBackup *model.AutoBackup, backupPrefix string) (*BackupExecutionResult, error) {
  146. // Generate unique filename with timestamp
  147. filename := fmt.Sprintf("%s_%s_%d.zip", backupPrefix, autoBackup.Name, time.Now().Unix())
  148. // Determine output path based on storage type
  149. var outputPath string
  150. if autoBackup.StorageType == model.StorageTypeS3 {
  151. // For S3 storage, create temporary file
  152. tempDir := os.TempDir()
  153. outputPath = filepath.Join(tempDir, filename)
  154. } else {
  155. // For local storage, use the configured storage path
  156. outputPath = filepath.Join(autoBackup.StoragePath, filename)
  157. }
  158. // Create backup using the main backup function
  159. backupResult, err := Backup()
  160. if err != nil {
  161. return nil, cosy.WrapErrorWithParams(ErrBackupNginx, err.Error())
  162. }
  163. // Write encrypted backup content to file
  164. if err := writeBackupFile(outputPath, backupResult.BackupContent); err != nil {
  165. return nil, err
  166. }
  167. // Create and write encryption key file
  168. keyPath := outputPath + ".key"
  169. if err := writeKeyFile(keyPath, backupResult.AESKey, backupResult.AESIv); err != nil {
  170. return nil, err
  171. }
  172. return &BackupExecutionResult{
  173. FilePath: outputPath,
  174. KeyPath: keyPath,
  175. }, nil
  176. }
  177. // createCustomDirectoryBackup creates an unencrypted backup of a custom directory.
  178. // This function handles custom directory backups which are stored as plain ZIP files.
  179. //
  180. // Parameters:
  181. // - autoBackup: The auto backup configuration
  182. //
  183. // Returns:
  184. // - BackupExecutionResult: Result containing file paths
  185. // - error: CosyError if backup creation fails
  186. func createCustomDirectoryBackup(autoBackup *model.AutoBackup) (*BackupExecutionResult, error) {
  187. // Validate that backup path is specified for custom directory backup
  188. if autoBackup.BackupPath == "" {
  189. return nil, ErrAutoBackupPathRequired
  190. }
  191. // Validate backup source path
  192. if err := ValidateBackupPath(autoBackup.BackupPath); err != nil {
  193. return nil, err
  194. }
  195. // Generate unique filename with timestamp
  196. filename := fmt.Sprintf("custom_dir_%s_%d.zip", autoBackup.Name, time.Now().Unix())
  197. // Determine output path based on storage type
  198. var outputPath string
  199. if autoBackup.StorageType == model.StorageTypeS3 {
  200. // For S3 storage, create temporary file
  201. tempDir := os.TempDir()
  202. outputPath = filepath.Join(tempDir, filename)
  203. } else {
  204. // For local storage, use the configured storage path
  205. outputPath = filepath.Join(autoBackup.StoragePath, filename)
  206. }
  207. // Create unencrypted ZIP archive of the custom directory
  208. if err := createZipArchive(outputPath, autoBackup.BackupPath); err != nil {
  209. return nil, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
  210. }
  211. return &BackupExecutionResult{
  212. FilePath: outputPath,
  213. KeyPath: "", // No key file for unencrypted backups
  214. }, nil
  215. }
  216. // writeBackupFile writes backup content to the specified file path with proper permissions.
  217. // This function ensures backup files are created with secure permissions.
  218. //
  219. // Parameters:
  220. // - filePath: Destination file path
  221. // - content: Backup content to write
  222. //
  223. // Returns:
  224. // - error: CosyError if file writing fails
  225. func writeBackupFile(filePath string, content []byte) error {
  226. if err := os.WriteFile(filePath, content, 0600); err != nil {
  227. return cosy.WrapErrorWithParams(ErrAutoBackupWriteFile, err.Error())
  228. }
  229. return nil
  230. }
  231. // writeKeyFile writes encryption key information to a key file.
  232. // This function creates a key file containing AES key and IV for encrypted backups.
  233. //
  234. // Parameters:
  235. // - keyPath: Path for the key file
  236. // - aesKey: Base64 encoded AES key
  237. // - aesIv: Base64 encoded AES initialization vector
  238. //
  239. // Returns:
  240. // - error: CosyError if key file writing fails
  241. func writeKeyFile(keyPath, aesKey, aesIv string) error {
  242. keyContent := fmt.Sprintf("AES_KEY=%s\nAES_IV=%s\n", aesKey, aesIv)
  243. if err := os.WriteFile(keyPath, []byte(keyContent), 0600); err != nil {
  244. return cosy.WrapErrorWithParams(ErrAutoBackupWriteKeyFile, err.Error())
  245. }
  246. return nil
  247. }
  248. // updateBackupStatus updates the backup status in the database.
  249. // This function provides a centralized way to update backup execution status.
  250. //
  251. // Parameters:
  252. // - id: Auto backup configuration ID
  253. // - status: New backup status
  254. // - errorMsg: Error message (empty for successful backups)
  255. //
  256. // Returns:
  257. // - error: Database error if update fails
  258. func updateBackupStatus(id uint64, status model.BackupStatus, errorMsg string) error {
  259. _, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
  260. "last_backup_status": status,
  261. "last_backup_error": errorMsg,
  262. })
  263. return err
  264. }
  265. // updateBackupStatusWithTime updates the backup status and timestamp in the database.
  266. // This function updates both status and execution time for completed backup operations.
  267. //
  268. // Parameters:
  269. // - id: Auto backup configuration ID
  270. // - status: New backup status
  271. // - errorMsg: Error message (empty for successful backups)
  272. // - backupTime: Timestamp of the backup execution
  273. //
  274. // Returns:
  275. // - error: Database error if update fails
  276. func updateBackupStatusWithTime(id uint64, status model.BackupStatus, errorMsg string, backupTime *time.Time) error {
  277. _, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
  278. "last_backup_status": status,
  279. "last_backup_error": errorMsg,
  280. "last_backup_time": backupTime,
  281. })
  282. return err
  283. }
  284. // GetEnabledAutoBackups retrieves all enabled auto backup configurations from the database.
  285. // This function is used by the cron scheduler to get active backup tasks.
  286. //
  287. // Returns:
  288. // - []*model.AutoBackup: List of enabled auto backup configurations
  289. // - error: Database error if query fails
  290. func GetEnabledAutoBackups() ([]*model.AutoBackup, error) {
  291. return query.AutoBackup.Where(query.AutoBackup.Enabled.Is(true)).Find()
  292. }
  293. // GetAutoBackupByID retrieves a specific auto backup configuration by its ID.
  294. // This function provides access to individual backup configurations.
  295. //
  296. // Parameters:
  297. // - id: Auto backup configuration ID
  298. //
  299. // Returns:
  300. // - *model.AutoBackup: The auto backup configuration
  301. // - error: Database error if query fails or record not found
  302. func GetAutoBackupByID(id uint64) (*model.AutoBackup, error) {
  303. return query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).First()
  304. }
  305. // validateStorageConfiguration validates the storage configuration based on storage type.
  306. // This function centralizes storage validation logic for both local and S3 storage.
  307. //
  308. // Parameters:
  309. // - autoBackup: The auto backup configuration to validate
  310. //
  311. // Returns:
  312. // - error: CosyError if validation fails, nil if configuration is valid
  313. func validateStorageConfiguration(autoBackup *model.AutoBackup) error {
  314. switch autoBackup.StorageType {
  315. case model.StorageTypeLocal:
  316. // For local storage, validate the storage path
  317. return ValidateStoragePath(autoBackup.StoragePath)
  318. case model.StorageTypeS3:
  319. // For S3 storage, test the connection
  320. s3Client, err := NewS3Client(autoBackup)
  321. if err != nil {
  322. return err
  323. }
  324. return s3Client.TestS3Connection(context.Background())
  325. default:
  326. return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
  327. }
  328. }
  329. // handleBackupStorage handles the storage of backup files based on storage type.
  330. // This function routes backup storage to the appropriate handler (local or S3).
  331. //
  332. // Parameters:
  333. // - autoBackup: The auto backup configuration
  334. // - result: The backup execution result containing file paths
  335. //
  336. // Returns:
  337. // - error: CosyError if storage operation fails
  338. func handleBackupStorage(autoBackup *model.AutoBackup, result *BackupExecutionResult) error {
  339. switch autoBackup.StorageType {
  340. case model.StorageTypeLocal:
  341. // For local storage, files are already written to the correct location
  342. logger.Infof("Backup files stored locally: %s", result.FilePath)
  343. return nil
  344. case model.StorageTypeS3:
  345. // For S3 storage, upload files to S3 and optionally clean up local files
  346. return handleS3Storage(autoBackup, result)
  347. default:
  348. return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
  349. }
  350. }
  351. // handleS3Storage handles S3 storage operations for backup files.
  352. // This function uploads backup files to S3 and manages local file cleanup.
  353. //
  354. // Parameters:
  355. // - autoBackup: The auto backup configuration
  356. // - result: The backup execution result containing file paths
  357. //
  358. // Returns:
  359. // - error: CosyError if S3 operations fail
  360. func handleS3Storage(autoBackup *model.AutoBackup, result *BackupExecutionResult) error {
  361. // Create S3 client
  362. s3Client, err := NewS3Client(autoBackup)
  363. if err != nil {
  364. return err
  365. }
  366. // Upload backup files to S3
  367. ctx := context.Background()
  368. if err := s3Client.UploadBackupFiles(ctx, result, autoBackup); err != nil {
  369. return err
  370. }
  371. // Clean up local files after successful S3 upload
  372. if err := cleanupLocalBackupFiles(result); err != nil {
  373. logger.Warnf("Failed to cleanup local backup files: %v", err)
  374. // Don't return error for cleanup failure as the backup was successful
  375. }
  376. logger.Infof("Backup files successfully uploaded to S3 and local files cleaned up")
  377. return nil
  378. }
  379. // cleanupLocalBackupFiles removes local backup files after successful S3 upload.
  380. // This function helps manage disk space by removing temporary local files.
  381. //
  382. // Parameters:
  383. // - result: The backup execution result containing file paths to clean up
  384. //
  385. // Returns:
  386. // - error: Standard error if cleanup fails
  387. func cleanupLocalBackupFiles(result *BackupExecutionResult) error {
  388. // Remove backup file
  389. if err := os.Remove(result.FilePath); err != nil && !os.IsNotExist(err) {
  390. return fmt.Errorf("failed to remove backup file %s: %v", result.FilePath, err)
  391. }
  392. // Remove key file if it exists
  393. if result.KeyPath != "" {
  394. if err := os.Remove(result.KeyPath); err != nil && !os.IsNotExist(err) {
  395. return fmt.Errorf("failed to remove key file %s: %v", result.KeyPath, err)
  396. }
  397. }
  398. return nil
  399. }
  400. // ValidateAutoBackupConfig performs comprehensive validation of auto backup configuration.
  401. // This function centralizes all validation logic for both creation and modification.
  402. //
  403. // Parameters:
  404. // - config: Auto backup configuration to validate
  405. //
  406. // Returns:
  407. // - error: CosyError if validation fails, nil if configuration is valid
  408. func ValidateAutoBackupConfig(config *model.AutoBackup) error {
  409. // Validate backup path for custom directory backup type
  410. if config.BackupType == model.BackupTypeCustomDir {
  411. if config.BackupPath == "" {
  412. return ErrAutoBackupPathRequired
  413. }
  414. // Use centralized path validation from backup package
  415. if err := ValidateBackupPath(config.BackupPath); err != nil {
  416. return err
  417. }
  418. }
  419. // Validate storage path using centralized validation
  420. if config.StoragePath != "" {
  421. if err := ValidateStoragePath(config.StoragePath); err != nil {
  422. return err
  423. }
  424. }
  425. // Validate S3 configuration if storage type is S3
  426. if config.StorageType == model.StorageTypeS3 {
  427. if err := ValidateS3Config(config); err != nil {
  428. return err
  429. }
  430. }
  431. return nil
  432. }
  433. // ValidateS3Config validates S3 storage configuration completeness.
  434. // This function ensures all required S3 fields are provided when S3 storage is selected.
  435. //
  436. // Parameters:
  437. // - config: Auto backup configuration with S3 settings
  438. //
  439. // Returns:
  440. // - error: CosyError if S3 configuration is incomplete, nil if valid
  441. func ValidateS3Config(config *model.AutoBackup) error {
  442. var missingFields []string
  443. // Check required S3 fields
  444. if config.S3Bucket == "" {
  445. missingFields = append(missingFields, "bucket")
  446. }
  447. if config.S3AccessKeyID == "" {
  448. missingFields = append(missingFields, "access_key_id")
  449. }
  450. if config.S3SecretAccessKey == "" {
  451. missingFields = append(missingFields, "secret_access_key")
  452. }
  453. // Return error if any required fields are missing
  454. if len(missingFields) > 0 {
  455. return cosy.WrapErrorWithParams(ErrAutoBackupS3ConfigIncomplete, strings.Join(missingFields, ", "))
  456. }
  457. return nil
  458. }
  459. // RestoreAutoBackup restores a soft-deleted auto backup configuration.
  460. // This function restores the backup configuration and re-registers the cron job if enabled.
  461. //
  462. // Parameters:
  463. // - id: Auto backup configuration ID to restore
  464. //
  465. // Returns:
  466. // - error: Database error if restore fails
  467. func RestoreAutoBackup(id uint64) error {
  468. // Restore the soft-deleted record
  469. _, err := query.AutoBackup.Unscoped().Where(query.AutoBackup.ID.Eq(id)).Update(query.AutoBackup.DeletedAt, nil)
  470. if err != nil {
  471. return err
  472. }
  473. // Get the restored backup configuration
  474. autoBackup, err := GetAutoBackupByID(id)
  475. if err != nil {
  476. return err
  477. }
  478. // Re-register cron job if the backup is enabled
  479. if autoBackup.Enabled {
  480. // Import cron package to register the job
  481. // Note: This would require importing the cron package, which might create circular dependency
  482. // The actual implementation should be handled at the API level
  483. logger.Infof("Auto backup %d restored and needs cron job registration", id)
  484. }
  485. return nil
  486. }