diff --git a/cmd/bbolt/command_surgery_cobra.go b/cmd/bbolt/command_surgery_cobra.go index fc709c98c..a62bec40a 100644 --- a/cmd/bbolt/command_surgery_cobra.go +++ b/cmd/bbolt/command_surgery_cobra.go @@ -30,12 +30,54 @@ func newSurgeryCobraCommand() *cobra.Command { Short: "surgery related commands", } + surgeryCmd.AddCommand(newSurgeryRevertMetaPageCommand()) surgeryCmd.AddCommand(newSurgeryClearPageElementsCommand()) surgeryCmd.AddCommand(newSurgeryFreelistCommand()) return surgeryCmd } +func newSurgeryRevertMetaPageCommand() *cobra.Command { + revertMetaPageCmd := &cobra.Command{ + Use: "revert-meta-page [options]", + Short: "Revert the meta page to revert the changes performed by the latest transaction", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("db file path not provided") + } + if len(args) > 1 { + return errors.New("too many arguments") + } + return nil + }, + RunE: surgeryRevertMetaPageFunc, + } + + revertMetaPageCmd.Flags().StringVar(&surgeryTargetDBFilePath, "output", "", "path to the target db file") + + return revertMetaPageCmd +} + +func surgeryRevertMetaPageFunc(cmd *cobra.Command, args []string) error { + srcDBPath := args[0] + + if err := checkDBPaths(srcDBPath, surgeryTargetDBFilePath); err != nil { + return err + } + + if err := common.CopyFile(srcDBPath, surgeryTargetDBFilePath); err != nil { + return fmt.Errorf("[revert-meta-page] copy file failed: %w", err) + } + + if err := surgeon.RevertMetaPage(surgeryTargetDBFilePath); err != nil { + return fmt.Errorf("revert-meta-page command failed: %w", err) + } + + fmt.Fprintln(os.Stdout, "The meta page is reverted.") + + return nil +} + func newSurgeryClearPageElementsCommand() *cobra.Command { clearElementCmd := &cobra.Command{ Use: "clear-page-elements [options]", @@ -210,3 +252,18 @@ func readMetaPage(path string) (*common.Meta, error) { } return common.LoadPageMeta(buf), nil } + +func checkDBPaths(srcPath, dstPath string) error { + _, err := os.Stat(srcPath) + if os.IsNotExist(err) { + return fmt.Errorf("source database file %q doesn't exist", srcPath) + } else if err != nil { + return fmt.Errorf("failed to open source database file %q: %v", srcPath, err) + } + + if dstPath == "" { + return fmt.Errorf("output database path wasn't given, specify output database file path with --output option") + } + + return nil +} diff --git a/cmd/bbolt/command_surgery_cobra_test.go b/cmd/bbolt/command_surgery_cobra_test.go index 0e35c05e9..fc0f6376e 100644 --- a/cmd/bbolt/command_surgery_cobra_test.go +++ b/cmd/bbolt/command_surgery_cobra_test.go @@ -2,6 +2,7 @@ package main_test import ( "fmt" + "os" "path/filepath" "testing" @@ -15,6 +16,51 @@ import ( "go.etcd.io/bbolt/internal/guts_cli" ) +func TestSurgery_RevertMetaPage(t *testing.T) { + pageSize := 4096 + db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize}) + srcPath := db.Path() + + defer requireDBNoChange(t, dbData(t, db.Path()), db.Path()) + + srcFile, err := os.Open(srcPath) + require.NoError(t, err) + defer srcFile.Close() + + // Read both meta0 and meta1 from srcFile + srcBuf0 := readPage(t, srcPath, 0, pageSize) + srcBuf1 := readPage(t, srcPath, 1, pageSize) + meta0Page := common.LoadPageMeta(srcBuf0) + meta1Page := common.LoadPageMeta(srcBuf1) + + // Get the non-active meta page + nonActiveSrcBuf := srcBuf0 + nonActiveMetaPageId := 0 + if meta0Page.Txid() > meta1Page.Txid() { + nonActiveSrcBuf = srcBuf1 + nonActiveMetaPageId = 1 + } + t.Logf("non active meta page id: %d", nonActiveMetaPageId) + + // revert the meta page + rootCmd := main.NewRootCommand() + output := filepath.Join(t.TempDir(), "db") + rootCmd.SetArgs([]string{ + "surgery", "revert-meta-page", srcPath, + "--output", output, + }) + err = rootCmd.Execute() + require.NoError(t, err) + + // read both meta0 and meta1 from dst file + dstBuf0 := readPage(t, output, 0, pageSize) + dstBuf1 := readPage(t, output, 1, pageSize) + + // check result. Note we should skip the page ID + assert.Equal(t, pageDataWithoutPageId(nonActiveSrcBuf), pageDataWithoutPageId(dstBuf0)) + assert.Equal(t, pageDataWithoutPageId(nonActiveSrcBuf), pageDataWithoutPageId(dstBuf1)) +} + func TestSurgery_ClearPageElements_Without_Overflow(t *testing.T) { testCases := []struct { name string diff --git a/cmd/bbolt/surgery_commands.go b/cmd/bbolt/surgery_commands.go index d0a1c8da7..385903134 100644 --- a/cmd/bbolt/surgery_commands.go +++ b/cmd/bbolt/surgery_commands.go @@ -40,8 +40,6 @@ func (cmd *surgeryCommand) Run(args ...string) error { case "help": fmt.Fprintln(cmd.Stderr, cmd.Usage()) return ErrUsage - case "revert-meta-page": - return newRevertMetaPageCommand(cmd).Run(args[1:]...) case "copy-page": return newCopyPageCommand(cmd).Run(args[1:]...) case "clear-page": @@ -90,56 +88,6 @@ Use "bbolt surgery [command] -h" for more information about a command. `, "\n") } -// revertMetaPageCommand represents the "surgery revert-meta-page" command execution. -type revertMetaPageCommand struct { - *surgeryCommand -} - -// newRevertMetaPageCommand returns a revertMetaPageCommand. -func newRevertMetaPageCommand(m *surgeryCommand) *revertMetaPageCommand { - c := &revertMetaPageCommand{} - c.surgeryCommand = m - return c -} - -// Run executes the command. -func (cmd *revertMetaPageCommand) 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 - } - - if err := cmd.parsePathsAndCopyFile(fs); err != nil { - return fmt.Errorf("revertMetaPageCommand failed to parse paths and copy file: %w", err) - } - - // revert the meta page - if err := surgeon.RevertMetaPage(cmd.dstPath); err != nil { - return fmt.Errorf("revertMetaPageCommand failed: %w", err) - } - - fmt.Fprintln(cmd.Stdout, "The meta page is reverted.") - return nil -} - -// Usage returns the help message. -func (cmd *revertMetaPageCommand) Usage() string { - return strings.TrimLeft(` -usage: bolt surgery revert-meta-page SRC DST - -RevertMetaPage copies the database file at SRC to a newly created database -file at DST. Afterwards, it reverts the meta page on the newly created -database at DST. - -The original database is left untouched. -`, "\n") -} - // copyPageCommand represents the "surgery copy-page" command execution. type copyPageCommand struct { *surgeryCommand diff --git a/cmd/bbolt/surgery_commands_test.go b/cmd/bbolt/surgery_commands_test.go index 8d96eb326..567c0c493 100644 --- a/cmd/bbolt/surgery_commands_test.go +++ b/cmd/bbolt/surgery_commands_test.go @@ -14,47 +14,6 @@ import ( "go.etcd.io/bbolt/internal/common" ) -func TestSurgery_RevertMetaPage(t *testing.T) { - pageSize := 4096 - db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize}) - srcPath := db.Path() - - defer requireDBNoChange(t, dbData(t, db.Path()), db.Path()) - - srcFile, err := os.Open(srcPath) - require.NoError(t, err) - defer srcFile.Close() - - // Read both meta0 and meta1 from srcFile - srcBuf0 := readPage(t, srcPath, 0, pageSize) - srcBuf1 := readPage(t, srcPath, 1, pageSize) - meta0Page := common.LoadPageMeta(srcBuf0) - meta1Page := common.LoadPageMeta(srcBuf1) - - // Get the non-active meta page - nonActiveSrcBuf := srcBuf0 - nonActiveMetaPageId := 0 - if meta0Page.Txid() > meta1Page.Txid() { - nonActiveSrcBuf = srcBuf1 - nonActiveMetaPageId = 1 - } - t.Logf("non active meta page id: %d", nonActiveMetaPageId) - - // revert the meta page - dstPath := filepath.Join(t.TempDir(), "dstdb") - m := NewMain() - err = m.Run("surgery", "revert-meta-page", srcPath, dstPath) - require.NoError(t, err) - - // read both meta0 and meta1 from dst file - dstBuf0 := readPage(t, dstPath, 0, pageSize) - dstBuf1 := readPage(t, dstPath, 1, pageSize) - - // check result. Note we should skip the page ID - assert.Equal(t, pageDataWithoutPageId(nonActiveSrcBuf), pageDataWithoutPageId(dstBuf0)) - assert.Equal(t, pageDataWithoutPageId(nonActiveSrcBuf), pageDataWithoutPageId(dstBuf1)) -} - func TestSurgery_CopyPage(t *testing.T) { pageSize := 4096 db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize})