feat: implement initial GoDump backup service with management web UI and MySQL support
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"godump/config"
|
||||
"godump/logger"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type DBStatus struct {
|
||||
Name string `json:"name"`
|
||||
FirstDiscovered time.Time `json:"first_discovered"`
|
||||
LastBackupTime time.Time `json:"last_backup_time"`
|
||||
LastBackupSize int64 `json:"last_backup_size"`
|
||||
LastBackupResult string `json:"last_backup_result"` // success, skipped, failed
|
||||
}
|
||||
|
||||
type InstanceStatus struct {
|
||||
Config config.InstanceConfig
|
||||
LastRunTime time.Time
|
||||
NextRunTime time.Time
|
||||
OverallResult string // success, partial, failed, running
|
||||
Databases map[string]*DBStatus
|
||||
IsRunning bool
|
||||
CronEntryID cron.EntryID
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type DBStatusSnapshot struct {
|
||||
Name string `json:"name"`
|
||||
FirstDiscovered time.Time `json:"first_discovered"`
|
||||
LastBackupTime time.Time `json:"last_backup_time"`
|
||||
LastBackupSize int64 `json:"last_backup_size"`
|
||||
LastBackupResult string `json:"last_backup_result"`
|
||||
}
|
||||
|
||||
type InstanceSnapshot struct {
|
||||
Name string
|
||||
Host string
|
||||
LastRunTime time.Time
|
||||
NextRunTime time.Time
|
||||
OverallResult string
|
||||
IsRunning bool
|
||||
Databases []DBStatusSnapshot
|
||||
}
|
||||
|
||||
func (s *InstanceStatus) Snapshot() InstanceSnapshot {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
snap := InstanceSnapshot{
|
||||
Name: s.Config.Name,
|
||||
Host: s.Config.Host,
|
||||
LastRunTime: s.LastRunTime,
|
||||
NextRunTime: s.NextRunTime,
|
||||
OverallResult: s.OverallResult,
|
||||
IsRunning: s.IsRunning,
|
||||
Databases: make([]DBStatusSnapshot, 0, len(s.Databases)),
|
||||
}
|
||||
|
||||
for _, db := range s.Databases {
|
||||
snap.Databases = append(snap.Databases, DBStatusSnapshot{
|
||||
Name: db.Name,
|
||||
FirstDiscovered: db.FirstDiscovered,
|
||||
LastBackupTime: db.LastBackupTime,
|
||||
LastBackupSize: db.LastBackupSize,
|
||||
LastBackupResult: db.LastBackupResult,
|
||||
})
|
||||
}
|
||||
|
||||
return snap
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
instances map[string]*InstanceStatus
|
||||
cron *cron.Cron
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManager(cfg *config.Config) *Manager {
|
||||
c := cron.New()
|
||||
c.Start()
|
||||
|
||||
m := &Manager{
|
||||
instances: make(map[string]*InstanceStatus),
|
||||
cron: c,
|
||||
}
|
||||
|
||||
for _, instCfg := range cfg.Instances {
|
||||
status := &InstanceStatus{
|
||||
Config: instCfg,
|
||||
Databases: make(map[string]*DBStatus),
|
||||
}
|
||||
m.instances[instCfg.Name] = status
|
||||
|
||||
// Schedule cron job
|
||||
if instCfg.Schedule != "" {
|
||||
var id cron.EntryID
|
||||
id, err := c.AddFunc(instCfg.Schedule, func(name string) func() {
|
||||
return func() {
|
||||
m.RunInstance(name)
|
||||
}
|
||||
}(instCfg.Name))
|
||||
|
||||
if err != nil {
|
||||
logger.Error(instCfg.Name, "Failed to schedule cron job: %v", err)
|
||||
} else {
|
||||
status.CronEntryID = id
|
||||
status.NextRunTime = c.Entry(id).Next
|
||||
logger.Info(instCfg.Name, "Scheduled backups, next run at %v", status.NextRunTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) GetInstances() []*InstanceStatus {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var result []*InstanceStatus
|
||||
for _, status := range m.instances {
|
||||
result = append(result, status)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Manager) GetInstance(name string) *InstanceStatus {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.instances[name]
|
||||
}
|
||||
|
||||
func (m *Manager) DiscoverInitial() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for name, inst := range m.instances {
|
||||
dbs, err := discoverDatabases(inst.Config)
|
||||
if err != nil {
|
||||
logger.Error(name, "Initial database discovery failed: %v", err)
|
||||
inst.mu.Lock()
|
||||
inst.OverallResult = "failed"
|
||||
inst.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
inst.mu.Lock()
|
||||
var latestInstanceTime time.Time
|
||||
var hasAnyBackup bool
|
||||
|
||||
for _, db := range dbs {
|
||||
if _, exists := inst.Databases[db]; !exists {
|
||||
var lastTime time.Time
|
||||
var lastSize int64
|
||||
var lastResult string
|
||||
|
||||
dbPath := filepath.Join(inst.Config.BackupDir, db)
|
||||
if entries, err := os.ReadDir(dbPath); err == nil {
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if info, err := e.Info(); err == nil {
|
||||
if info.ModTime().After(lastTime) {
|
||||
lastTime = info.ModTime()
|
||||
lastSize = info.Size()
|
||||
lastResult = "success"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inst.Databases[db] = &DBStatus{
|
||||
Name: db,
|
||||
FirstDiscovered: time.Now(),
|
||||
LastBackupTime: lastTime,
|
||||
LastBackupSize: lastSize,
|
||||
LastBackupResult: lastResult,
|
||||
}
|
||||
if lastTime.After(latestInstanceTime) {
|
||||
latestInstanceTime = lastTime
|
||||
}
|
||||
if lastResult != "" {
|
||||
hasAnyBackup = true
|
||||
}
|
||||
logger.Info(name, "Discovered initial database: %s", db)
|
||||
}
|
||||
}
|
||||
|
||||
if latestInstanceTime.After(inst.LastRunTime) {
|
||||
inst.LastRunTime = latestInstanceTime
|
||||
}
|
||||
if hasAnyBackup && inst.OverallResult == "" {
|
||||
inst.OverallResult = "success"
|
||||
}
|
||||
inst.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) RunAll() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for name := range m.instances {
|
||||
go m.RunInstance(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) RunInstance(name string) {
|
||||
inst := m.GetInstance(name)
|
||||
if inst == nil {
|
||||
return
|
||||
}
|
||||
|
||||
inst.mu.Lock()
|
||||
if inst.IsRunning {
|
||||
inst.mu.Unlock()
|
||||
logger.Warn(name, "Backup job already running, skipping")
|
||||
return
|
||||
}
|
||||
inst.IsRunning = true
|
||||
inst.OverallResult = "running"
|
||||
inst.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
inst.mu.Lock()
|
||||
inst.IsRunning = false
|
||||
inst.LastRunTime = time.Now()
|
||||
if inst.CronEntryID != 0 {
|
||||
inst.NextRunTime = m.cron.Entry(inst.CronEntryID).Next
|
||||
}
|
||||
inst.mu.Unlock()
|
||||
}()
|
||||
|
||||
logger.Info(name, "Starting backup job")
|
||||
|
||||
// 1. Discovery
|
||||
dbs, err := discoverDatabases(inst.Config)
|
||||
if err != nil {
|
||||
logger.Error(name, "Database discovery failed: %v", err)
|
||||
inst.mu.Lock()
|
||||
inst.OverallResult = "failed"
|
||||
inst.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
inst.mu.Lock()
|
||||
for _, db := range dbs {
|
||||
if _, exists := inst.Databases[db]; !exists {
|
||||
inst.Databases[db] = &DBStatus{
|
||||
Name: db,
|
||||
FirstDiscovered: time.Now(),
|
||||
}
|
||||
logger.Info(name, "Discovered new database: %s", db)
|
||||
}
|
||||
}
|
||||
inst.mu.Unlock()
|
||||
|
||||
// 2. Backup Execution
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
// We only backup the discovered databases. If a DB disappeared, it will not be in `dbs`.
|
||||
for _, db := range dbs {
|
||||
logger.Info(name, "Starting backup for database %s", db)
|
||||
start := time.Now()
|
||||
size, err := backupDatabase(inst.Config, db)
|
||||
duration := time.Since(start)
|
||||
|
||||
inst.mu.Lock()
|
||||
dbStatus := inst.Databases[db]
|
||||
dbStatus.LastBackupTime = time.Now()
|
||||
if err != nil {
|
||||
logger.Error(name, "Failed backup for database %s: %v", db, err)
|
||||
dbStatus.LastBackupResult = "failed"
|
||||
failedCount++
|
||||
} else {
|
||||
logger.Info(name, "Completed backup for database %s in %v, size %d bytes", db, duration, size)
|
||||
dbStatus.LastBackupResult = "success"
|
||||
dbStatus.LastBackupSize = size
|
||||
successCount++
|
||||
}
|
||||
inst.mu.Unlock()
|
||||
}
|
||||
|
||||
// 3. Retention Enforcement
|
||||
logger.Info(name, "Running retention policy cleanup (keep %d days)", inst.Config.RetentionDays)
|
||||
deleted, err := enforceRetention(inst.Config)
|
||||
if err != nil {
|
||||
logger.Error(name, "Retention cleanup encountered errors: %v", err)
|
||||
} else {
|
||||
logger.Info(name, "Retention cleanup finished, deleted %d files", deleted)
|
||||
}
|
||||
|
||||
inst.mu.Lock()
|
||||
if failedCount == 0 {
|
||||
inst.OverallResult = "success"
|
||||
} else if successCount == 0 {
|
||||
inst.OverallResult = "failed"
|
||||
} else {
|
||||
inst.OverallResult = "partial"
|
||||
}
|
||||
inst.mu.Unlock()
|
||||
|
||||
logger.Info(name, "Backup job completed. Result: %s", inst.OverallResult)
|
||||
}
|
||||
Reference in New Issue
Block a user