Files
2026-07-04 17:20:28 +08:00

108 lines
3.3 KiB
Go

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("──────────────────────────────────────────────")
}