// Copyright 2021 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package commands import ( "context" "fmt" "io" "path/filepath" "sort" "time" "github.com/fatih/color" "github.com/dolthub/dolt/go/cmd/dolt/cli" "github.com/dolthub/dolt/go/gen/fb/serial" "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" "github.com/dolthub/dolt/go/libraries/doltcore/env" "github.com/dolthub/dolt/go/libraries/utils/argparser" "github.com/dolthub/dolt/go/libraries/utils/filesys" "github.com/dolthub/dolt/go/store/chunks" "github.com/dolthub/dolt/go/store/nbs" "github.com/dolthub/dolt/go/store/prolly/tree" "github.com/dolthub/dolt/go/store/spec" "github.com/dolthub/dolt/go/store/types" ) const numFilesParam = "numfiles" type RootsCmd struct { } // Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command func (cmd RootsCmd) Name() string { return "roots" } // Hidden should return true if this command should be hidden from the help text func (cmd RootsCmd) Hidden() bool { return true } // Description returns a description of the command func (cmd RootsCmd) Description() string { return "Displays store root values (or potential store root values) that we find in the current database." } func (cmd RootsCmd) Docs() *cli.CommandDocumentation { return nil } func (cmd RootsCmd) ArgParser() *argparser.ArgParser { ap := argparser.NewArgParserWithMaxArgs(cmd.Name(), 0) ap.SupportsInt(numFilesParam, "n", "number", "Number of table files to scan.") return ap } // Exec executes the command func (cmd RootsCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int { ap := cmd.ArgParser() help, _ := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, cli.CommandDocumentationContent{}, ap)) apr := cli.ParseArgsOrDie(ap, args, help) doltDir := dEnv.GetDoltDir() if doltDir == "" { cli.Println(color.YellowString("can no longer find a database on disk")) return 1 } dir := filepath.Join(doltDir, dbfactory.DataDir) oldgen := filepath.Join(dir, "oldgen") itr, err := NewTableFileIter([]string{dir, oldgen}, dEnv.FS) if err != nil { return BuildVerrAndExit("Unable to read table files.", err) } n := apr.GetIntOrDefault(numFilesParam, len(itr.files)) for i := 0; i < n; i++ { fPath, modified := itr.next() err = cmd.processTableFile(ctx, fPath, modified, dEnv.FS) if err == io.EOF { break } else if err != nil { cli.Println(color.YellowString("Failed to process '%s'. Cause: %v", fPath, err)) } } return 0 } func (cmd RootsCmd) processTableFile(ctx context.Context, path string, modified time.Time, fs filesys.Filesys) error { cli.Printf("Processing '%s' last modified: %v\n", path, modified) rdCloser, err := fs.OpenForRead(path) if err != nil { return err } defer rdCloser.Close() return nbs.IterChunks(ctx, rdCloser.(io.ReadSeeker), func(chunk chunks.Chunk) (stop bool, err error) { // Want a clean db every loop sp, _ := spec.ForDatabase("mem") vrw := sp.GetVRW(ctx) value, err := types.DecodeValue(chunk, vrw) if err != nil { return false, err } if sm, ok := value.(types.SerialMessage); ok { if serial.GetFileID(sm) == serial.StoreRootFileID { msg, err := serial.TryGetRootAsStoreRoot([]byte(sm), serial.MessagePrefixSz) if err != nil { return false, err } ambytes := msg.AddressMapBytes() node, fileId, err := tree.NodeFromBytes(ambytes) if err != nil { return false, err } if fileId != serial.AddressMapFileID { return false, fmt.Errorf("unexpected file ID for address map, expected %s, found %s", serial.AddressMapFileID, fileId) } err = tree.OutputAddressMapNode(cli.OutStream, node) if err != nil { return false, err } cli.Println() } } else { return false, fmt.Errorf("unexpected value type found in table file: %T", value) } return false, nil }) } type fileAndTime struct { Path string Modified time.Time } type TableFileIter struct { files []fileAndTime pos int } func NewTableFileIter(dirs []string, fs filesys.Filesys) (*TableFileIter, error) { var tableFiles []fileAndTime for _, dir := range dirs { err := fs.Iter(dir, false, func(path string, size int64, isDir bool) (stop bool) { if !isDir { filename := filepath.Base(path) if len(filename) == 32 { t, ok := fs.LastModified(path) if !ok { t = time.Now() } tableFiles = append(tableFiles, fileAndTime{path, t}) } } return false }) if err != nil { return nil, err } } if len(tableFiles) == 0 { return nil, fmt.Errorf("No table files found in '%v'", dirs) } sort.Slice(tableFiles, func(i, j int) bool { return tableFiles[i].Modified.Sub(tableFiles[j].Modified) > 0 }) return &TableFileIter{tableFiles, 0}, nil } func (itr *TableFileIter) next() (string, time.Time) { if itr.pos >= len(itr.files) { return "", time.Time{} } curr := itr.files[itr.pos] itr.pos++ return curr.Path, curr.Modified }