feat: v2.0 Sprint 3 - Cloud URI Support & Command Integration (Part 1)
Sprint 3 Implementation - Cloud URI Support: New Features: ✅ Cloud URI parser (s3://bucket/path) ✅ Backup command with --cloud URI flag ✅ Restore from cloud URIs ✅ Verify cloud backups ✅ Cleanup cloud storage with retention policy New Files: - internal/cloud/uri.go - Cloud URI parser - internal/restore/ - Cloud download module - internal/restore/cloud_download.go - Download & verify helper Modified Commands: - cmd/backup.go - Added --cloud s3://bucket/path flag - cmd/restore.go - Auto-detect & download from cloud URIs - cmd/verify.go - Verify backups from cloud storage - cmd/cleanup.go - Apply retention policy to cloud storage URI Support: - s3://bucket/path/file.dump - AWS S3 - minio://bucket/path/file.dump - MinIO - b2://bucket/path/file.dump - Backblaze B2 - gs://bucket/path/file.dump - Google Cloud Storage Examples: # Backup with cloud URI dbbackup backup single mydb --cloud s3://my-bucket/backups/ # Restore from cloud dbbackup restore single s3://my-bucket/backups/mydb.dump --confirm # Verify cloud backup dbbackup verify-backup s3://my-bucket/backups/mydb.dump # Cleanup old cloud backups dbbackup cleanup s3://my-bucket/backups/ --retention-days 30 Features: - Automatic download to temp directory - SHA-256 verification after download - Automatic temp file cleanup - Progress tracking for downloads - Metadata synchronization - Retention policy for cloud storage Sprint 3 Part 1 COMPLETE!
This commit is contained in:
184
cmd/cleanup.go
184
cmd/cleanup.go
@@ -1,11 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dbbackup/internal/cloud"
|
||||
"dbbackup/internal/metadata"
|
||||
"dbbackup/internal/retention"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -53,7 +56,15 @@ func init() {
|
||||
}
|
||||
|
||||
func runCleanup(cmd *cobra.Command, args []string) error {
|
||||
backupDir := args[0]
|
||||
backupPath := args[0]
|
||||
|
||||
// Check if this is a cloud URI
|
||||
if isCloudURIPath(backupPath) {
|
||||
return runCloudCleanup(cmd.Context(), backupPath)
|
||||
}
|
||||
|
||||
// Local cleanup
|
||||
backupDir := backupPath
|
||||
|
||||
// Validate directory exists
|
||||
if !dirExists(backupDir) {
|
||||
@@ -150,3 +161,174 @@ func dirExists(path string) bool {
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// isCloudURIPath checks if a path is a cloud URI
|
||||
func isCloudURIPath(s string) bool {
|
||||
return cloud.IsCloudURI(s)
|
||||
}
|
||||
|
||||
// runCloudCleanup applies retention policy to cloud storage
|
||||
func runCloudCleanup(ctx context.Context, uri string) error {
|
||||
// Parse cloud URI
|
||||
cloudURI, err := cloud.ParseCloudURI(uri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cloud URI: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("☁️ Cloud Cleanup Policy:\n")
|
||||
fmt.Printf(" URI: %s\n", uri)
|
||||
fmt.Printf(" Provider: %s\n", cloudURI.Provider)
|
||||
fmt.Printf(" Bucket: %s\n", cloudURI.Bucket)
|
||||
if cloudURI.Path != "" {
|
||||
fmt.Printf(" Prefix: %s\n", cloudURI.Path)
|
||||
}
|
||||
fmt.Printf(" Retention: %d days\n", retentionDays)
|
||||
fmt.Printf(" Min backups: %d\n", minBackups)
|
||||
if dryRun {
|
||||
fmt.Printf(" Mode: DRY RUN (no files will be deleted)\n")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Create cloud backend
|
||||
cfg := cloudURI.ToConfig()
|
||||
backend, err := cloud.NewBackend(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cloud backend: %w", err)
|
||||
}
|
||||
|
||||
// List all backups
|
||||
backups, err := backend.List(ctx, cloudURI.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list cloud backups: %w", err)
|
||||
}
|
||||
|
||||
if len(backups) == 0 {
|
||||
fmt.Println("No backups found in cloud storage")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d backup(s) in cloud storage\n\n", len(backups))
|
||||
|
||||
// Filter backups based on pattern if specified
|
||||
var filteredBackups []cloud.BackupInfo
|
||||
if cleanupPattern != "" {
|
||||
for _, backup := range backups {
|
||||
matched, _ := filepath.Match(cleanupPattern, backup.Name)
|
||||
if matched {
|
||||
filteredBackups = append(filteredBackups, backup)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Pattern matched %d backup(s)\n\n", len(filteredBackups))
|
||||
} else {
|
||||
filteredBackups = backups
|
||||
}
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
// Already sorted by backend.List
|
||||
|
||||
// Calculate retention date
|
||||
cutoffDate := time.Now().AddDate(0, 0, -retentionDays)
|
||||
|
||||
// Determine which backups to delete
|
||||
var toDelete []cloud.BackupInfo
|
||||
var toKeep []cloud.BackupInfo
|
||||
|
||||
for _, backup := range filteredBackups {
|
||||
if backup.LastModified.Before(cutoffDate) {
|
||||
toDelete = append(toDelete, backup)
|
||||
} else {
|
||||
toKeep = append(toKeep, backup)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we keep minimum backups
|
||||
totalBackups := len(filteredBackups)
|
||||
if totalBackups-len(toDelete) < minBackups {
|
||||
// Need to keep more backups
|
||||
keepCount := minBackups - len(toKeep)
|
||||
if keepCount > len(toDelete) {
|
||||
keepCount = len(toDelete)
|
||||
}
|
||||
|
||||
// Move oldest from toDelete to toKeep
|
||||
for i := len(toDelete) - 1; i >= len(toDelete)-keepCount && i >= 0; i-- {
|
||||
toKeep = append(toKeep, toDelete[i])
|
||||
toDelete = toDelete[:i]
|
||||
}
|
||||
}
|
||||
|
||||
// Display results
|
||||
fmt.Printf("📊 Results:\n")
|
||||
fmt.Printf(" Total backups: %d\n", totalBackups)
|
||||
fmt.Printf(" Eligible for deletion: %d\n", len(toDelete))
|
||||
fmt.Printf(" Will keep: %d\n", len(toKeep))
|
||||
fmt.Println()
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
if dryRun {
|
||||
fmt.Printf("🔍 Would delete %d backup(s):\n", len(toDelete))
|
||||
} else {
|
||||
fmt.Printf("🗑️ Deleting %d backup(s):\n", len(toDelete))
|
||||
}
|
||||
|
||||
var totalSize int64
|
||||
var deletedCount int
|
||||
|
||||
for _, backup := range toDelete {
|
||||
fmt.Printf(" - %s (%s, %s old)\n",
|
||||
backup.Name,
|
||||
cloud.FormatSize(backup.Size),
|
||||
formatBackupAge(backup.LastModified))
|
||||
|
||||
totalSize += backup.Size
|
||||
|
||||
if !dryRun {
|
||||
if err := backend.Delete(ctx, backup.Key); err != nil {
|
||||
fmt.Printf(" ❌ Error: %v\n", err)
|
||||
} else {
|
||||
deletedCount++
|
||||
// Also try to delete metadata
|
||||
backend.Delete(ctx, backup.Key+".meta.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n💾 Space %s: %s\n",
|
||||
map[bool]string{true: "would be freed", false: "freed"}[dryRun],
|
||||
cloud.FormatSize(totalSize))
|
||||
|
||||
if !dryRun && deletedCount > 0 {
|
||||
fmt.Printf("✅ Successfully deleted %d backup(s)\n", deletedCount)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No backups eligible for deletion")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatBackupAge returns a human-readable age string from a time.Time
|
||||
func formatBackupAge(t time.Time) string {
|
||||
d := time.Since(t)
|
||||
days := int(d.Hours() / 24)
|
||||
|
||||
if days == 0 {
|
||||
return "today"
|
||||
} else if days == 1 {
|
||||
return "1 day"
|
||||
} else if days < 30 {
|
||||
return fmt.Sprintf("%d days", days)
|
||||
} else if days < 365 {
|
||||
months := days / 30
|
||||
if months == 1 {
|
||||
return "1 month"
|
||||
}
|
||||
return fmt.Sprintf("%d months", months)
|
||||
} else {
|
||||
years := days / 365
|
||||
if years == 1 {
|
||||
return "1 year"
|
||||
}
|
||||
return fmt.Sprintf("%d years", years)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user