diff --git a/cmd/pbm/delete.go b/cmd/pbm/delete.go index 6e7706dd3..6f78b3a22 100644 --- a/cmd/pbm/delete.go +++ b/cmd/pbm/delete.go @@ -1,9 +1,9 @@ package main import ( - "bufio" "context" "fmt" + "github.com/percona/percona-backup-mongodb/pbm/util" "io" "os" "strings" @@ -59,7 +59,7 @@ func deleteBackup( cid, err = deleteManyBackup(ctx, pbm, d) } if err != nil { - if errors.Is(err, errUserCanceled) { + if errors.Is(err, errors.ErrUserCanceled) { return outMsg{err.Error()}, nil } return nil, err @@ -109,7 +109,7 @@ func deleteBackupByName(ctx context.Context, pbm *sdk.Client, d *deleteBcpOpts) return sdk.NoOpID, nil } if !d.yes { - err := askConfirmation("Are you sure you want to delete backup?") + err := util.AskConfirmation("Are you sure you want to delete this backup?") if err != nil { return sdk.NoOpID, err } @@ -146,7 +146,7 @@ func deleteManyBackup(ctx context.Context, pbm *sdk.Client, d *deleteBcpOpts) (s return sdk.NoOpID, nil } if !d.yes { - if err := askConfirmation("Are you sure you want to delete backups?"); err != nil { + if err := util.AskConfirmation("Are you sure you want to delete backups?"); err != nil { return sdk.NoOpID, err } } @@ -219,8 +219,8 @@ func deletePITR( if d.all { q = "Are you sure you want to delete ALL chunks?" } - if err := askConfirmation(q); err != nil { - if errors.Is(err, errUserCanceled) { + if err := util.AskConfirmation(q); err != nil { + if errors.Is(err, errors.ErrUserCanceled) { return outMsg{err.Error()}, nil } return nil, err @@ -288,8 +288,8 @@ func doCleanup(ctx context.Context, conn connect.Client, pbm *sdk.Client, d *cle return &outMsg{""}, nil } if !d.yes { - if err := askConfirmation("Are you sure you want to delete?"); err != nil { - if errors.Is(err, errUserCanceled) { + if err := util.AskConfirmation("Are you sure you want to delete?"); err != nil { + if errors.Is(err, errors.ErrUserCanceled) { return outMsg{err.Error()}, nil } return nil, err @@ -414,33 +414,6 @@ func printDeleteInfoTo(w io.Writer, backups []backup.BackupMeta, chunks []oplog. } } -var errUserCanceled = errors.New("canceled") - -func askConfirmation(question string) error { - fi, err := os.Stdin.Stat() - if err != nil { - return errors.Wrap(err, "stat stdin") - } - if (fi.Mode() & os.ModeCharDevice) == 0 { - return errors.New("no tty") - } - - fmt.Printf("%s [y/N] ", question) - - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - if err := scanner.Err(); err != nil { - return errors.Wrap(err, "read stdin") - } - - switch strings.TrimSpace(scanner.Text()) { - case "yes", "Yes", "YES", "Y", "y": - return nil - } - - return errUserCanceled -} - func waitForDelete( ctx context.Context, conn connect.Client, diff --git a/cmd/pbm/restore.go b/cmd/pbm/restore.go index b370d02a5..4dff620df 100644 --- a/cmd/pbm/restore.go +++ b/cmd/pbm/restore.go @@ -49,6 +49,7 @@ type restoreOpts struct { nsFrom string nsTo string usersAndRoles bool + confirmYes bool rsMap string conf string ts string @@ -91,10 +92,9 @@ No other pbm command is available while the restore is running! `, r.Snapshot, r.Name) } - return fmt.Sprintf("Restore of the snapshot from '%s' has started", r.Snapshot) + return fmt.Sprintf("\nRestore of the snapshot from '%s' has started", r.Snapshot) case r.PITR != "": - return fmt.Sprintf("Restore to the point in time '%s' has started", r.PITR) - + return fmt.Sprintf("\nRestore to the point in time '%s' has started", r.PITR) default: return "" } @@ -449,15 +449,10 @@ func doRestore( } err = yaml.UnmarshalStrict(buf, &cmd.Restore.ExtConf) if err != nil { - return nil, errors.Wrap(err, "unable to unmarshal config file") + return nil, errors.Wrap(err, "unable to unmarshal config file") } } - err = sendCmd(ctx, conn, cmd) - if err != nil { - return nil, errors.Wrap(err, "send command") - } - if outf != outText { return &restore.RestoreMeta{ Name: name, @@ -477,7 +472,23 @@ func doRestore( if o.pitr != "" { pitrs = fmt.Sprintf(" to point-in-time %s", o.pitr) } - fmt.Printf("Starting restore %s%s%s", name, pitrs, bcpName) + + fmt.Println("Restore:") + fmt.Printf(" - %s%s%s\n", name, pitrs, bcpName) + + if !o.confirmYes { + err := util.AskConfirmation("Are you sure you want to restore this backup?") + if err != nil { + return nil, err + } + } + + err = sendCmd(ctx, conn, cmd) + if err != nil { + return nil, errors.Wrap(err, "send command") + } + + fmt.Printf("Starting restore") var ( fn getRestoreMetaFn diff --git a/pbm/errors/errors.go b/pbm/errors/errors.go index 601026d3a..1229202d2 100644 --- a/pbm/errors/errors.go +++ b/pbm/errors/errors.go @@ -9,6 +9,8 @@ import ( // ErrNotFound - object not found var ErrNotFound = New("not found") +var ErrUserCanceled = New("user canceled") + func New(text string) error { return stderrors.New(text) //nolint:goerr113 } diff --git a/pbm/util/cmd_prompt.go b/pbm/util/cmd_prompt.go new file mode 100644 index 000000000..d512b76b9 --- /dev/null +++ b/pbm/util/cmd_prompt.go @@ -0,0 +1,39 @@ +package util + +import ( + "bufio" + "fmt" + "os" + "runtime" + "strings" + + "github.com/percona/percona-backup-mongodb/pbm/errors" +) + +func AskConfirmation(question string) error { + fi, err := os.Stdin.Stat() + if err != nil { + return errors.Wrap(err, "stat stdin") + } + if (fi.Mode() & os.ModeCharDevice) == 0 { + return errors.New("no tty") + } + + if runtime.GOOS == "linux" { + question = fmt.Sprintf("\033[1;33m%s\033[0m", question) // Yellow text for Linux terminals + } + fmt.Printf("%s [y/N] ", question) + + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + if err := scanner.Err(); err != nil { + return errors.Wrap(err, "read stdin") + } + + switch strings.TrimSpace(scanner.Text()) { + case "yes", "Yes", "YES", "Y", "y": + return nil + } + + return errors.ErrUserCanceled +}