cmd/gitbayd/restoredrill.go

v1.41.0
gitbay/cmd/gitbayd/restoredrill.go history · blame · raw

107 lines · 3393 bytes

  1package main
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io/fs"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10	"time"
 11
 12	"github.com/spf13/cobra"
 13
 14	"gitbay.org/gitbay/internal/store"
 15)
 16
 17// restoreDrillCmd rehearses the archive half of a restore: extract a
 18// full archive into an empty directory, run verify's checks on what was
 19// extracted, and report the elapsed time and the newest activity the
 20// restored database holds.
 21func restoreDrillCmd() *cobra.Command {
 22	var into, identity string
 23	cmd := &cobra.Command{
 24		Use:   "restore-drill <archive> --into <dir>",
 25		Short: "restore a full archive into an empty directory, verify it, report elapsed time and the newest recovered activity",
 26		Long: `Extracts every member of a full backup archive into --into, which must
 27be empty or absent, and runs the checks of backup --verify on the
 28extracted copy: database integrity, every repository present and
 29passing git fsck --connectivity-only, release assets and LFS object
 30digests. It then prints the newest issue, comment and push in the
 31restored database, which is the recovery point, and the elapsed time.
 32
 33The result is a server.root a gitbayd can be pointed at. The archive
 34does not carry server.secret_key_file or config.toml; the Admin wiki's
 35Restore drill section covers those and the offsite path.`,
 36		Args: cobra.ExactArgs(1),
 37		RunE: func(cmd *cobra.Command, args []string) error {
 38			if into == "" {
 39				return errors.New("--into <dir> is required")
 40			}
 41			return restoreDrill(args[0], identity, into)
 42		},
 43	}
 44	cmd.Flags().StringVar(&into, "into", "", "empty or absent directory to restore into")
 45	cmd.Flags().StringVar(&identity, "identity", "", "an age identity file that opens an encrypted archive")
 46	return cmd
 47}
 48
 49func restoreDrill(archive, identity, into string) error {
 50	start := time.Now()
 51	ents, err := os.ReadDir(into)
 52	switch {
 53	case errors.Is(err, fs.ErrNotExist):
 54		if err := os.MkdirAll(into, 0o700); err != nil {
 55			return err
 56		}
 57	case err != nil:
 58		return err
 59	case len(ents) > 0:
 60		return fmt.Errorf("%s is not empty; restore into an empty or absent directory", into)
 61	}
 62	checkErr := checkArchive(archive, identity, into, true)
 63	if ents, err := os.ReadDir(into); err == nil {
 64		var names []string
 65		for _, e := range ents {
 66			names = append(names, e.Name())
 67		}
 68		fmt.Printf("restored into %s: %s\n", into, strings.Join(names, " "))
 69	}
 70	// store.Open would create a missing database.
 71	db := filepath.Join(into, "gitbay.db")
 72	if _, err := os.Stat(db); err == nil {
 73		if err := printNewest(db); err != nil {
 74			checkErr = errors.Join(checkErr, err)
 75		}
 76	}
 77	fmt.Printf("elapsed %s\n", time.Since(start).Round(100*time.Millisecond))
 78	return checkErr
 79}
 80
 81// newest are the recovered timestamps a drill records.
 82var newest = []struct{ label, query string }{
 83	{"issue", "SELECT MAX(created_at) FROM issues"},
 84	{"issue comment", "SELECT MAX(created_at) FROM issue_comments"},
 85	{"merge request comment", "SELECT MAX(created_at) FROM mr_comments"},
 86	{"push", "SELECT MAX(created_at) FROM events WHERE kind = 'push'"},
 87}
 88
 89func printNewest(db string) error {
 90	st, err := store.Open(db)
 91	if err != nil {
 92		return err
 93	}
 94	defer st.Close()
 95	for _, n := range newest {
 96		var at *string
 97		if err := st.DB.QueryRow(n.query).Scan(&at); err != nil {
 98			return fmt.Errorf("newest %s: %w", n.label, err)
 99		}
100		v := "none"
101		if at != nil {
102			v = *at
103		}
104		fmt.Printf("newest %s: %s\n", n.label, v)
105	}
106	return nil
107}