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  }