github.com/GGP1/kure@v0.8.4/commands/backup/backup.go (about) 1 package backup 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "strconv" 11 "sync" 12 "time" 13 14 "github.com/GGP1/kure/auth" 15 cmdutil "github.com/GGP1/kure/commands" 16 "github.com/GGP1/kure/config" 17 "github.com/GGP1/kure/sig" 18 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 bolt "go.etcd.io/bbolt" 22 ) 23 24 const example = ` 25 * Create a file backup 26 kure backup --path path/to/file 27 28 * Serve the database on a local server, port 7777 29 kure backup --http --port 7777 30 31 * Download database 32 curl localhost:7777 > database_name` 33 34 type backupOptions struct { 35 path string 36 port uint16 37 httpB bool 38 } 39 40 // NewCmd returns a new command. 41 func NewCmd(db *bolt.DB) *cobra.Command { 42 opts := backupOptions{} 43 cmd := &cobra.Command{ 44 Use: "backup", 45 Short: "Create database backup", 46 Example: example, 47 PreRunE: auth.Login(db), 48 RunE: opts.runBackup(db), 49 PostRun: func(cmd *cobra.Command, args []string) { 50 // Reset variables (session) 51 opts = backupOptions{ 52 port: 8080, 53 } 54 }, 55 } 56 57 f := cmd.Flags() 58 f.BoolVar(&opts.httpB, "http", false, "serve database file on a local server") 59 f.StringVar(&opts.path, "path", "", "destination file path") 60 f.Uint16Var(&opts.port, "port", 8080, "server port") 61 62 return cmd 63 } 64 65 func (opts *backupOptions) runBackup(db *bolt.DB) cmdutil.RunEFunc { 66 return func(cmd *cobra.Command, args []string) error { 67 if opts.httpB { 68 return serveFile(db, opts.port) 69 } 70 71 return fileBackup(db, opts.path) 72 } 73 } 74 75 // serveFile serves the file on localhost. 76 func serveFile(db *bolt.DB, port uint16) error { 77 if port == 0 { 78 return errors.New("invalid port") 79 } 80 81 server := &http.Server{ 82 Addr: fmt.Sprintf(":%d", port), 83 } 84 sig.Signal.AddCleanup(func() error { 85 // Do not exit after a signal as we are handling the shutdown 86 sig.Signal.KeepAlive() 87 fmt.Println("Shutting down server...") 88 89 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 90 defer cancel() 91 92 if err := server.Shutdown(ctx); err != nil { 93 return errors.Wrap(err, "graceful shutdown") 94 } 95 96 if err := server.Close(); err != nil { 97 return errors.Wrap(err, "closing server") 98 } 99 return nil 100 }) 101 102 // Register route only once, otherwise it will panic if 103 // called multiple times inside a session 104 var once sync.Once 105 once.Do(func() { 106 http.HandleFunc("/", httpBackup(db)) 107 }) 108 fmt.Printf("Serving database on http://localhost:%d (Press Ctrl+C to quit)\n", port) 109 110 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 111 return errors.Wrap(err, "starting server") 112 } 113 114 return nil 115 } 116 117 // fileBackup writes the database to a new file. 118 func fileBackup(db *bolt.DB, path string) error { 119 if path == "" { 120 return cmdutil.ErrInvalidPath 121 } 122 123 dir := filepath.Dir(path) 124 125 if err := os.MkdirAll(dir, 0o700); err != nil { 126 return errors.Wrap(err, "making directory") 127 } 128 129 if err := os.Chdir(dir); err != nil { 130 return errors.Wrap(err, "changing working directory") 131 } 132 133 f, err := os.OpenFile(filepath.Base(path), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) 134 if err != nil { 135 return errors.Wrap(err, "opening file") 136 } 137 138 if err := writeTo(db, f); err != nil { 139 return err 140 } 141 142 if err := f.Close(); err != nil { 143 return errors.Wrap(err, "closing file") 144 } 145 146 abs, _ := filepath.Abs(path) 147 fmt.Println("Backup created at", abs) 148 return nil 149 } 150 151 // httpBackup writes a consistent view of the database to a http endpoint. 152 func httpBackup(db *bolt.DB) http.HandlerFunc { 153 name := filepath.Base(config.GetString("database.path")) 154 disposition := fmt.Sprintf(`attachment; filename=%q`, name) 155 156 return func(w http.ResponseWriter, r *http.Request) { 157 err := db.View(func(tx *bolt.Tx) error { 158 w.Header().Set("Content-Type", "application/octet-stream") 159 w.Header().Set("Content-Disposition", disposition) 160 w.Header().Set("Content-Length", strconv.Itoa(int(tx.Size()))) 161 if _, err := tx.WriteTo(w); err != nil { 162 return errors.Wrap(err, "writing the database") 163 } 164 165 return nil 166 }) 167 if err != nil { 168 http.Error(w, err.Error(), http.StatusInternalServerError) 169 } 170 } 171 } 172 173 // writeTo writes the entire database to a writer. 174 func writeTo(db *bolt.DB, w io.Writer) error { 175 return db.View(func(tx *bolt.Tx) error { 176 if _, err := tx.WriteTo(w); err != nil { 177 return errors.Wrap(err, "writing the database") 178 } 179 return nil 180 }) 181 }