package backup import ( "fmt" "log/slog" "os" "github.com/spf13/cobra" "git.misaka.ren/M1saka/docker_backup/internal/backup" "git.misaka.ren/M1saka/docker_backup/internal/config" ) var backupCmd = &cobra.Command{ Use: "backup", Short: "Run a backup of all configured projects", Long: `Stop each Docker Compose project, copy its directory, restart immediately, and compress the snapshot.`, RunE: runBackup, } func init() { rootCmd.AddCommand(backupCmd) rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.yaml", "path to config file") rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "print what would be done without executing") } func runBackup(cmd *cobra.Command, args []string) error { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) cfg, err := config.Load(cfgFile) if err != nil { return fmt.Errorf("load config: %w", err) } if !backup.IsDockerAvailable() { logger.Warn("docker not found on PATH; commands will fail") } engine := backup.NewEngine(cfg.Global.BackupDir, cfg.Global.TempDir, dryRun, logger) ctx := cmd.Context() results := engine.Run(ctx, cfg.Projects) // Print summary. fmt.Println() printResults(results) // Apply retention and upload after backup. for i, r := range results { if r.Error != nil { logger.Warn("skipping retention/upload for failed project", "project", r.ProjectName) continue } proj := cfg.Projects[i] // Retention cleanup. if !dryRun { policy := backup.RetentionPolicy{ Count: proj.Retention.Count, Days: proj.Retention.Days, } cleaned, err := backup.ApplyRetention(cfg.Global.BackupDir, proj.Name, policy) if err != nil { logger.Warn("retention cleanup failed", "project", proj.Name, "error", err) } else if cleaned > 0 { logger.Info("retention cleanup", "project", proj.Name, "removed", cleaned) } } // Remote upload. if cfg.HasRemote() && !dryRun { if err := uploadToRemotes(ctx, cfg.Remote, proj.Name, r.BackupPath, logger); err != nil { logger.Warn("remote upload failed", "project", proj.Name, "error", err) } } } // Return non-zero exit if any project failed. for _, r := range results { if r.Error != nil { return fmt.Errorf("one or more projects failed") } } return nil } func printResults(results []backup.Result) { fmt.Println("──────────────────────────────────────────────") fmt.Println(" Backup Results") fmt.Println("──────────────────────────────────────────────") for _, r := range results { status := "OK" if r.Error != nil { status = fmt.Sprintf("FAIL: %v", r.Error) } fmt.Printf(" %-12s %s\n", r.ProjectName, status) if r.BackupPath != "" { fmt.Printf(" file: %s\n", r.BackupPath) fmt.Printf(" size: %s\n", backup.FormatSize(r.FileSize)) } if r.Downtime > 0 { fmt.Printf(" downtime: %v\n", r.Downtime.Round(0)) } fmt.Printf(" duration: %v\n", r.Duration.Round(0)) fmt.Println() } fmt.Println("──────────────────────────────────────────────") }