- Replace all emoji characters with ASCII equivalents throughout codebase - Replace Unicode box-drawing characters (═║╔╗╚╝━─) with ASCII (+|-=) - Replace checkmarks (✓✗) with [OK]/[FAIL] markers - 59 files updated, 741 lines changed - Improves terminal compatibility and reduces visual noise
726 lines
19 KiB
Go
726 lines
19 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"dbbackup/internal/catalog"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
catalogDBPath string
|
|
catalogFormat string
|
|
catalogLimit int
|
|
catalogDatabase string
|
|
catalogStartDate string
|
|
catalogEndDate string
|
|
catalogInterval string
|
|
catalogVerbose bool
|
|
)
|
|
|
|
// catalogCmd represents the catalog command group
|
|
var catalogCmd = &cobra.Command{
|
|
Use: "catalog",
|
|
Short: "Backup catalog management",
|
|
Long: `Manage the backup catalog - a SQLite database tracking all backups.
|
|
|
|
The catalog provides:
|
|
- Searchable history of all backups
|
|
- Gap detection for backup schedules
|
|
- Statistics and reporting
|
|
- Integration with DR drill testing
|
|
|
|
Examples:
|
|
# Sync backups from a directory
|
|
dbbackup catalog sync /backups
|
|
|
|
# List all backups
|
|
dbbackup catalog list
|
|
|
|
# Show catalog statistics
|
|
dbbackup catalog stats
|
|
|
|
# Detect gaps in backup schedule
|
|
dbbackup catalog gaps mydb --interval 24h
|
|
|
|
# Search backups
|
|
dbbackup catalog search --database mydb --after 2024-01-01`,
|
|
}
|
|
|
|
// catalogSyncCmd syncs backups from directory
|
|
var catalogSyncCmd = &cobra.Command{
|
|
Use: "sync [directory]",
|
|
Short: "Sync backups from directory into catalog",
|
|
Long: `Scan a directory for backup files and import them into the catalog.
|
|
|
|
This command:
|
|
- Finds all .meta.json files
|
|
- Imports backup metadata into SQLite catalog
|
|
- Detects removed backups
|
|
- Updates changed entries
|
|
|
|
Examples:
|
|
# Sync from backup directory
|
|
dbbackup catalog sync /backups
|
|
|
|
# Sync with verbose output
|
|
dbbackup catalog sync /backups --verbose`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: runCatalogSync,
|
|
}
|
|
|
|
// catalogListCmd lists backups
|
|
var catalogListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List backups in catalog",
|
|
Long: `List all backups in the catalog with optional filtering.
|
|
|
|
Examples:
|
|
# List all backups
|
|
dbbackup catalog list
|
|
|
|
# List backups for specific database
|
|
dbbackup catalog list --database mydb
|
|
|
|
# List last 10 backups
|
|
dbbackup catalog list --limit 10
|
|
|
|
# Output as JSON
|
|
dbbackup catalog list --format json`,
|
|
RunE: runCatalogList,
|
|
}
|
|
|
|
// catalogStatsCmd shows statistics
|
|
var catalogStatsCmd = &cobra.Command{
|
|
Use: "stats",
|
|
Short: "Show catalog statistics",
|
|
Long: `Display comprehensive backup statistics.
|
|
|
|
Shows:
|
|
- Total backup count and size
|
|
- Backups by database
|
|
- Backups by type and status
|
|
- Verification and drill test coverage
|
|
|
|
Examples:
|
|
# Show overall stats
|
|
dbbackup catalog stats
|
|
|
|
# Stats for specific database
|
|
dbbackup catalog stats --database mydb
|
|
|
|
# Output as JSON
|
|
dbbackup catalog stats --format json`,
|
|
RunE: runCatalogStats,
|
|
}
|
|
|
|
// catalogGapsCmd detects schedule gaps
|
|
var catalogGapsCmd = &cobra.Command{
|
|
Use: "gaps [database]",
|
|
Short: "Detect gaps in backup schedule",
|
|
Long: `Analyze backup history and detect schedule gaps.
|
|
|
|
This helps identify:
|
|
- Missed backups
|
|
- Schedule irregularities
|
|
- RPO violations
|
|
|
|
Examples:
|
|
# Check all databases for gaps (24h expected interval)
|
|
dbbackup catalog gaps
|
|
|
|
# Check specific database with custom interval
|
|
dbbackup catalog gaps mydb --interval 6h
|
|
|
|
# Check gaps in date range
|
|
dbbackup catalog gaps --after 2024-01-01 --before 2024-02-01`,
|
|
RunE: runCatalogGaps,
|
|
}
|
|
|
|
// catalogSearchCmd searches backups
|
|
var catalogSearchCmd = &cobra.Command{
|
|
Use: "search",
|
|
Short: "Search backups in catalog",
|
|
Long: `Search for backups matching specific criteria.
|
|
|
|
Examples:
|
|
# Search by database name (supports wildcards)
|
|
dbbackup catalog search --database "prod*"
|
|
|
|
# Search by date range
|
|
dbbackup catalog search --after 2024-01-01 --before 2024-02-01
|
|
|
|
# Search verified backups only
|
|
dbbackup catalog search --verified
|
|
|
|
# Search encrypted backups
|
|
dbbackup catalog search --encrypted`,
|
|
RunE: runCatalogSearch,
|
|
}
|
|
|
|
// catalogInfoCmd shows entry details
|
|
var catalogInfoCmd = &cobra.Command{
|
|
Use: "info [backup-path]",
|
|
Short: "Show detailed info for a backup",
|
|
Long: `Display detailed information about a specific backup.
|
|
|
|
Examples:
|
|
# Show info by path
|
|
dbbackup catalog info /backups/mydb_20240115.dump.gz`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runCatalogInfo,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(catalogCmd)
|
|
|
|
// Default catalog path
|
|
defaultCatalogPath := filepath.Join(getDefaultConfigDir(), "catalog.db")
|
|
|
|
// Global catalog flags
|
|
catalogCmd.PersistentFlags().StringVar(&catalogDBPath, "catalog-db", defaultCatalogPath,
|
|
"Path to catalog SQLite database")
|
|
catalogCmd.PersistentFlags().StringVar(&catalogFormat, "format", "table",
|
|
"Output format: table, json, csv")
|
|
|
|
// Add subcommands
|
|
catalogCmd.AddCommand(catalogSyncCmd)
|
|
catalogCmd.AddCommand(catalogListCmd)
|
|
catalogCmd.AddCommand(catalogStatsCmd)
|
|
catalogCmd.AddCommand(catalogGapsCmd)
|
|
catalogCmd.AddCommand(catalogSearchCmd)
|
|
catalogCmd.AddCommand(catalogInfoCmd)
|
|
|
|
// Sync flags
|
|
catalogSyncCmd.Flags().BoolVarP(&catalogVerbose, "verbose", "v", false, "Show detailed output")
|
|
|
|
// List flags
|
|
catalogListCmd.Flags().IntVar(&catalogLimit, "limit", 50, "Maximum entries to show")
|
|
catalogListCmd.Flags().StringVar(&catalogDatabase, "database", "", "Filter by database name")
|
|
|
|
// Stats flags
|
|
catalogStatsCmd.Flags().StringVar(&catalogDatabase, "database", "", "Show stats for specific database")
|
|
|
|
// Gaps flags
|
|
catalogGapsCmd.Flags().StringVar(&catalogInterval, "interval", "24h", "Expected backup interval")
|
|
catalogGapsCmd.Flags().StringVar(&catalogStartDate, "after", "", "Start date (YYYY-MM-DD)")
|
|
catalogGapsCmd.Flags().StringVar(&catalogEndDate, "before", "", "End date (YYYY-MM-DD)")
|
|
|
|
// Search flags
|
|
catalogSearchCmd.Flags().StringVar(&catalogDatabase, "database", "", "Filter by database name (supports wildcards)")
|
|
catalogSearchCmd.Flags().StringVar(&catalogStartDate, "after", "", "Backups after date (YYYY-MM-DD)")
|
|
catalogSearchCmd.Flags().StringVar(&catalogEndDate, "before", "", "Backups before date (YYYY-MM-DD)")
|
|
catalogSearchCmd.Flags().IntVar(&catalogLimit, "limit", 100, "Maximum results")
|
|
catalogSearchCmd.Flags().Bool("verified", false, "Only verified backups")
|
|
catalogSearchCmd.Flags().Bool("encrypted", false, "Only encrypted backups")
|
|
catalogSearchCmd.Flags().Bool("drill-tested", false, "Only drill-tested backups")
|
|
}
|
|
|
|
func getDefaultConfigDir() string {
|
|
home, _ := os.UserHomeDir()
|
|
return filepath.Join(home, ".dbbackup")
|
|
}
|
|
|
|
func openCatalog() (*catalog.SQLiteCatalog, error) {
|
|
return catalog.NewSQLiteCatalog(catalogDBPath)
|
|
}
|
|
|
|
func runCatalogSync(cmd *cobra.Command, args []string) error {
|
|
dir := args[0]
|
|
|
|
// Validate directory
|
|
info, err := os.Stat(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("directory not found: %s", dir)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("not a directory: %s", dir)
|
|
}
|
|
|
|
absDir, _ := filepath.Abs(dir)
|
|
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
fmt.Printf("[DIR] Syncing backups from: %s\n", absDir)
|
|
fmt.Printf("[STATS] Catalog database: %s\n\n", catalogDBPath)
|
|
|
|
ctx := context.Background()
|
|
result, err := cat.SyncFromDirectory(ctx, absDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update last sync time
|
|
cat.SetLastSync(ctx)
|
|
|
|
// Show results
|
|
fmt.Printf("=====================================================\n")
|
|
fmt.Printf(" Sync Results\n")
|
|
fmt.Printf("=====================================================\n")
|
|
fmt.Printf(" [OK] Added: %d\n", result.Added)
|
|
fmt.Printf(" [SYNC] Updated: %d\n", result.Updated)
|
|
fmt.Printf(" [DEL] Removed: %d\n", result.Removed)
|
|
if result.Errors > 0 {
|
|
fmt.Printf(" [FAIL] Errors: %d\n", result.Errors)
|
|
}
|
|
fmt.Printf(" [TIME] Duration: %.2fs\n", result.Duration)
|
|
fmt.Printf("=====================================================\n")
|
|
|
|
// Show details if verbose
|
|
if catalogVerbose && len(result.Details) > 0 {
|
|
fmt.Printf("\nDetails:\n")
|
|
for _, detail := range result.Details {
|
|
fmt.Printf(" %s\n", detail)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runCatalogList(cmd *cobra.Command, args []string) error {
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
query := &catalog.SearchQuery{
|
|
Database: catalogDatabase,
|
|
Limit: catalogLimit,
|
|
OrderBy: "created_at",
|
|
OrderDesc: true,
|
|
}
|
|
|
|
entries, err := cat.Search(ctx, query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
fmt.Println("No backups in catalog. Run 'dbbackup catalog sync <directory>' to import backups.")
|
|
return nil
|
|
}
|
|
|
|
if catalogFormat == "json" {
|
|
data, _ := json.MarshalIndent(entries, "", " ")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
// Table format
|
|
fmt.Printf("%-30s %-12s %-10s %-20s %-10s %s\n",
|
|
"DATABASE", "TYPE", "SIZE", "CREATED", "STATUS", "PATH")
|
|
fmt.Println(strings.Repeat("-", 120))
|
|
|
|
for _, entry := range entries {
|
|
dbName := truncateString(entry.Database, 28)
|
|
backupPath := truncateString(filepath.Base(entry.BackupPath), 40)
|
|
|
|
status := string(entry.Status)
|
|
if entry.VerifyValid != nil && *entry.VerifyValid {
|
|
status = "[OK] verified"
|
|
}
|
|
if entry.DrillSuccess != nil && *entry.DrillSuccess {
|
|
status = "[OK] tested"
|
|
}
|
|
|
|
fmt.Printf("%-30s %-12s %-10s %-20s %-10s %s\n",
|
|
dbName,
|
|
entry.DatabaseType,
|
|
catalog.FormatSize(entry.SizeBytes),
|
|
entry.CreatedAt.Format("2006-01-02 15:04"),
|
|
status,
|
|
backupPath,
|
|
)
|
|
}
|
|
|
|
fmt.Printf("\nShowing %d of %d total backups\n", len(entries), len(entries))
|
|
return nil
|
|
}
|
|
|
|
func runCatalogStats(cmd *cobra.Command, args []string) error {
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
var stats *catalog.Stats
|
|
if catalogDatabase != "" {
|
|
stats, err = cat.StatsByDatabase(ctx, catalogDatabase)
|
|
} else {
|
|
stats, err = cat.Stats(ctx)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if catalogFormat == "json" {
|
|
data, _ := json.MarshalIndent(stats, "", " ")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
// Table format
|
|
fmt.Printf("=====================================================\n")
|
|
if catalogDatabase != "" {
|
|
fmt.Printf(" Catalog Statistics: %s\n", catalogDatabase)
|
|
} else {
|
|
fmt.Printf(" Catalog Statistics\n")
|
|
}
|
|
fmt.Printf("=====================================================\n\n")
|
|
|
|
fmt.Printf("[STATS] Total Backups: %d\n", stats.TotalBackups)
|
|
fmt.Printf("[SAVE] Total Size: %s\n", stats.TotalSizeHuman)
|
|
fmt.Printf("[SIZE] Average Size: %s\n", catalog.FormatSize(stats.AvgSize))
|
|
fmt.Printf("[TIME] Average Duration: %.1fs\n", stats.AvgDuration)
|
|
fmt.Printf("[OK] Verified: %d\n", stats.VerifiedCount)
|
|
fmt.Printf("[TEST] Drill Tested: %d\n", stats.DrillTestedCount)
|
|
|
|
if stats.OldestBackup != nil {
|
|
fmt.Printf("📅 Oldest Backup: %s\n", stats.OldestBackup.Format("2006-01-02 15:04"))
|
|
}
|
|
if stats.NewestBackup != nil {
|
|
fmt.Printf("📅 Newest Backup: %s\n", stats.NewestBackup.Format("2006-01-02 15:04"))
|
|
}
|
|
|
|
if len(stats.ByDatabase) > 0 && catalogDatabase == "" {
|
|
fmt.Printf("\n[DIR] By Database:\n")
|
|
for db, count := range stats.ByDatabase {
|
|
fmt.Printf(" %-30s %d\n", db, count)
|
|
}
|
|
}
|
|
|
|
if len(stats.ByType) > 0 {
|
|
fmt.Printf("\n[PKG] By Type:\n")
|
|
for t, count := range stats.ByType {
|
|
fmt.Printf(" %-15s %d\n", t, count)
|
|
}
|
|
}
|
|
|
|
if len(stats.ByStatus) > 0 {
|
|
fmt.Printf("\n[LOG] By Status:\n")
|
|
for s, count := range stats.ByStatus {
|
|
fmt.Printf(" %-15s %d\n", s, count)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\n=====================================================\n")
|
|
return nil
|
|
}
|
|
|
|
func runCatalogGaps(cmd *cobra.Command, args []string) error {
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Parse interval
|
|
interval, err := time.ParseDuration(catalogInterval)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid interval: %w", err)
|
|
}
|
|
|
|
config := &catalog.GapDetectionConfig{
|
|
ExpectedInterval: interval,
|
|
Tolerance: interval / 4, // 25% tolerance
|
|
RPOThreshold: interval * 2, // 2x interval = critical
|
|
}
|
|
|
|
// Parse date range
|
|
if catalogStartDate != "" {
|
|
t, err := time.Parse("2006-01-02", catalogStartDate)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid start date: %w", err)
|
|
}
|
|
config.StartDate = &t
|
|
}
|
|
if catalogEndDate != "" {
|
|
t, err := time.Parse("2006-01-02", catalogEndDate)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid end date: %w", err)
|
|
}
|
|
config.EndDate = &t
|
|
}
|
|
|
|
var allGaps map[string][]*catalog.Gap
|
|
|
|
if len(args) > 0 {
|
|
// Specific database
|
|
database := args[0]
|
|
gaps, err := cat.DetectGaps(ctx, database, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(gaps) > 0 {
|
|
allGaps = map[string][]*catalog.Gap{database: gaps}
|
|
}
|
|
} else {
|
|
// All databases
|
|
allGaps, err = cat.DetectAllGaps(ctx, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if catalogFormat == "json" {
|
|
data, _ := json.MarshalIndent(allGaps, "", " ")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
if len(allGaps) == 0 {
|
|
fmt.Printf("[OK] No backup gaps detected (expected interval: %s)\n", interval)
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("=====================================================\n")
|
|
fmt.Printf(" Backup Gaps Detected (expected interval: %s)\n", interval)
|
|
fmt.Printf("=====================================================\n\n")
|
|
|
|
totalGaps := 0
|
|
criticalGaps := 0
|
|
|
|
for database, gaps := range allGaps {
|
|
fmt.Printf("[DIR] %s (%d gaps)\n", database, len(gaps))
|
|
|
|
for _, gap := range gaps {
|
|
totalGaps++
|
|
icon := "[INFO]"
|
|
switch gap.Severity {
|
|
case catalog.SeverityWarning:
|
|
icon = "[WARN]"
|
|
case catalog.SeverityCritical:
|
|
icon = "🚨"
|
|
criticalGaps++
|
|
}
|
|
|
|
fmt.Printf(" %s %s\n", icon, gap.Description)
|
|
fmt.Printf(" Gap: %s → %s (%s)\n",
|
|
gap.GapStart.Format("2006-01-02 15:04"),
|
|
gap.GapEnd.Format("2006-01-02 15:04"),
|
|
catalog.FormatDuration(gap.Duration))
|
|
fmt.Printf(" Expected at: %s\n", gap.ExpectedAt.Format("2006-01-02 15:04"))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Printf("=====================================================\n")
|
|
fmt.Printf("Total: %d gaps detected", totalGaps)
|
|
if criticalGaps > 0 {
|
|
fmt.Printf(" (%d critical)", criticalGaps)
|
|
}
|
|
fmt.Println()
|
|
|
|
return nil
|
|
}
|
|
|
|
func runCatalogSearch(cmd *cobra.Command, args []string) error {
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
query := &catalog.SearchQuery{
|
|
Database: catalogDatabase,
|
|
Limit: catalogLimit,
|
|
OrderBy: "created_at",
|
|
OrderDesc: true,
|
|
}
|
|
|
|
// Parse date range
|
|
if catalogStartDate != "" {
|
|
t, err := time.Parse("2006-01-02", catalogStartDate)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid start date: %w", err)
|
|
}
|
|
query.StartDate = &t
|
|
}
|
|
if catalogEndDate != "" {
|
|
t, err := time.Parse("2006-01-02", catalogEndDate)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid end date: %w", err)
|
|
}
|
|
query.EndDate = &t
|
|
}
|
|
|
|
// Boolean filters
|
|
if verified, _ := cmd.Flags().GetBool("verified"); verified {
|
|
t := true
|
|
query.Verified = &t
|
|
}
|
|
if encrypted, _ := cmd.Flags().GetBool("encrypted"); encrypted {
|
|
t := true
|
|
query.Encrypted = &t
|
|
}
|
|
if drillTested, _ := cmd.Flags().GetBool("drill-tested"); drillTested {
|
|
t := true
|
|
query.DrillTested = &t
|
|
}
|
|
|
|
entries, err := cat.Search(ctx, query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
fmt.Println("No matching backups found.")
|
|
return nil
|
|
}
|
|
|
|
if catalogFormat == "json" {
|
|
data, _ := json.MarshalIndent(entries, "", " ")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Found %d matching backups:\n\n", len(entries))
|
|
|
|
for _, entry := range entries {
|
|
fmt.Printf("[DIR] %s\n", entry.Database)
|
|
fmt.Printf(" Path: %s\n", entry.BackupPath)
|
|
fmt.Printf(" Type: %s | Size: %s | Created: %s\n",
|
|
entry.DatabaseType,
|
|
catalog.FormatSize(entry.SizeBytes),
|
|
entry.CreatedAt.Format("2006-01-02 15:04:05"))
|
|
if entry.Encrypted {
|
|
fmt.Printf(" [LOCK] Encrypted\n")
|
|
}
|
|
if entry.VerifyValid != nil && *entry.VerifyValid {
|
|
fmt.Printf(" [OK] Verified: %s\n", entry.VerifiedAt.Format("2006-01-02 15:04"))
|
|
}
|
|
if entry.DrillSuccess != nil && *entry.DrillSuccess {
|
|
fmt.Printf(" [TEST] Drill Tested: %s\n", entry.DrillTestedAt.Format("2006-01-02 15:04"))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runCatalogInfo(cmd *cobra.Command, args []string) error {
|
|
backupPath := args[0]
|
|
|
|
cat, err := openCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cat.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Try absolute path
|
|
absPath, _ := filepath.Abs(backupPath)
|
|
entry, err := cat.GetByPath(ctx, absPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if entry == nil {
|
|
// Try as provided
|
|
entry, err = cat.GetByPath(ctx, backupPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if entry == nil {
|
|
return fmt.Errorf("backup not found in catalog: %s", backupPath)
|
|
}
|
|
|
|
if catalogFormat == "json" {
|
|
data, _ := json.MarshalIndent(entry, "", " ")
|
|
fmt.Println(string(data))
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("=====================================================\n")
|
|
fmt.Printf(" Backup Details\n")
|
|
fmt.Printf("=====================================================\n\n")
|
|
|
|
fmt.Printf("[DIR] Database: %s\n", entry.Database)
|
|
fmt.Printf("🔧 Type: %s\n", entry.DatabaseType)
|
|
fmt.Printf("[HOST] Host: %s:%d\n", entry.Host, entry.Port)
|
|
fmt.Printf("📂 Path: %s\n", entry.BackupPath)
|
|
fmt.Printf("[PKG] Backup Type: %s\n", entry.BackupType)
|
|
fmt.Printf("[SAVE] Size: %s (%d bytes)\n", catalog.FormatSize(entry.SizeBytes), entry.SizeBytes)
|
|
fmt.Printf("[HASH] SHA256: %s\n", entry.SHA256)
|
|
fmt.Printf("📅 Created: %s\n", entry.CreatedAt.Format("2006-01-02 15:04:05 MST"))
|
|
fmt.Printf("[TIME] Duration: %.2fs\n", entry.Duration)
|
|
fmt.Printf("[LOG] Status: %s\n", entry.Status)
|
|
|
|
if entry.Compression != "" {
|
|
fmt.Printf("[PKG] Compression: %s\n", entry.Compression)
|
|
}
|
|
if entry.Encrypted {
|
|
fmt.Printf("[LOCK] Encrypted: yes\n")
|
|
}
|
|
if entry.CloudLocation != "" {
|
|
fmt.Printf("[CLOUD] Cloud: %s\n", entry.CloudLocation)
|
|
}
|
|
if entry.RetentionPolicy != "" {
|
|
fmt.Printf("📆 Retention: %s\n", entry.RetentionPolicy)
|
|
}
|
|
|
|
fmt.Printf("\n[STATS] Verification:\n")
|
|
if entry.VerifiedAt != nil {
|
|
status := "[FAIL] Failed"
|
|
if entry.VerifyValid != nil && *entry.VerifyValid {
|
|
status = "[OK] Valid"
|
|
}
|
|
fmt.Printf(" Status: %s (checked %s)\n", status, entry.VerifiedAt.Format("2006-01-02 15:04"))
|
|
} else {
|
|
fmt.Printf(" Status: [WAIT] Not verified\n")
|
|
}
|
|
|
|
fmt.Printf("\n[TEST] DR Drill Test:\n")
|
|
if entry.DrillTestedAt != nil {
|
|
status := "[FAIL] Failed"
|
|
if entry.DrillSuccess != nil && *entry.DrillSuccess {
|
|
status = "[OK] Passed"
|
|
}
|
|
fmt.Printf(" Status: %s (tested %s)\n", status, entry.DrillTestedAt.Format("2006-01-02 15:04"))
|
|
} else {
|
|
fmt.Printf(" Status: [WAIT] Not tested\n")
|
|
}
|
|
|
|
if len(entry.Metadata) > 0 {
|
|
fmt.Printf("\n[NOTE] Additional Metadata:\n")
|
|
for k, v := range entry.Metadata {
|
|
fmt.Printf(" %s: %s\n", k, v)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\n=====================================================\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
func truncateString(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen-3] + "..."
|
|
}
|