package 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) } }