108 lines
3.3 KiB
Go
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("──────────────────────────────────────────────")
|
|
}
|