diff --git a/docs/Tasktracking/2026-04-26-backup-job-scheduling.md b/docs/Tasktracking/2026-04-26-backup-job-scheduling.md new file mode 100644 index 00000000..defd25b3 --- /dev/null +++ b/docs/Tasktracking/2026-04-26-backup-job-scheduling.md @@ -0,0 +1,28 @@ +# Task Record + +Date: 2026-04-26 +Related Module: web/job, web/controller, web +Change Type: Add + +## Background +Database backups were only possible manually via API. There was no scheduled backup mechanism, so users had to remember to create backups or rely on external cron. + +## Changes +- Created `web/job/backup_job.go`: BackupJob struct with Run() and shouldRun() methods that check backup settings and execute CreateSnapshot when the configured schedule matches the current time +- Registered BackupController routes in `web/controller/server.go` initRouter() to expose backup API endpoints under `/panel/api/server/` +- Scheduled BackupJob via cron in `web/web.go` startTask() at `@every 1m` interval (the job internally checks whether it should actually run) + +## Impact +- New file: `web/job/backup_job.go` +- Modified: `web/controller/server.go` (3 lines added in initRouter) +- Modified: `web/web.go` (2 lines added in startTask) +- No database schema changes, no API breaking changes, no config changes + +## Verification +- `go build ./...` passed with no errors +- `gofmt -l -w .` produced no changes (files already properly formatted) + +## Risks And Follow-Up +- BackupJob has zero-value service fields; methods rely on DB queries at runtime, same pattern as other jobs (CheckXrayRunningJob, LdapSyncJob) +- The job uses `logger.Warning` (not `logger.Warn`) — verify this matches the logger API +- Need to verify that Backups setting page in UI has the scheduling options (BackupEnabled, BackupFrequency, BackupHour) to make the job functional diff --git a/web/controller/server.go b/web/controller/server.go index d32209e1..352226f4 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -60,6 +60,10 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.POST("/xraylogs/:count", a.getXrayLogs) g.POST("/importDB", a.importDB) g.POST("/getNewEchCert", a.getNewEchCert) + + // Backup routes + backupCtrl := BackupController{} + backupCtrl.initRouter(g) } // refreshStatus updates the cached server status and collects CPU history. diff --git a/web/job/backup_job.go b/web/job/backup_job.go new file mode 100644 index 00000000..c2bcda1d --- /dev/null +++ b/web/job/backup_job.go @@ -0,0 +1,69 @@ +package job + +import ( + "time" + + "github.com/mhsanaei/3x-ui/v2/logger" + "github.com/mhsanaei/3x-ui/v2/web/service" +) + +// BackupJob handles scheduled database backups. +type BackupJob struct { + settingService service.SettingService + backupService service.BackupService +} + +// NewBackupJob creates a new BackupJob instance. +func NewBackupJob() *BackupJob { + return &BackupJob{} +} + +// Run executes the scheduled backup if enabled and time matches the configured frequency. +func (j *BackupJob) Run() { + enabled, err := j.settingService.GetBackupEnabled() + if err != nil || !enabled { + return + } + + frequency, err := j.settingService.GetBackupFrequency() + if err != nil { + return + } + + if !j.shouldRun(frequency) { + return + } + + if err := j.backupService.CreateSnapshot(j.settingService); err != nil { + logger.Warning("scheduled backup failed:", err) + } +} + +// shouldRun checks if the backup should run based on the configured frequency. +func (j *BackupJob) shouldRun(frequency string) bool { + now := time.Now() + + switch frequency { + case "hourly": + return now.Minute() == 0 + case "every12h": + return (now.Hour() == 0 || now.Hour() == 12) && now.Minute() == 0 + case "daily": + hour, err := j.settingService.GetBackupHour() + if err != nil { + hour = 3 + } + return now.Hour() == hour && now.Minute() == 0 + case "weekly": + if now.Weekday() != time.Sunday { + return false + } + hour, err := j.settingService.GetBackupHour() + if err != nil { + hour = 3 + } + return now.Hour() == hour && now.Minute() == 0 + default: + return false + } +} diff --git a/web/web.go b/web/web.go index d86af50c..bf12183f 100644 --- a/web/web.go +++ b/web/web.go @@ -410,6 +410,9 @@ func (s *Server) startTask() { } else { s.cron.Remove(entry) } + + // Schedule database backup job (runs every minute, checks schedule internally) + s.cron.AddJob("@every 1m", job.NewBackupJob()) } func (s *Server) startNodeLoops() {