cmd/gitbayd/restoredrill.go
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}