package main
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"time"
)
// defaultBackupKeep matches deploy/backup.sh's own default — kept the same
// so the two tools' pruning behavior doesn't quietly diverge.
const defaultBackupKeep = 14
// runBackupNow is the local (no ssh) equivalent of deploy/backup.sh's core
// snapshot: stream a tar of the whole /data volume out of the running
// server container, write it atomically (a .part file renamed on success,
// so a crash mid-stream never leaves a truncated backup mistaken for a
// good one), then prune down to defaultBackupKeep. It's a live, crash-
// consistent copy (bbolt tolerates this), not a maintenance-window
// snapshot — same caveat deploy/backup.sh's own header documents.
func runBackupNow(ctx context.Context, sourceDir string) (path string, err error) {
backupDir := filepath.Join(sourceDir, "backups")
if err := os.MkdirAll(backupDir, 0755); err != nil {
return "", err
}
timestamp := time.Now().UTC().Format("20060102T150405Z")
out := filepath.Join(backupDir, fmt.Sprintf("gitfed-%s.tar.gz", timestamp))
tmp := out + ".part"
f, err := os.Create(tmp)
if err != nil {
return "", err
}
cmdErr := runToFile(ctx, f, "kubectl", "-n", "gitfed", "exec", "deployment/gitfed", "-c", "server",
"--", "tar", "czf", "-", "-C", "/data", ".")
closeErr := f.Close()
if cmdErr != nil {
os.Remove(tmp)
return "", cmdErr
}
if closeErr != nil {
os.Remove(tmp)
return "", closeErr
}
if err := os.Rename(tmp, out); err != nil {
return "", err
}
if err := pruneBackups(sourceDir, defaultBackupKeep); err != nil {
// A pruning failure shouldn't fail the backup that already
// succeeded — the disk just has more history than intended.
return out, nil
}
return out, nil
}
// pruneBackups keeps the keep most recent backups/gitfed-*.tar.gz files
// (by modification time) and removes the rest — same "keep N most recent"
// behavior as deploy/backup.sh's `ls -1t | tail -n +N+1 | xargs rm`.
func pruneBackups(sourceDir string, keep int) error {
matches, err := filepath.Glob(filepath.Join(sourceDir, "backups", "gitfed-*.tar.gz"))
if err != nil {
return err
}
if len(matches) <= keep {
return nil
}
type fileTime struct {
path string
mod time.Time
}
files := make([]fileTime, 0, len(matches))
for _, m := range matches {
info, err := os.Stat(m)
if err != nil {
continue
}
files = append(files, fileTime{path: m, mod: info.ModTime()})
}
sort.Slice(files, func(i, j int) bool { return files[i].mod.After(files[j].mod) })
for _, f := range files[keep:] {
if err := os.Remove(f.path); err != nil {
return err
}
}
return nil
}