- Add SHA-256 checksum generation for all backups - Implement verify-backup command for integrity validation - Add JSON metadata format (.meta.json) with full backup info - Create retention policy engine with smart cleanup - Add cleanup command with dry-run and pattern matching - Integrate metadata generation into backup flow - Maintain backward compatibility with legacy .info files New commands: - dbbackup verify-backup [files] - Verify backup integrity - dbbackup cleanup [dir] - Clean old backups with retention policy New packages: - internal/metadata - Backup metadata management - internal/verification - Checksum validation - internal/retention - Retention policy engine
225 lines
5.7 KiB
Go
225 lines
5.7 KiB
Go
package retention
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
|
|
"dbbackup/internal/metadata"
|
|
)
|
|
|
|
// Policy defines the retention rules
|
|
type Policy struct {
|
|
RetentionDays int
|
|
MinBackups int
|
|
DryRun bool
|
|
}
|
|
|
|
// CleanupResult contains information about cleanup operations
|
|
type CleanupResult struct {
|
|
TotalBackups int
|
|
EligibleForDeletion int
|
|
Deleted []string
|
|
Kept []string
|
|
SpaceFreed int64
|
|
Errors []error
|
|
}
|
|
|
|
// ApplyPolicy enforces the retention policy on backups in a directory
|
|
func ApplyPolicy(backupDir string, policy Policy) (*CleanupResult, error) {
|
|
result := &CleanupResult{
|
|
Deleted: make([]string, 0),
|
|
Kept: make([]string, 0),
|
|
Errors: make([]error, 0),
|
|
}
|
|
|
|
// List all backups in directory
|
|
backups, err := metadata.ListBackups(backupDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list backups: %w", err)
|
|
}
|
|
|
|
result.TotalBackups = len(backups)
|
|
|
|
// Sort backups by timestamp (oldest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Timestamp.Before(backups[j].Timestamp)
|
|
})
|
|
|
|
// Calculate cutoff date
|
|
cutoffDate := time.Now().AddDate(0, 0, -policy.RetentionDays)
|
|
|
|
// Determine which backups to delete
|
|
for i, backup := range backups {
|
|
// Always keep minimum number of backups (most recent ones)
|
|
backupsRemaining := len(backups) - i
|
|
if backupsRemaining <= policy.MinBackups {
|
|
result.Kept = append(result.Kept, backup.BackupFile)
|
|
continue
|
|
}
|
|
|
|
// Check if backup is older than retention period
|
|
if backup.Timestamp.Before(cutoffDate) {
|
|
result.EligibleForDeletion++
|
|
|
|
if policy.DryRun {
|
|
result.Deleted = append(result.Deleted, backup.BackupFile)
|
|
} else {
|
|
// Delete backup file and associated metadata
|
|
if err := deleteBackup(backup.BackupFile); err != nil {
|
|
result.Errors = append(result.Errors,
|
|
fmt.Errorf("failed to delete %s: %w", backup.BackupFile, err))
|
|
} else {
|
|
result.Deleted = append(result.Deleted, backup.BackupFile)
|
|
result.SpaceFreed += backup.SizeBytes
|
|
}
|
|
}
|
|
} else {
|
|
result.Kept = append(result.Kept, backup.BackupFile)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// deleteBackup removes a backup file and all associated files
|
|
func deleteBackup(backupFile string) error {
|
|
// Delete main backup file
|
|
if err := os.Remove(backupFile); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to delete backup file: %w", err)
|
|
}
|
|
|
|
// Delete metadata file
|
|
metaFile := backupFile + ".meta.json"
|
|
if err := os.Remove(metaFile); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to delete metadata file: %w", err)
|
|
}
|
|
|
|
// Delete legacy .sha256 file if exists
|
|
sha256File := backupFile + ".sha256"
|
|
if err := os.Remove(sha256File); err != nil && !os.IsNotExist(err) {
|
|
// Don't fail if .sha256 doesn't exist (new format)
|
|
}
|
|
|
|
// Delete legacy .info file if exists
|
|
infoFile := backupFile + ".info"
|
|
if err := os.Remove(infoFile); err != nil && !os.IsNotExist(err) {
|
|
// Don't fail if .info doesn't exist (new format)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOldestBackups returns the N oldest backups in a directory
|
|
func GetOldestBackups(backupDir string, count int) ([]*metadata.BackupMetadata, error) {
|
|
backups, err := metadata.ListBackups(backupDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sort by timestamp (oldest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Timestamp.Before(backups[j].Timestamp)
|
|
})
|
|
|
|
if count > len(backups) {
|
|
count = len(backups)
|
|
}
|
|
|
|
return backups[:count], nil
|
|
}
|
|
|
|
// GetNewestBackups returns the N newest backups in a directory
|
|
func GetNewestBackups(backupDir string, count int) ([]*metadata.BackupMetadata, error) {
|
|
backups, err := metadata.ListBackups(backupDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sort by timestamp (newest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Timestamp.After(backups[j].Timestamp)
|
|
})
|
|
|
|
if count > len(backups) {
|
|
count = len(backups)
|
|
}
|
|
|
|
return backups[:count], nil
|
|
}
|
|
|
|
// CleanupByPattern removes backups matching a specific pattern
|
|
func CleanupByPattern(backupDir, pattern string, policy Policy) (*CleanupResult, error) {
|
|
result := &CleanupResult{
|
|
Deleted: make([]string, 0),
|
|
Kept: make([]string, 0),
|
|
Errors: make([]error, 0),
|
|
}
|
|
|
|
// Find matching backup files
|
|
searchPattern := filepath.Join(backupDir, pattern)
|
|
matches, err := filepath.Glob(searchPattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to match pattern: %w", err)
|
|
}
|
|
|
|
// Filter to only .dump or .sql files
|
|
var backupFiles []string
|
|
for _, match := range matches {
|
|
ext := filepath.Ext(match)
|
|
if ext == ".dump" || ext == ".sql" {
|
|
backupFiles = append(backupFiles, match)
|
|
}
|
|
}
|
|
|
|
// Load metadata for matched backups
|
|
var backups []*metadata.BackupMetadata
|
|
for _, file := range backupFiles {
|
|
meta, err := metadata.Load(file)
|
|
if err != nil {
|
|
// Skip files without metadata
|
|
continue
|
|
}
|
|
backups = append(backups, meta)
|
|
}
|
|
|
|
result.TotalBackups = len(backups)
|
|
|
|
// Sort by timestamp
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Timestamp.Before(backups[j].Timestamp)
|
|
})
|
|
|
|
cutoffDate := time.Now().AddDate(0, 0, -policy.RetentionDays)
|
|
|
|
// Apply policy
|
|
for i, backup := range backups {
|
|
backupsRemaining := len(backups) - i
|
|
if backupsRemaining <= policy.MinBackups {
|
|
result.Kept = append(result.Kept, backup.BackupFile)
|
|
continue
|
|
}
|
|
|
|
if backup.Timestamp.Before(cutoffDate) {
|
|
result.EligibleForDeletion++
|
|
|
|
if policy.DryRun {
|
|
result.Deleted = append(result.Deleted, backup.BackupFile)
|
|
} else {
|
|
if err := deleteBackup(backup.BackupFile); err != nil {
|
|
result.Errors = append(result.Errors, err)
|
|
} else {
|
|
result.Deleted = append(result.Deleted, backup.BackupFile)
|
|
result.SpaceFreed += backup.SizeBytes
|
|
}
|
|
}
|
|
} else {
|
|
result.Kept = append(result.Kept, backup.BackupFile)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|