backup_service.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. package services
  2. import (
  3. "archive/zip"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "strings"
  12. "sync"
  13. "time"
  14. "ems-backend/models"
  15. "github.com/google/uuid"
  16. "github.com/minio/minio-go/v7"
  17. "github.com/minio/minio-go/v7/pkg/credentials"
  18. "github.com/robfig/cron/v3"
  19. "github.com/xuri/excelize/v2"
  20. )
  21. type BackupService struct {
  22. Cron *cron.Cron
  23. Job cron.EntryID
  24. Lock sync.Mutex
  25. }
  26. var GlobalBackupService *BackupService
  27. func InitBackupService() {
  28. GlobalBackupService = &BackupService{
  29. Cron: cron.New(),
  30. }
  31. GlobalBackupService.Cron.Start()
  32. GlobalBackupService.LoadAndSchedule()
  33. }
  34. func (s *BackupService) LoadAndSchedule() {
  35. config, err := s.GetConfig()
  36. if err != nil {
  37. log.Printf("Failed to load backup config: %v", err)
  38. return
  39. }
  40. if (config.Enabled || config.ResourceEnabled) && config.Time != "" {
  41. s.ScheduleBackup(config.Time)
  42. } else {
  43. s.StopSchedule()
  44. }
  45. }
  46. func (s *BackupService) StopSchedule() {
  47. if s.Job != 0 {
  48. s.Cron.Remove(s.Job)
  49. s.Job = 0
  50. }
  51. }
  52. func (s *BackupService) ScheduleBackup(timeStr string) {
  53. s.StopSchedule()
  54. // timeStr is "HH:mm"
  55. // cron spec: "minute hour * * *"
  56. t, err := time.Parse("15:04", timeStr)
  57. if err != nil {
  58. log.Printf("Invalid time format: %v", err)
  59. return
  60. }
  61. spec := fmt.Sprintf("%d %d * * *", t.Minute(), t.Hour())
  62. id, err := s.Cron.AddFunc(spec, func() {
  63. log.Println("Starting scheduled backup...")
  64. config, _ := s.GetConfig()
  65. if config.Enabled {
  66. s.PerformBackup()
  67. }
  68. if config.ResourceEnabled {
  69. s.PerformResourceBackup()
  70. }
  71. })
  72. if err != nil {
  73. log.Printf("Failed to schedule backup: %v", err)
  74. return
  75. }
  76. s.Job = id
  77. log.Printf("Backup scheduled for %s (spec: %s)", timeStr, spec)
  78. }
  79. func (s *BackupService) GetConfig() (*models.BackupConfig, error) {
  80. var config models.BackupConfig
  81. // Defaults
  82. config.KeepDays = 7
  83. var sysConfig models.SysConfig
  84. // Load Schedule
  85. if err := models.DB.Where("config_key = ?", "sys.backup.config").First(&sysConfig).Error; err == nil {
  86. json.Unmarshal([]byte(sysConfig.ConfigValue), &config)
  87. }
  88. return &config, nil
  89. }
  90. func (s *BackupService) SaveConfig(config *models.BackupConfig) error {
  91. data, _ := json.Marshal(config)
  92. // Upsert
  93. var sysConfig models.SysConfig
  94. err := models.DB.Where("config_key = ?", "sys.backup.config").First(&sysConfig).Error
  95. if err != nil {
  96. sysConfig = models.SysConfig{
  97. ConfigKey: "sys.backup.config",
  98. ConfigValue: string(data),
  99. ConfigType: "Y",
  100. Remark: "Backup Configuration",
  101. }
  102. if err := models.DB.Create(&sysConfig).Error; err != nil {
  103. return err
  104. }
  105. } else {
  106. sysConfig.ConfigValue = string(data)
  107. if err := models.DB.Save(&sysConfig).Error; err != nil {
  108. return err
  109. }
  110. }
  111. // Reschedule
  112. if config.Enabled || config.ResourceEnabled {
  113. s.ScheduleBackup(config.Time)
  114. } else {
  115. s.StopSchedule()
  116. }
  117. return nil
  118. }
  119. func (s *BackupService) PerformBackup() {
  120. s.Lock.Lock()
  121. defer s.Lock.Unlock()
  122. startTime := time.Now()
  123. backupLog := models.BackupLog{
  124. ID: uuid.New(), // Explicitly generate UUID
  125. StartTime: startTime,
  126. Status: "RUNNING",
  127. UploadStatus: "PENDING",
  128. }
  129. if err := models.DB.Create(&backupLog).Error; err != nil {
  130. log.Printf("[ERROR] Failed to create backup log: %v", err)
  131. // Proceeding might be futile if we can't update status, but let's try
  132. } else {
  133. log.Printf("[INFO] Created backup log: %s", backupLog.ID)
  134. }
  135. // 1. Prepare Paths
  136. backupDir := "backups"
  137. if _, err := os.Stat(backupDir); os.IsNotExist(err) {
  138. os.MkdirAll(backupDir, 0755)
  139. }
  140. fileName := fmt.Sprintf("ems_backup_%s.sql", startTime.Format("20060102_150405"))
  141. filePath := filepath.Join(backupDir, fileName)
  142. // 2. Run pg_dump
  143. // Env vars for pg_dump
  144. pgUser := os.Getenv("POSTGRES_USER")
  145. pgHost := os.Getenv("POSTGRES_HOST")
  146. pgPort := os.Getenv("POSTGRES_PORT")
  147. pgDB := os.Getenv("POSTGRES_DB")
  148. pgPass := os.Getenv("POSTGRES_PASSWORD") // Or DB_PASSWORD
  149. // Ensure vars are set
  150. if pgHost == "" { pgHost = "postgres" }
  151. if pgUser == "" { pgUser = "ems" }
  152. if pgDB == "" { pgDB = "ems" }
  153. if pgPort == "" { pgPort = "5432" }
  154. // Construct command
  155. cmd := exec.Command("pg_dump", "-h", pgHost, "-p", pgPort, "-U", pgUser, "-d", pgDB, "-f", filePath)
  156. cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", pgPass))
  157. log.Printf("Executing backup command: %s", cmd.String())
  158. output, err := cmd.CombinedOutput()
  159. if err != nil {
  160. errMsg := fmt.Sprintf("pg_dump failed: %s, output: %s", err, string(output))
  161. log.Println(errMsg)
  162. backupLog.Status = "FAILED"
  163. backupLog.Message = errMsg
  164. backupLog.EndTime = time.Now()
  165. models.DB.Save(&backupLog)
  166. return
  167. }
  168. // Check file size
  169. info, err := os.Stat(filePath)
  170. if err == nil {
  171. backupLog.Size = info.Size()
  172. }
  173. backupLog.FileName = fileName
  174. backupLog.Status = "SUCCESS"
  175. backupLog.Message = "Backup created locally."
  176. models.DB.Save(&backupLog)
  177. // 3. Upload to MinIO
  178. config, _ := s.GetConfig()
  179. if config.Endpoint != "" && config.Bucket != "" {
  180. err := s.uploadToMinIO(filePath, fileName, config)
  181. if err != nil {
  182. log.Printf("MinIO upload failed: %v", err)
  183. backupLog.Message += fmt.Sprintf(" Upload failed: %v", err)
  184. backupLog.UploadStatus = "FAILED"
  185. } else {
  186. backupLog.UploadStatus = "UPLOADED"
  187. backupLog.Message += " Uploaded to MinIO."
  188. // Optional: Remove local file after successful upload to save space
  189. // os.Remove(filePath)
  190. }
  191. models.DB.Save(&backupLog)
  192. }
  193. }
  194. // Helper function to clean endpoint
  195. func cleanEndpoint(endpoint string) (string, bool) {
  196. useSSL := false
  197. if strings.HasPrefix(endpoint, "https://") {
  198. useSSL = true
  199. endpoint = strings.TrimPrefix(endpoint, "https://")
  200. } else {
  201. endpoint = strings.TrimPrefix(endpoint, "http://")
  202. }
  203. // Remove trailing slash if any
  204. endpoint = strings.TrimRight(endpoint, "/")
  205. return endpoint, useSSL
  206. }
  207. func (s *BackupService) uploadToMinIO(filePath, objectName string, config *models.BackupConfig) error {
  208. ctx := context.Background()
  209. endpoint, useSSL := cleanEndpoint(config.Endpoint)
  210. log.Printf("[DEBUG] Connecting to MinIO: Endpoint=%s, UseSSL=%v, Bucket=%s", endpoint, useSSL, config.Bucket)
  211. // Initialize minio client object.
  212. minioClient, err := minio.New(endpoint, &minio.Options{
  213. Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""),
  214. Secure: useSSL,
  215. })
  216. if err != nil {
  217. log.Printf("[ERROR] Failed to create MinIO client: %v", err)
  218. return err
  219. }
  220. // Make bucket if not exists
  221. exists, err := minioClient.BucketExists(ctx, config.Bucket)
  222. if err != nil {
  223. log.Printf("[ERROR] Failed to check bucket existence: %v", err)
  224. return err
  225. }
  226. if !exists {
  227. log.Printf("[INFO] Bucket %s does not exist, creating it...", config.Bucket)
  228. err = minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{})
  229. if err != nil {
  230. log.Printf("[ERROR] Failed to create bucket: %v", err)
  231. return err
  232. }
  233. }
  234. // Upload the file
  235. log.Printf("[INFO] Uploading file %s to bucket %s object %s", filePath, config.Bucket, objectName)
  236. info, err := minioClient.FPutObject(ctx, config.Bucket, objectName, filePath, minio.PutObjectOptions{})
  237. if err != nil {
  238. log.Printf("[ERROR] Failed to upload object: %v", err)
  239. return err
  240. }
  241. log.Printf("[SUCCESS] Successfully uploaded %s of size %d", objectName, info.Size)
  242. return nil
  243. }
  244. func (s *BackupService) GetLogs() ([]models.BackupLog, error) {
  245. logs := make([]models.BackupLog, 0)
  246. err := models.DB.Order("start_time desc").Limit(20).Find(&logs).Error
  247. if err != nil {
  248. log.Printf("[ERROR] GetLogs error: %v", err)
  249. } else {
  250. log.Printf("[DEBUG] GetLogs found %d records", len(logs))
  251. }
  252. return logs, err
  253. }
  254. // TestConnection tests the MinIO connection with the provided config
  255. func (s *BackupService) TestConnection(config *models.BackupConfig) error {
  256. ctx := context.Background()
  257. endpoint, useSSL := cleanEndpoint(config.Endpoint)
  258. log.Printf("[DEBUG] Testing MinIO Connection: Endpoint=%s, UseSSL=%v, AccessKey=***%s", endpoint, useSSL, getLast4(config.AccessKey))
  259. // Initialize minio client object.
  260. minioClient, err := minio.New(endpoint, &minio.Options{
  261. Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""),
  262. Secure: useSSL,
  263. })
  264. if err != nil {
  265. log.Printf("[ERROR] Failed to create MinIO client during test: %v", err)
  266. return fmt.Errorf("failed to create minio client: %v", err)
  267. }
  268. // Try to list buckets to verify credentials and endpoint
  269. log.Println("[DEBUG] Listing buckets to verify connection...")
  270. buckets, err := minioClient.ListBuckets(ctx)
  271. if err != nil {
  272. log.Printf("[ERROR] Failed to list buckets: %v", err)
  273. return fmt.Errorf("failed to connect to minio: %v", err)
  274. }
  275. log.Printf("[DEBUG] List buckets success. Found %d buckets.", len(buckets))
  276. // Also check if specific bucket exists if provided
  277. if config.Bucket != "" {
  278. log.Printf("[DEBUG] Checking specific bucket: %s", config.Bucket)
  279. exists, err := minioClient.BucketExists(ctx, config.Bucket)
  280. if err != nil {
  281. log.Printf("[ERROR] Failed to check bucket existence: %v", err)
  282. return fmt.Errorf("failed to check bucket existence: %v", err)
  283. }
  284. if !exists {
  285. log.Printf("[WARN] Bucket %s does not exist.", config.Bucket)
  286. // For test connection, maybe just returning "success but bucket missing" or "success" is enough.
  287. // Just return nil (success) if we can talk to MinIO.
  288. } else {
  289. log.Printf("[DEBUG] Bucket %s exists.", config.Bucket)
  290. }
  291. }
  292. return nil
  293. }
  294. func getLast4(s string) string {
  295. if len(s) > 4 {
  296. return s[len(s)-4:]
  297. }
  298. return s
  299. }
  300. // DownloadFile retrieves a file from MinIO and returns it as a stream
  301. func (s *BackupService) DownloadFile(logID string) (*minio.Object, string, error) {
  302. // 1. Get Log to find filename
  303. var backupLog models.BackupLog
  304. if err := models.DB.Where("id = ?", logID).First(&backupLog).Error; err != nil {
  305. return nil, "", fmt.Errorf("backup log not found: %v", err)
  306. }
  307. // 2. Get Config to find bucket/creds
  308. config, err := s.GetConfig()
  309. if err != nil {
  310. return nil, "", err
  311. }
  312. if config.Endpoint == "" || config.Bucket == "" {
  313. return nil, "", fmt.Errorf("minio not configured")
  314. }
  315. endpoint, useSSL := cleanEndpoint(config.Endpoint)
  316. // 3. Init Client
  317. minioClient, err := minio.New(endpoint, &minio.Options{
  318. Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""),
  319. Secure: useSSL,
  320. })
  321. if err != nil {
  322. return nil, "", err
  323. }
  324. // 4. Get Object
  325. object, err := minioClient.GetObject(context.Background(), config.Bucket, backupLog.FileName, minio.GetObjectOptions{})
  326. if err != nil {
  327. return nil, "", err
  328. }
  329. // Verify object exists/readable
  330. _, err = object.Stat()
  331. if err != nil {
  332. return nil, "", fmt.Errorf("failed to stat object in minio: %v", err)
  333. }
  334. return object, backupLog.FileName, nil
  335. }
  336. func (s *BackupService) PerformResourceBackup() {
  337. s.Lock.Lock()
  338. defer s.Lock.Unlock()
  339. startTime := time.Now()
  340. // 使用 _resource_ 前缀区分数据库备份
  341. fileName := fmt.Sprintf("ems_resource_%s.zip", startTime.Format("20060102_150405"))
  342. backupLog := models.BackupLog{
  343. ID: uuid.New(),
  344. StartTime: startTime,
  345. Status: "RUNNING",
  346. UploadStatus: "PENDING",
  347. FileName: fileName,
  348. }
  349. if err := models.DB.Create(&backupLog).Error; err != nil {
  350. log.Printf("[ERROR] Failed to create resource backup log: %v", err)
  351. }
  352. // 1. 查询数据 (资源与物联中心的四个部分)
  353. var sources []models.IntegrationSource
  354. var devices []models.Device
  355. var templates []models.EquipmentCleaningFormulaTemplate
  356. var locations []models.SysLocation
  357. // 查询数据
  358. models.DB.Find(&sources)
  359. models.DB.Find(&devices)
  360. models.DB.Find(&templates)
  361. models.DB.Find(&locations)
  362. // 填充 DeviceCount (IntegrationSource 需要此字段用于导出,虽然后端导入时不一定用)
  363. for i := range sources {
  364. var count int64
  365. models.DB.Model(&models.Device{}).Where("source_id = ?", sources[i].ID).Count(&count)
  366. sources[i].DeviceCount = count
  367. }
  368. // 2. 创建 Zip 文件
  369. backupDir := "backups"
  370. if _, err := os.Stat(backupDir); os.IsNotExist(err) {
  371. os.MkdirAll(backupDir, 0755)
  372. }
  373. filePath := filepath.Join(backupDir, fileName)
  374. zipFile, err := os.Create(filePath)
  375. if err != nil {
  376. s.logBackupError(&backupLog, fmt.Sprintf("Failed to create zip file: %v", err))
  377. return
  378. }
  379. // defer will be called when function returns
  380. defer zipFile.Close()
  381. archive := zip.NewWriter(zipFile)
  382. defer archive.Close()
  383. // 辅助函数: 写入 Excel 到 Zip
  384. writeExcel := func(filename string, f *excelize.File) error {
  385. w, err := archive.Create(filename)
  386. if err != nil {
  387. return err
  388. }
  389. // WriteTo 将 Excel 文件内容写入 Zip 中的文件流
  390. if _, err := f.WriteTo(w); err != nil {
  391. return err
  392. }
  393. return nil
  394. }
  395. // 3. 生成 Excel 文件并写入 Zip
  396. // 3.1 Integration Sources (数据源)
  397. // 格式参考前端: 名称, 驱动类型, 配置信息, 状态, 设备数量
  398. fSources := excelize.NewFile()
  399. fSources.SetSheetName("Sheet1", "数据源列表")
  400. headers := []string{"名称", "驱动类型", "配置信息", "状态", "设备数量"}
  401. for i, h := range headers {
  402. cell, _ := excelize.CoordinatesToCellName(i+1, 1)
  403. fSources.SetCellValue("数据源列表", cell, h)
  404. }
  405. for i, src := range sources {
  406. row := i + 2
  407. status := "离线"
  408. if src.Status == "ONLINE" {
  409. status = "在线"
  410. }
  411. fSources.SetCellValue("数据源列表", fmt.Sprintf("A%d", row), src.Name)
  412. fSources.SetCellValue("数据源列表", fmt.Sprintf("B%d", row), src.DriverType)
  413. fSources.SetCellValue("数据源列表", fmt.Sprintf("C%d", row), string(src.Config)) // JSON String
  414. fSources.SetCellValue("数据源列表", fmt.Sprintf("D%d", row), status)
  415. fSources.SetCellValue("数据源列表", fmt.Sprintf("E%d", row), src.DeviceCount)
  416. }
  417. if err := writeExcel("integration_sources.xlsx", fSources); err != nil {
  418. s.logBackupError(&backupLog, fmt.Sprintf("Failed to write sources excel: %v", err))
  419. return
  420. }
  421. // 3.2 Devices (设备)
  422. // 格式参考前端: Name, DeviceType, ExternalID, LocationID, Status, SourceID, AttributeMapping
  423. fDevices := excelize.NewFile()
  424. fDevices.SetSheetName("Sheet1", "Devices")
  425. headersDev := []string{"Name", "DeviceType", "ExternalID", "LocationID", "Status", "SourceID", "AttributeMapping"}
  426. for i, h := range headersDev {
  427. cell, _ := excelize.CoordinatesToCellName(i+1, 1)
  428. fDevices.SetCellValue("Devices", cell, h)
  429. }
  430. for i, d := range devices {
  431. row := i + 2
  432. // AttributeMapping 是 JSONB 类型,转为字符串
  433. fDevices.SetCellValue("Devices", fmt.Sprintf("A%d", row), d.Name)
  434. fDevices.SetCellValue("Devices", fmt.Sprintf("B%d", row), d.DeviceType)
  435. fDevices.SetCellValue("Devices", fmt.Sprintf("C%d", row), d.ExternalID)
  436. fDevices.SetCellValue("Devices", fmt.Sprintf("D%d", row), d.LocationID)
  437. fDevices.SetCellValue("Devices", fmt.Sprintf("E%d", row), d.Status)
  438. fDevices.SetCellValue("Devices", fmt.Sprintf("F%d", row), d.SourceID)
  439. fDevices.SetCellValue("Devices", fmt.Sprintf("G%d", row), string(d.AttributeMapping))
  440. }
  441. if err := writeExcel("devices.xlsx", fDevices); err != nil {
  442. s.logBackupError(&backupLog, fmt.Sprintf("Failed to write devices excel: %v", err))
  443. return
  444. }
  445. // 3.3 Cleaning Templates (清洗模版)
  446. // 格式参考前端: 模板名称, 设备类型, 描述, 清洗公式配置
  447. fTpl := excelize.NewFile()
  448. fTpl.SetSheetName("Sheet1", "Sheet1")
  449. headersTpl := []string{"模板名称", "设备类型", "描述", "清洗公式配置"}
  450. for i, h := range headersTpl {
  451. cell, _ := excelize.CoordinatesToCellName(i+1, 1)
  452. fTpl.SetCellValue("Sheet1", cell, h)
  453. }
  454. for i, t := range templates {
  455. row := i + 2
  456. fTpl.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), t.Name)
  457. fTpl.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), t.EquipmentType)
  458. fTpl.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), t.Description)
  459. fTpl.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), string(t.Formula))
  460. }
  461. if err := writeExcel("cleaning_templates.xlsx", fTpl); err != nil {
  462. s.logBackupError(&backupLog, fmt.Sprintf("Failed to write templates excel: %v", err))
  463. return
  464. }
  465. // 3.4 Locations (位置)
  466. // 前端暂无明确导出,使用通用字段
  467. fLoc := excelize.NewFile()
  468. fLoc.SetSheetName("Sheet1", "Locations")
  469. headersLoc := []string{"ID", "Name", "ParentID", "Type"}
  470. for i, h := range headersLoc {
  471. cell, _ := excelize.CoordinatesToCellName(i+1, 1)
  472. fLoc.SetCellValue("Locations", cell, h)
  473. }
  474. for i, l := range locations {
  475. row := i + 2
  476. fLoc.SetCellValue("Locations", fmt.Sprintf("A%d", row), l.ID)
  477. fLoc.SetCellValue("Locations", fmt.Sprintf("B%d", row), l.Name)
  478. fLoc.SetCellValue("Locations", fmt.Sprintf("C%d", row), l.ParentID)
  479. fLoc.SetCellValue("Locations", fmt.Sprintf("D%d", row), l.Type)
  480. }
  481. if err := writeExcel("sys_locations.xlsx", fLoc); err != nil {
  482. s.logBackupError(&backupLog, fmt.Sprintf("Failed to write locations excel: %v", err))
  483. return
  484. }
  485. // 关闭 Zip Writer 以确保所有数据写入文件
  486. if err := archive.Close(); err != nil {
  487. s.logBackupError(&backupLog, fmt.Sprintf("Failed to close archive: %v", err))
  488. return
  489. }
  490. // Note: zipFile.Close() is deferred
  491. // 3. 更新日志并上传
  492. info, err := os.Stat(filePath)
  493. if err == nil {
  494. backupLog.Size = info.Size()
  495. }
  496. backupLog.FilePath = fileName // 保存文件名作为相对路径
  497. backupLog.Status = "SUCCESS"
  498. backupLog.Message = "Resource backup (Excel) created successfully."
  499. backupLog.EndTime = time.Now()
  500. models.DB.Save(&backupLog)
  501. // 4. 上传到 MinIO (复用现有的上传逻辑)
  502. config, _ := s.GetConfig()
  503. if config.Endpoint != "" && config.Bucket != "" {
  504. err := s.uploadToMinIO(filePath, fileName, config)
  505. if err != nil {
  506. log.Printf("MinIO upload failed: %v", err)
  507. backupLog.Message += fmt.Sprintf(" Upload failed: %v", err)
  508. backupLog.UploadStatus = "FAILED"
  509. } else {
  510. backupLog.UploadStatus = "UPLOADED"
  511. backupLog.Message += " Uploaded to MinIO."
  512. }
  513. models.DB.Save(&backupLog)
  514. }
  515. }
  516. // 辅助方法: 记录错误
  517. func (s *BackupService) logBackupError(log *models.BackupLog, msg string) {
  518. log.Status = "FAILED"
  519. log.Message = msg
  520. log.EndTime = time.Now()
  521. models.DB.Save(log)
  522. }