package database import ( domain "RenewCMS/internal/domain/update" "errors" "os" "path/filepath" "sort" "strings" "time" "gorm.io/gorm" ) var errBackupExists = errors.New("a backup with this name already exists") const ( backupsToKeep = 3 backupFileExtension = ".db" backupFilePrefix = "renewcms-" backupTimeLayout = "20060102T150405.000000000Z" ) type SQLiteBackup struct { db *gorm.DB directory string now func() time.Time } var _ domain.DatabaseBackup = &SQLiteBackup{} func NewSQLiteBackup(db *gorm.DB, directory string, now func() time.Time) *SQLiteBackup { return &SQLiteBackup{db: db, directory: directory, now: now} } func (b *SQLiteBackup) Backup() (string, error) { if err := os.MkdirAll(b.directory, os.ModePerm); err != nil { return "", err } target := filepath.Join(b.directory, backupFilePrefix+b.now().UTC().Format(backupTimeLayout)+backupFileExtension) if _, err := os.Stat(target); err == nil { return "", errBackupExists } if err := b.db.Exec("VACUUM INTO ?", target).Error; err != nil { _ = os.Remove(target) return "", err } if err := b.prune(); err != nil { return "", err } return target, nil } type backupFile struct { path string modified time.Time } func (b *SQLiteBackup) prune() error { entries, err := os.ReadDir(b.directory) if err != nil { return err } files := make([]backupFile, 0, len(entries)) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), backupFileExtension) { continue } info, err := entry.Info() if err != nil { return err } files = append(files, backupFile{path: filepath.Join(b.directory, entry.Name()), modified: info.ModTime()}) } sort.Slice(files, func(i, j int) bool { if files[i].modified.Equal(files[j].modified) { return files[i].path > files[j].path } return files[i].modified.After(files[j].modified) }) for _, stale := range files[min(backupsToKeep, len(files)):] { if err := os.Remove(stale.path); err != nil { return err } } return nil }