|
|
@@ -1,6 +1,7 @@
|
|
|
package services
|
|
|
|
|
|
import (
|
|
|
+ "archive/zip"
|
|
|
"context"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
@@ -43,7 +44,7 @@ func (s *BackupService) LoadAndSchedule() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if config.Enabled && config.Time != "" {
|
|
|
+ if (config.Enabled || config.ResourceEnabled) && config.Time != "" {
|
|
|
s.ScheduleBackup(config.Time)
|
|
|
} else {
|
|
|
s.StopSchedule()
|
|
|
@@ -70,7 +71,13 @@ func (s *BackupService) ScheduleBackup(timeStr string) {
|
|
|
|
|
|
id, err := s.Cron.AddFunc(spec, func() {
|
|
|
log.Println("Starting scheduled backup...")
|
|
|
- s.PerformBackup()
|
|
|
+ config, _ := s.GetConfig()
|
|
|
+ if config.Enabled {
|
|
|
+ s.PerformBackup()
|
|
|
+ }
|
|
|
+ if config.ResourceEnabled {
|
|
|
+ s.PerformResourceBackup()
|
|
|
+ }
|
|
|
})
|
|
|
if err != nil {
|
|
|
log.Printf("Failed to schedule backup: %v", err)
|
|
|
@@ -117,7 +124,7 @@ func (s *BackupService) SaveConfig(config *models.BackupConfig) error {
|
|
|
}
|
|
|
|
|
|
// Reschedule
|
|
|
- if config.Enabled {
|
|
|
+ if config.Enabled || config.ResourceEnabled {
|
|
|
s.ScheduleBackup(config.Time)
|
|
|
} else {
|
|
|
s.StopSchedule()
|
|
|
@@ -373,3 +380,123 @@ func (s *BackupService) DownloadFile(logID string) (*minio.Object, string, error
|
|
|
|
|
|
return object, backupLog.FileName, nil
|
|
|
}
|
|
|
+
|
|
|
+func (s *BackupService) PerformResourceBackup() {
|
|
|
+ s.Lock.Lock()
|
|
|
+ defer s.Lock.Unlock()
|
|
|
+
|
|
|
+ startTime := time.Now()
|
|
|
+ // 使用 _resource_ 前缀区分数据库备份
|
|
|
+ fileName := fmt.Sprintf("ems_resource_%s.zip", startTime.Format("20060102_150405"))
|
|
|
+
|
|
|
+ backupLog := models.BackupLog{
|
|
|
+ ID: uuid.New(),
|
|
|
+ StartTime: startTime,
|
|
|
+ Status: "RUNNING",
|
|
|
+ UploadStatus: "PENDING",
|
|
|
+ FileName: fileName,
|
|
|
+ }
|
|
|
+ if err := models.DB.Create(&backupLog).Error; err != nil {
|
|
|
+ log.Printf("[ERROR] Failed to create resource backup log: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 查询数据 (资源与物联中心的四个部分)
|
|
|
+ var sources []models.IntegrationSource
|
|
|
+ var devices []models.Device
|
|
|
+ var templates []models.EquipmentCleaningFormulaTemplate
|
|
|
+ var locations []models.SysLocation
|
|
|
+
|
|
|
+ // 查询数据
|
|
|
+ models.DB.Find(&sources)
|
|
|
+ models.DB.Find(&devices)
|
|
|
+ models.DB.Find(&templates)
|
|
|
+ models.DB.Find(&locations)
|
|
|
+
|
|
|
+ // 2. 创建 Zip 文件
|
|
|
+ backupDir := "backups"
|
|
|
+ if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
|
|
+ os.MkdirAll(backupDir, 0755)
|
|
|
+ }
|
|
|
+ filePath := filepath.Join(backupDir, fileName)
|
|
|
+
|
|
|
+ zipFile, err := os.Create(filePath)
|
|
|
+ if err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to create zip file: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // defer will be called when function returns
|
|
|
+ defer zipFile.Close()
|
|
|
+
|
|
|
+ archive := zip.NewWriter(zipFile)
|
|
|
+ defer archive.Close()
|
|
|
+
|
|
|
+ // 辅助函数: 写入 JSON 到 Zip
|
|
|
+ writeJSON := func(name string, data interface{}) error {
|
|
|
+ w, err := archive.Create(name)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ encoder := json.NewEncoder(w)
|
|
|
+ encoder.SetIndent("", " ")
|
|
|
+ return encoder.Encode(data)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入四个部分的 JSON 文件
|
|
|
+ if err := writeJSON("integration_sources.json", sources); err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to write sources: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := writeJSON("devices.json", devices); err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to write devices: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := writeJSON("cleaning_templates.json", templates); err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to write templates: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := writeJSON("sys_locations.json", locations); err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to write locations: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭 Zip Writer 以确保所有数据写入文件
|
|
|
+ if err := archive.Close(); err != nil {
|
|
|
+ s.logBackupError(&backupLog, fmt.Sprintf("Failed to close archive: %v", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Note: zipFile.Close() is deferred
|
|
|
+
|
|
|
+ // 3. 更新日志并上传
|
|
|
+ info, err := os.Stat(filePath)
|
|
|
+ if err == nil {
|
|
|
+ backupLog.Size = info.Size()
|
|
|
+ }
|
|
|
+ backupLog.FilePath = fileName // 保存文件名作为相对路径
|
|
|
+ backupLog.Status = "SUCCESS"
|
|
|
+ backupLog.Message = "Resource backup created successfully."
|
|
|
+ backupLog.EndTime = time.Now()
|
|
|
+ models.DB.Save(&backupLog)
|
|
|
+
|
|
|
+ // 4. 上传到 MinIO (复用现有的上传逻辑)
|
|
|
+ config, _ := s.GetConfig()
|
|
|
+ if config.Endpoint != "" && config.Bucket != "" {
|
|
|
+ err := s.uploadToMinIO(filePath, fileName, config)
|
|
|
+ if err != nil {
|
|
|
+ log.Printf("MinIO upload failed: %v", err)
|
|
|
+ backupLog.Message += fmt.Sprintf(" Upload failed: %v", err)
|
|
|
+ backupLog.UploadStatus = "FAILED"
|
|
|
+ } else {
|
|
|
+ backupLog.UploadStatus = "UPLOADED"
|
|
|
+ backupLog.Message += " Uploaded to MinIO."
|
|
|
+ }
|
|
|
+ models.DB.Save(&backupLog)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 辅助方法: 记录错误
|
|
|
+func (s *BackupService) logBackupError(log *models.BackupLog, msg string) {
|
|
|
+ log.Status = "FAILED"
|
|
|
+ log.Message = msg
|
|
|
+ log.EndTime = time.Now()
|
|
|
+ models.DB.Save(log)
|
|
|
+}
|