Something went wrong. Try again.
🪺 PoP Zero is my homelab
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152package main
import ( "context" "errors" "flag" "fmt" "log/slog" "os" "os/signal" "path/filepath" "strings" "syscall" "time"
"golang.org/x/sync/errgroup")
type ignores []string
func (i *ignores) String() string { return strings.Join(*i, ",") }
func (i *ignores) Set(v string) error { *i = append(*i, v) return nil}
var ( path = flag.String("path", "/mnt/Cofrin", "path to back up") ignore ignores dbs = flag.String("dbs", "", "directory of per-database dump configs") alert = flag.Bool("test-alert", false, "send a test message to Nextcloud Talk and exit"))
func init() { flag.Var(&ignore, "ignore", "path to exclude (repeatable)")}
func run() error { if os.Getenv("RESTIC_REPOSITORY") == "" { return errors.New("RESTIC_REPOSITORY is required") } if os.Getenv("RESTIC_PASSWORD") == "" { return errors.New("RESTIC_PASSWORD is required") }
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Hour) defer cancel()
if err := ensureRepo(ctx); err != nil { return fmt.Errorf("checking repository: %w", err) }
skip := []string(ignore) pths := []string{*path}
if *dbs != "" { ls, err := loadDBs(*dbs) if err != nil { return fmt.Errorf("loading db configs: %w", err) } if len(ls) > 0 { ds := filepath.Join(os.TempDir(), "pop0-dumps") if err := os.RemoveAll(ds); err != nil { return fmt.Errorf("cleaning dumps dir: %w", err) } if err := os.MkdirAll(ds, 0o700); err != nil { return fmt.Errorf("creating dumps dir: %w", err) } defer func() { if err := os.RemoveAll(ds); err != nil { slog.Error("removing dumps dir", "err", err) } }()
var g errgroup.Group for _, d := range ls { if d.exclude { skip = append(skip, d.path) } g.Go(func() error { slog.Info("dumping database", "kind", d.kind, "host", d.host, "db", d.db) if err := d.dump(ds); err != nil { return fmt.Errorf("dumping %s: %w", d.kind, err) } return nil }) } if err := g.Wait(); err != nil { return err } pths = append(pths, ds) } }
slog.Info("backing up", "paths", pths, "exclude", skip) if err := backup(ctx, pths, skip); err != nil { return err }
slog.Info("pruning old snapshots") if err := forget(ctx); err != nil { return fmt.Errorf("forget: %w", err) }
slog.Info("backup done") return nil}
func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "backup dumps databases and saves a local path to a restic repository.\n\nUsage of %s:\n", os.Args[0]) flag.PrintDefaults() } flag.Parse()
n, err := newNotifier() if err != nil { slog.Error("creating notifier", "err", err) os.Exit(1) }
if *alert { if err := n.alert("this is a test alert from the backup program"); err != nil { slog.Error("sending alert", "err", err) os.Exit(1) } slog.Info("test alert sent") return }
ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGTERM) go func() { <-ch const msg = "backup killed by signal (SIGTERM)" slog.Error(msg) if err := n.alert(msg); err != nil { slog.Error("sending alert on signal", "err", err) } os.Exit(1) }()
if err := run(); err != nil { slog.Error("backup failed", "err", err) if err := n.alert(err.Error()); err != nil { slog.Error("sending alert", "err", err) } os.Exit(1) }}