feat: v2.0 Sprint 1 - Backup Verification & Retention Policy
- 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
This commit is contained in:
152
cmd/cleanup.go
Normal file
152
cmd/cleanup.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dbbackup/internal/metadata"
|
||||
"dbbackup/internal/retention"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cleanupCmd = &cobra.Command{
|
||||
Use: "cleanup [backup-directory]",
|
||||
Short: "Clean up old backups based on retention policy",
|
||||
Long: `Remove old backup files based on retention policy while maintaining minimum backup count.
|
||||
|
||||
The retention policy ensures:
|
||||
1. Backups older than --retention-days are eligible for deletion
|
||||
2. At least --min-backups most recent backups are always kept
|
||||
3. Both conditions must be met for deletion
|
||||
|
||||
Examples:
|
||||
# Clean up backups older than 30 days (keep at least 5)
|
||||
dbbackup cleanup /backups --retention-days 30 --min-backups 5
|
||||
|
||||
# Dry run to see what would be deleted
|
||||
dbbackup cleanup /backups --retention-days 7 --dry-run
|
||||
|
||||
# Clean up specific database backups only
|
||||
dbbackup cleanup /backups --pattern "mydb_*.dump"
|
||||
|
||||
# Aggressive cleanup (keep only 3 most recent)
|
||||
dbbackup cleanup /backups --retention-days 1 --min-backups 3`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCleanup,
|
||||
}
|
||||
|
||||
var (
|
||||
retentionDays int
|
||||
minBackups int
|
||||
dryRun bool
|
||||
cleanupPattern string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cleanupCmd)
|
||||
cleanupCmd.Flags().IntVar(&retentionDays, "retention-days", 30, "Delete backups older than this many days")
|
||||
cleanupCmd.Flags().IntVar(&minBackups, "min-backups", 5, "Always keep at least this many backups")
|
||||
cleanupCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be deleted without actually deleting")
|
||||
cleanupCmd.Flags().StringVar(&cleanupPattern, "pattern", "", "Only clean up backups matching this pattern (e.g., 'mydb_*.dump')")
|
||||
}
|
||||
|
||||
func runCleanup(cmd *cobra.Command, args []string) error {
|
||||
backupDir := args[0]
|
||||
|
||||
// Validate directory exists
|
||||
if !dirExists(backupDir) {
|
||||
return fmt.Errorf("backup directory does not exist: %s", backupDir)
|
||||
}
|
||||
|
||||
// Create retention policy
|
||||
policy := retention.Policy{
|
||||
RetentionDays: retentionDays,
|
||||
MinBackups: minBackups,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
fmt.Printf("🗑️ Cleanup Policy:\n")
|
||||
fmt.Printf(" Directory: %s\n", backupDir)
|
||||
fmt.Printf(" Retention: %d days\n", policy.RetentionDays)
|
||||
fmt.Printf(" Min backups: %d\n", policy.MinBackups)
|
||||
if cleanupPattern != "" {
|
||||
fmt.Printf(" Pattern: %s\n", cleanupPattern)
|
||||
}
|
||||
if dryRun {
|
||||
fmt.Printf(" Mode: DRY RUN (no files will be deleted)\n")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
var result *retention.CleanupResult
|
||||
var err error
|
||||
|
||||
// Apply policy
|
||||
if cleanupPattern != "" {
|
||||
result, err = retention.CleanupByPattern(backupDir, cleanupPattern, policy)
|
||||
} else {
|
||||
result, err = retention.ApplyPolicy(backupDir, policy)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cleanup failed: %w", err)
|
||||
}
|
||||
|
||||
// Display results
|
||||
fmt.Printf("📊 Results:\n")
|
||||
fmt.Printf(" Total backups: %d\n", result.TotalBackups)
|
||||
fmt.Printf(" Eligible for deletion: %d\n", result.EligibleForDeletion)
|
||||
|
||||
if len(result.Deleted) > 0 {
|
||||
fmt.Printf("\n")
|
||||
if dryRun {
|
||||
fmt.Printf("🔍 Would delete %d backup(s):\n", len(result.Deleted))
|
||||
} else {
|
||||
fmt.Printf("✅ Deleted %d backup(s):\n", len(result.Deleted))
|
||||
}
|
||||
for _, file := range result.Deleted {
|
||||
fmt.Printf(" - %s\n", filepath.Base(file))
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Kept) > 0 && len(result.Kept) <= 10 {
|
||||
fmt.Printf("\n📦 Kept %d backup(s):\n", len(result.Kept))
|
||||
for _, file := range result.Kept {
|
||||
fmt.Printf(" - %s\n", filepath.Base(file))
|
||||
}
|
||||
} else if len(result.Kept) > 10 {
|
||||
fmt.Printf("\n📦 Kept %d backup(s)\n", len(result.Kept))
|
||||
}
|
||||
|
||||
if !dryRun && result.SpaceFreed > 0 {
|
||||
fmt.Printf("\n💾 Space freed: %s\n", metadata.FormatSize(result.SpaceFreed))
|
||||
}
|
||||
|
||||
if len(result.Errors) > 0 {
|
||||
fmt.Printf("\n⚠️ Errors:\n")
|
||||
for _, err := range result.Errors {
|
||||
fmt.Printf(" - %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
|
||||
if dryRun {
|
||||
fmt.Println("✅ Dry run completed (no files were deleted)")
|
||||
} else if len(result.Deleted) > 0 {
|
||||
fmt.Println("✅ Cleanup completed successfully")
|
||||
} else {
|
||||
fmt.Println("ℹ️ No backups eligible for deletion")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
141
cmd/verify.go
Normal file
141
cmd/verify.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dbbackup/internal/metadata"
|
||||
"dbbackup/internal/verification"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var verifyBackupCmd = &cobra.Command{
|
||||
Use: "verify-backup [backup-file]",
|
||||
Short: "Verify backup file integrity with checksums",
|
||||
Long: `Verify the integrity of one or more backup files by comparing their SHA-256 checksums
|
||||
against the stored metadata. This ensures that backups have not been corrupted.
|
||||
|
||||
Examples:
|
||||
# Verify a single backup
|
||||
dbbackup verify-backup /backups/mydb_20260115.dump
|
||||
|
||||
# Verify all backups in a directory
|
||||
dbbackup verify-backup /backups/*.dump
|
||||
|
||||
# Quick verification (size check only, no checksum)
|
||||
dbbackup verify-backup /backups/mydb.dump --quick
|
||||
|
||||
# Verify and show detailed information
|
||||
dbbackup verify-backup /backups/mydb.dump --verbose`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runVerifyBackup,
|
||||
}
|
||||
|
||||
var (
|
||||
quickVerify bool
|
||||
verboseVerify bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(verifyBackupCmd)
|
||||
verifyBackupCmd.Flags().BoolVar(&quickVerify, "quick", false, "Quick verification (size check only)")
|
||||
verifyBackupCmd.Flags().BoolVarP(&verboseVerify, "verbose", "v", false, "Show detailed information")
|
||||
}
|
||||
|
||||
func runVerifyBackup(cmd *cobra.Command, args []string) error {
|
||||
// Expand glob patterns
|
||||
var backupFiles []string
|
||||
for _, pattern := range args {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern %s: %w", pattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
// Not a glob, use as-is
|
||||
backupFiles = append(backupFiles, pattern)
|
||||
} else {
|
||||
backupFiles = append(backupFiles, matches...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(backupFiles) == 0 {
|
||||
return fmt.Errorf("no backup files found")
|
||||
}
|
||||
|
||||
fmt.Printf("Verifying %d backup file(s)...\n\n", len(backupFiles))
|
||||
|
||||
successCount := 0
|
||||
failureCount := 0
|
||||
|
||||
for _, backupFile := range backupFiles {
|
||||
// Skip metadata files
|
||||
if strings.HasSuffix(backupFile, ".meta.json") ||
|
||||
strings.HasSuffix(backupFile, ".sha256") ||
|
||||
strings.HasSuffix(backupFile, ".info") {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("📁 %s\n", filepath.Base(backupFile))
|
||||
|
||||
if quickVerify {
|
||||
// Quick check: size only
|
||||
err := verification.QuickCheck(backupFile)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ FAILED: %v\n\n", err)
|
||||
failureCount++
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" ✅ VALID (quick check)\n\n")
|
||||
successCount++
|
||||
} else {
|
||||
// Full verification with SHA-256
|
||||
result, err := verification.Verify(backupFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verification error: %w", err)
|
||||
}
|
||||
|
||||
if result.Valid {
|
||||
fmt.Printf(" ✅ VALID\n")
|
||||
if verboseVerify {
|
||||
meta, _ := metadata.Load(backupFile)
|
||||
fmt.Printf(" Size: %s\n", metadata.FormatSize(meta.SizeBytes))
|
||||
fmt.Printf(" SHA-256: %s\n", meta.SHA256)
|
||||
fmt.Printf(" Database: %s (%s)\n", meta.Database, meta.DatabaseType)
|
||||
fmt.Printf(" Created: %s\n", meta.Timestamp.Format(time.RFC3339))
|
||||
}
|
||||
fmt.Println()
|
||||
successCount++
|
||||
} else {
|
||||
fmt.Printf(" ❌ FAILED: %v\n", result.Error)
|
||||
if verboseVerify {
|
||||
if !result.FileExists {
|
||||
fmt.Printf(" File does not exist\n")
|
||||
} else if !result.MetadataExists {
|
||||
fmt.Printf(" Metadata file missing\n")
|
||||
} else if !result.SizeMatch {
|
||||
fmt.Printf(" Size mismatch\n")
|
||||
} else {
|
||||
fmt.Printf(" Expected: %s\n", result.ExpectedSHA256)
|
||||
fmt.Printf(" Got: %s\n", result.CalculatedSHA256)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
failureCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
fmt.Println(strings.Repeat("─", 50))
|
||||
fmt.Printf("Total: %d backups\n", len(backupFiles))
|
||||
fmt.Printf("✅ Valid: %d\n", successCount)
|
||||
if failureCount > 0 {
|
||||
fmt.Printf("❌ Failed: %d\n", failureCount)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user