auto_backup.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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.GetName(), 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.BackupTypeNginxAndNginxUI:
  124. return createEncryptedBackup(autoBackup)
  125. case model.BackupTypeCustomDir:
  126. return createCustomDirectoryBackup(autoBackup)
  127. default:
  128. return nil, cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.BackupType))
  129. }
  130. }
  131. // createEncryptedBackup creates an encrypted backup for Nginx/Nginx UI configurations.
  132. // This function handles all configuration backup types that require encryption.
  133. //
  134. // Parameters:
  135. // - autoBackup: The auto backup configuration
  136. // - backupPrefix: Prefix for the backup filename
  137. //
  138. // Returns:
  139. // - BackupExecutionResult: Result containing file paths
  140. // - error: CosyError if backup creation fails
  141. func createEncryptedBackup(autoBackup *model.AutoBackup) (*BackupExecutionResult, error) {
  142. // Generate unique filename with timestamp
  143. filename := fmt.Sprintf("%s_%d.zip", autoBackup.GetName(), time.Now().Unix())
  144. // Determine output path based on storage type
  145. var outputPath string
  146. if autoBackup.StorageType == model.StorageTypeS3 {
  147. // For S3 storage, create temporary file
  148. tempDir := os.TempDir()
  149. outputPath = filepath.Join(tempDir, filename)
  150. } else {
  151. // For local storage, use the configured storage path
  152. outputPath = filepath.Join(autoBackup.StoragePath, filename)
  153. }
  154. // Create backup using the main backup function
  155. backupResult, err := Backup()
  156. if err != nil {
  157. return nil, cosy.WrapErrorWithParams(ErrBackupNginx, err.Error())
  158. }
  159. // Write encrypted backup content to file
  160. if err := writeBackupFile(outputPath, backupResult.BackupContent); err != nil {
  161. return nil, err
  162. }
  163. // Create and write encryption key file
  164. keyPath := outputPath + ".key"
  165. if err := writeKeyFile(keyPath, backupResult.AESKey, backupResult.AESIv); err != nil {
  166. return nil, err
  167. }
  168. return &BackupExecutionResult{
  169. FilePath: outputPath,
  170. KeyPath: keyPath,
  171. }, nil
  172. }
  173. // createCustomDirectoryBackup creates an unencrypted backup of a custom directory.
  174. // This function handles custom directory backups which are stored as plain ZIP files.
  175. //
  176. // Parameters:
  177. // - autoBackup: The auto backup configuration
  178. //
  179. // Returns:
  180. // - BackupExecutionResult: Result containing file paths
  181. // - error: CosyError if backup creation fails
  182. func createCustomDirectoryBackup(autoBackup *model.AutoBackup) (*BackupExecutionResult, error) {
  183. // Validate that backup path is specified for custom directory backup
  184. if autoBackup.BackupPath == "" {
  185. return nil, ErrAutoBackupPathRequired
  186. }
  187. // Validate backup source path
  188. if err := ValidateBackupPath(autoBackup.BackupPath); err != nil {
  189. return nil, err
  190. }
  191. // Generate unique filename with timestamp
  192. filename := fmt.Sprintf("custom_dir_%s_%d.zip", autoBackup.GetName(), time.Now().Unix())
  193. // Determine output path based on storage type
  194. var outputPath string
  195. if autoBackup.StorageType == model.StorageTypeS3 {
  196. // For S3 storage, create temporary file
  197. tempDir := os.TempDir()
  198. outputPath = filepath.Join(tempDir, filename)
  199. } else {
  200. // For local storage, use the configured storage path
  201. outputPath = filepath.Join(autoBackup.StoragePath, filename)
  202. }
  203. // Create unencrypted ZIP archive of the custom directory
  204. if err := createZipArchive(outputPath, autoBackup.BackupPath); err != nil {
  205. return nil, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
  206. }
  207. return &BackupExecutionResult{
  208. FilePath: outputPath,
  209. KeyPath: "", // No key file for unencrypted backups
  210. }, nil
  211. }
  212. // writeBackupFile writes backup content to the specified file path with proper permissions.
  213. // This function ensures backup files are created with secure permissions.
  214. //
  215. // Parameters:
  216. // - filePath: Destination file path
  217. // - content: Backup content to write
  218. //
  219. // Returns:
  220. // - error: CosyError if file writing fails
  221. func writeBackupFile(filePath string, content []byte) error {
  222. if err := os.WriteFile(filePath, content, 0600); err != nil {
  223. return cosy.WrapErrorWithParams(ErrAutoBackupWriteFile, err.Error())
  224. }
  225. return nil
  226. }
  227. // writeKeyFile writes encryption key information to a key file.
  228. // This function creates a key file containing AES key and IV for encrypted backups.
  229. //
  230. // Parameters:
  231. // - keyPath: Path for the key file
  232. // - aesKey: Base64 encoded AES key
  233. // - aesIv: Base64 encoded AES initialization vector
  234. //
  235. // Returns:
  236. // - error: CosyError if key file writing fails
  237. func writeKeyFile(keyPath, aesKey, aesIv string) error {
  238. keyContent := fmt.Sprintf("%s:%s", aesKey, aesIv)
  239. if err := os.WriteFile(keyPath, []byte(keyContent), 0600); err != nil {
  240. return cosy.WrapErrorWithParams(ErrAutoBackupWriteKeyFile, err.Error())
  241. }
  242. return nil
  243. }
  244. // updateBackupStatus updates the backup status in the database.
  245. // This function provides a centralized way to update backup execution status.
  246. //
  247. // Parameters:
  248. // - id: Auto backup configuration ID
  249. // - status: New backup status
  250. // - errorMsg: Error message (empty for successful backups)
  251. //
  252. // Returns:
  253. // - error: Database error if update fails
  254. func updateBackupStatus(id uint64, status model.BackupStatus, errorMsg string) error {
  255. _, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
  256. "last_backup_status": status,
  257. "last_backup_error": errorMsg,
  258. })
  259. return err
  260. }
  261. // updateBackupStatusWithTime updates the backup status and timestamp in the database.
  262. // This function updates both status and execution time for completed backup operations.
  263. //
  264. // Parameters:
  265. // - id: Auto backup configuration ID
  266. // - status: New backup status
  267. // - errorMsg: Error message (empty for successful backups)
  268. // - backupTime: Timestamp of the backup execution
  269. //
  270. // Returns:
  271. // - error: Database error if update fails
  272. func updateBackupStatusWithTime(id uint64, status model.BackupStatus, errorMsg string, backupTime *time.Time) error {
  273. _, err := query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).Updates(map[string]interface{}{
  274. "last_backup_status": status,
  275. "last_backup_error": errorMsg,
  276. "last_backup_time": backupTime,
  277. })
  278. return err
  279. }
  280. // GetEnabledAutoBackups retrieves all enabled auto backup configurations from the database.
  281. // This function is used by the cron scheduler to get active backup tasks.
  282. //
  283. // Returns:
  284. // - []*model.AutoBackup: List of enabled auto backup configurations
  285. // - error: Database error if query fails
  286. func GetEnabledAutoBackups() ([]*model.AutoBackup, error) {
  287. return query.AutoBackup.Where(query.AutoBackup.Enabled.Is(true)).Find()
  288. }
  289. // GetAutoBackupByID retrieves a specific auto backup configuration by its ID.
  290. // This function provides access to individual backup configurations.
  291. //
  292. // Parameters:
  293. // - id: Auto backup configuration ID
  294. //
  295. // Returns:
  296. // - *model.AutoBackup: The auto backup configuration
  297. // - error: Database error if query fails or record not found
  298. func GetAutoBackupByID(id uint64) (*model.AutoBackup, error) {
  299. return query.AutoBackup.Where(query.AutoBackup.ID.Eq(id)).First()
  300. }
  301. // validateStorageConfiguration validates the storage configuration based on storage type.
  302. // This function centralizes storage validation logic for both local and S3 storage.
  303. //
  304. // Parameters:
  305. // - autoBackup: The auto backup configuration to validate
  306. //
  307. // Returns:
  308. // - error: CosyError if validation fails, nil if configuration is valid
  309. func validateStorageConfiguration(autoBackup *model.AutoBackup) error {
  310. switch autoBackup.StorageType {
  311. case model.StorageTypeLocal:
  312. // For local storage, validate the storage path
  313. return ValidateStoragePath(autoBackup.StoragePath)
  314. case model.StorageTypeS3:
  315. // For S3 storage, test the connection
  316. s3Client, err := NewS3Client(autoBackup)
  317. if err != nil {
  318. return err
  319. }
  320. return s3Client.TestS3Connection(context.Background())
  321. default:
  322. return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
  323. }
  324. }
  325. // handleBackupStorage handles the storage of backup files based on storage type.
  326. // This function routes backup storage to the appropriate handler (local or S3).
  327. //
  328. // Parameters:
  329. // - autoBackup: The auto backup configuration
  330. // - result: The backup execution result containing file paths
  331. //
  332. // Returns:
  333. // - error: CosyError if storage operation fails
  334. func handleBackupStorage(autoBackup *model.AutoBackup, result *BackupExecutionResult) error {
  335. switch autoBackup.StorageType {
  336. case model.StorageTypeLocal:
  337. // For local storage, files are already written to the correct location
  338. logger.Infof("Backup files stored locally: %s", result.FilePath)
  339. return nil
  340. case model.StorageTypeS3:
  341. // For S3 storage, upload files to S3 and optionally clean up local files
  342. return handleS3Storage(autoBackup, result)
  343. default:
  344. return cosy.WrapErrorWithParams(ErrAutoBackupUnsupportedType, string(autoBackup.StorageType))
  345. }
  346. }
  347. // handleS3Storage handles S3 storage operations for backup files.
  348. // This function uploads backup files to S3 and manages local file cleanup.
  349. //
  350. // Parameters:
  351. // - autoBackup: The auto backup configuration
  352. // - result: The backup execution result containing file paths
  353. //
  354. // Returns:
  355. // - error: CosyError if S3 operations fail
  356. func handleS3Storage(autoBackup *model.AutoBackup, result *BackupExecutionResult) error {
  357. // Create S3 client
  358. s3Client, err := NewS3Client(autoBackup)
  359. if err != nil {
  360. return err
  361. }
  362. // Upload backup files to S3
  363. ctx := context.Background()
  364. if err := s3Client.UploadBackupFiles(ctx, result, autoBackup); err != nil {
  365. return err
  366. }
  367. // Clean up local files after successful S3 upload
  368. if err := cleanupLocalBackupFiles(result); err != nil {
  369. logger.Warnf("Failed to cleanup local backup files: %v", err)
  370. // Don't return error for cleanup failure as the backup was successful
  371. }
  372. logger.Infof("Backup files successfully uploaded to S3 and local files cleaned up")
  373. return nil
  374. }
  375. // cleanupLocalBackupFiles removes local backup files after successful S3 upload.
  376. // This function helps manage disk space by removing temporary local files.
  377. //
  378. // Parameters:
  379. // - result: The backup execution result containing file paths to clean up
  380. //
  381. // Returns:
  382. // - error: Standard error if cleanup fails
  383. func cleanupLocalBackupFiles(result *BackupExecutionResult) error {
  384. // Remove backup file
  385. if err := os.Remove(result.FilePath); err != nil && !os.IsNotExist(err) {
  386. return fmt.Errorf("failed to remove backup file %s: %v", result.FilePath, err)
  387. }
  388. // Remove key file if it exists
  389. if result.KeyPath != "" {
  390. if err := os.Remove(result.KeyPath); err != nil && !os.IsNotExist(err) {
  391. return fmt.Errorf("failed to remove key file %s: %v", result.KeyPath, err)
  392. }
  393. }
  394. return nil
  395. }
  396. // ValidateAutoBackupConfig performs comprehensive validation of auto backup configuration.
  397. // This function centralizes all validation logic for both creation and modification.
  398. //
  399. // Parameters:
  400. // - config: Auto backup configuration to validate
  401. //
  402. // Returns:
  403. // - error: CosyError if validation fails, nil if configuration is valid
  404. func ValidateAutoBackupConfig(config *model.AutoBackup) error {
  405. // Validate backup path for custom directory backup type
  406. if config.BackupType == model.BackupTypeCustomDir {
  407. if config.BackupPath == "" {
  408. return ErrAutoBackupPathRequired
  409. }
  410. // Use centralized path validation from backup package
  411. if err := ValidateBackupPath(config.BackupPath); err != nil {
  412. return err
  413. }
  414. }
  415. // Validate storage path using centralized validation
  416. if config.StorageType == model.StorageTypeLocal && config.StoragePath != "" {
  417. if err := ValidateStoragePath(config.StoragePath); err != nil {
  418. return err
  419. }
  420. }
  421. // Validate S3 configuration if storage type is S3
  422. if config.StorageType == model.StorageTypeS3 {
  423. if err := ValidateS3Config(config); err != nil {
  424. return err
  425. }
  426. }
  427. return nil
  428. }
  429. // ValidateS3Config validates S3 storage configuration completeness.
  430. // This function ensures all required S3 fields are provided when S3 storage is selected.
  431. //
  432. // Parameters:
  433. // - config: Auto backup configuration with S3 settings
  434. //
  435. // Returns:
  436. // - error: CosyError if S3 configuration is incomplete, nil if valid
  437. func ValidateS3Config(config *model.AutoBackup) error {
  438. var missingFields []string
  439. // Check required S3 fields
  440. if config.S3Bucket == "" {
  441. missingFields = append(missingFields, "bucket")
  442. }
  443. if config.S3AccessKeyID == "" {
  444. missingFields = append(missingFields, "access_key_id")
  445. }
  446. if config.S3SecretAccessKey == "" {
  447. missingFields = append(missingFields, "secret_access_key")
  448. }
  449. // Return error if any required fields are missing
  450. if len(missingFields) > 0 {
  451. return cosy.WrapErrorWithParams(ErrAutoBackupS3ConfigIncomplete, strings.Join(missingFields, ", "))
  452. }
  453. return nil
  454. }
  455. // RestoreAutoBackup restores a soft-deleted auto backup configuration.
  456. // This function restores the backup configuration and re-registers the cron job if enabled.
  457. //
  458. // Parameters:
  459. // - id: Auto backup configuration ID to restore
  460. //
  461. // Returns:
  462. // - error: Database error if restore fails
  463. func RestoreAutoBackup(id uint64) error {
  464. // Restore the soft-deleted record
  465. _, err := query.AutoBackup.Unscoped().Where(query.AutoBackup.ID.Eq(id)).Update(query.AutoBackup.DeletedAt, nil)
  466. if err != nil {
  467. return err
  468. }
  469. // Get the restored backup configuration
  470. autoBackup, err := GetAutoBackupByID(id)
  471. if err != nil {
  472. return err
  473. }
  474. // Re-register cron job if the backup is enabled
  475. if autoBackup.Enabled {
  476. // Import cron package to register the job
  477. // Note: This would require importing the cron package, which might create circular dependency
  478. // The actual implementation should be handled at the API level
  479. logger.Infof("Auto backup %d restored and needs cron job registration", id)
  480. }
  481. return nil
  482. }