diff --git a/cmd/bbolt/command_check.go b/cmd/bbolt/command_check.go new file mode 100644 index 000000000..6ff383cb9 --- /dev/null +++ b/cmd/bbolt/command_check.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "github.com/spf13/cobra" + + bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt/internal/guts_cli" +) + +func newCheckCommand() *cobra.Command { + checkCmd := &cobra.Command{ + Use: "check ", + Short: "verify integrity of bbolt database data", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return checkFunc(cmd, args[0]) + }, + } + + return checkCmd +} + +func checkFunc(cmd *cobra.Command, dbPath string) error { + if _, err := checkSourceDBPath(dbPath); err != nil { + return err + } + + // Open database. + db, err := bolt.Open(dbPath, 0600, &bolt.Options{ + ReadOnly: true, + PreLoadFreelist: true, + }) + if err != nil { + return err + } + defer db.Close() + + // Perform consistency check. + return db.View(func(tx *bolt.Tx) error { + var count int + for err := range tx.Check(bolt.WithKVStringer(CmdKvStringer())) { + fmt.Fprintln(cmd.OutOrStdout(), err) + count++ + } + + // Print summary of errors. + if count > 0 { + fmt.Fprintf(cmd.OutOrStdout(), "%d errors found\n", count) + return guts_cli.ErrCorrupt + } + + // Notify user that database is valid. + fmt.Fprintln(cmd.OutOrStdout(), "OK") + return nil + }) +} diff --git a/cmd/bbolt/command_check_test.go b/cmd/bbolt/command_check_test.go new file mode 100644 index 000000000..02500745c --- /dev/null +++ b/cmd/bbolt/command_check_test.go @@ -0,0 +1,33 @@ +package main_test + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" + + main "go.etcd.io/bbolt/cmd/bbolt" + "go.etcd.io/bbolt/internal/btesting" +) + +func TestCheckCommand_Run(t *testing.T) { + db := btesting.MustCreateDB(t) + db.Close() + defer requireDBNoChange(t, dbData(t, db.Path()), db.Path()) + + rootCmd := main.NewRootCommand() + // capture output for assertion + outputBuf := bytes.NewBufferString("") + rootCmd.SetOut(outputBuf) + + rootCmd.SetArgs([]string{ + "check", db.Path(), + }) + err := rootCmd.Execute() + require.NoError(t, err) + + output, err := io.ReadAll(outputBuf) + require.NoError(t, err) + require.Equalf(t, "OK\n", string(output), "unexpected stdout:\n\n%s", string(output)) +} diff --git a/cmd/bbolt/command_root.go b/cmd/bbolt/command_root.go index b69a619ed..979f485ab 100644 --- a/cmd/bbolt/command_root.go +++ b/cmd/bbolt/command_root.go @@ -20,6 +20,7 @@ func NewRootCommand() *cobra.Command { newVersionCobraCommand(), newSurgeryCobraCommand(), newInspectCobraCommand(), + newCheckCommand(), ) return rootCmd diff --git a/cmd/bbolt/main.go b/cmd/bbolt/main.go index a5a4e9f2e..2c803f311 100644 --- a/cmd/bbolt/main.go +++ b/cmd/bbolt/main.go @@ -123,8 +123,6 @@ func (m *Main) Run(args ...string) error { return newBenchCommand(m).Run(args[1:]...) case "buckets": return newBucketsCommand(m).Run(args[1:]...) - case "check": - return newCheckCommand(m).Run(args[1:]...) case "compact": return newCompactCommand(m).Run(args[1:]...) case "dump": @@ -180,82 +178,6 @@ Use "bbolt [command] -h" for more information about a command. `, "\n") } -// checkCommand represents the "check" command execution. -type checkCommand struct { - baseCommand -} - -// newCheckCommand returns a checkCommand. -func newCheckCommand(m *Main) *checkCommand { - c := &checkCommand{} - c.baseCommand = m.baseCommand - return c -} - -// Run executes the command. -func (cmd *checkCommand) Run(args ...string) error { - // Parse flags. - fs := flag.NewFlagSet("", flag.ContinueOnError) - help := fs.Bool("h", false, "") - if err := fs.Parse(args); err != nil { - return err - } else if *help { - fmt.Fprintln(cmd.Stderr, cmd.Usage()) - return ErrUsage - } - - // Require database path. - path := fs.Arg(0) - if path == "" { - return ErrPathRequired - } else if _, err := os.Stat(path); os.IsNotExist(err) { - return ErrFileNotFound - } - - // Open database. - db, err := bolt.Open(path, 0600, &bolt.Options{ - ReadOnly: true, - PreLoadFreelist: true, - }) - if err != nil { - return err - } - defer db.Close() - - // Perform consistency check. - return db.View(func(tx *bolt.Tx) error { - var count int - for err := range tx.Check(bolt.WithKVStringer(CmdKvStringer())) { - fmt.Fprintln(cmd.Stdout, err) - count++ - } - - // Print summary of errors. - if count > 0 { - fmt.Fprintf(cmd.Stdout, "%d errors found\n", count) - return guts_cli.ErrCorrupt - } - - // Notify user that database is valid. - fmt.Fprintln(cmd.Stdout, "OK") - return nil - }) -} - -// Usage returns the help message. -func (cmd *checkCommand) Usage() string { - return strings.TrimLeft(` -usage: bolt check PATH - -Check opens a database at PATH and runs an exhaustive check to verify that -all pages are accessible or are marked as freed. It also verifies that no -pages are double referenced. - -Verification errors will stream out as they are found and the process will -return after all pages have been checked. -`, "\n") -} - // infoCommand represents the "info" command execution. type infoCommand struct { baseCommand diff --git a/cmd/bbolt/main_test.go b/cmd/bbolt/main_test.go index 8a5cc94e8..2dfb04449 100644 --- a/cmd/bbolt/main_test.go +++ b/cmd/bbolt/main_test.go @@ -79,20 +79,6 @@ func TestStatsCommand_Run_EmptyDatabase(t *testing.T) { } } -func TestCheckCommand_Run(t *testing.T) { - db := btesting.MustCreateDB(t) - db.Close() - - defer requireDBNoChange(t, dbData(t, db.Path()), db.Path()) - - m := NewMain() - err := m.Run("check", db.Path()) - require.NoError(t, err) - if m.Stdout.String() != "OK\n" { - t.Fatalf("unexpected stdout:\n\n%s", m.Stdout.String()) - } -} - func TestDumpCommand_Run(t *testing.T) { db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: 4096}) db.Close()