auto_backup.go 18 KB

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