mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-11-14 12:15:18 +00:00
565 lines
17 KiB
Go
565 lines
17 KiB
Go
// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
|
|
// of this source code is governed by a BSD-style license that can be found in
|
|
// the LICENSE file.
|
|
|
|
package tool
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"text/tabwriter"
|
|
|
|
"github.com/cockroachdb/pebble"
|
|
"github.com/cockroachdb/pebble/internal/base"
|
|
"github.com/cockroachdb/pebble/internal/humanize"
|
|
"github.com/cockroachdb/pebble/internal/keyspan"
|
|
"github.com/cockroachdb/pebble/internal/private"
|
|
"github.com/cockroachdb/pebble/internal/rangedel"
|
|
"github.com/cockroachdb/pebble/sstable"
|
|
"github.com/cockroachdb/pebble/vfs"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// sstableT implements sstable-level tools, including both configuration state
|
|
// and the commands themselves.
|
|
type sstableT struct {
|
|
Root *cobra.Command
|
|
Check *cobra.Command
|
|
Layout *cobra.Command
|
|
Properties *cobra.Command
|
|
Scan *cobra.Command
|
|
Space *cobra.Command
|
|
|
|
// Configuration and state.
|
|
opts *pebble.Options
|
|
comparers sstable.Comparers
|
|
mergers sstable.Mergers
|
|
|
|
// Flags.
|
|
fmtKey keyFormatter
|
|
fmtValue valueFormatter
|
|
start key
|
|
end key
|
|
filter key
|
|
count int64
|
|
verbose bool
|
|
}
|
|
|
|
func newSSTable(
|
|
opts *pebble.Options, comparers sstable.Comparers, mergers sstable.Mergers,
|
|
) *sstableT {
|
|
s := &sstableT{
|
|
opts: opts,
|
|
comparers: comparers,
|
|
mergers: mergers,
|
|
}
|
|
s.fmtKey.mustSet("quoted")
|
|
s.fmtValue.mustSet("[%x]")
|
|
|
|
s.Root = &cobra.Command{
|
|
Use: "sstable",
|
|
Short: "sstable introspection tools",
|
|
}
|
|
s.Check = &cobra.Command{
|
|
Use: "check <sstables>",
|
|
Short: "verify checksums and metadata",
|
|
Long: ``,
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: s.runCheck,
|
|
}
|
|
s.Layout = &cobra.Command{
|
|
Use: "layout <sstables>",
|
|
Short: "print sstable block and record layout",
|
|
Long: `
|
|
Print the layout for the sstables. The -v flag controls whether record layout
|
|
is displayed or omitted.
|
|
`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: s.runLayout,
|
|
}
|
|
s.Properties = &cobra.Command{
|
|
Use: "properties <sstables>",
|
|
Short: "print sstable properties",
|
|
Long: `
|
|
Print the properties for the sstables. The -v flag controls whether the
|
|
properties are pretty-printed or displayed in a verbose/raw format.
|
|
`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: s.runProperties,
|
|
}
|
|
s.Scan = &cobra.Command{
|
|
Use: "scan <sstables>",
|
|
Short: "print sstable records",
|
|
Long: `
|
|
Print the records in the sstables. The sstables are scanned in command line
|
|
order which means the records will be printed in that order. Raw range
|
|
tombstones are displayed interleaved with point records.
|
|
`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: s.runScan,
|
|
}
|
|
s.Space = &cobra.Command{
|
|
Use: "space <sstables>",
|
|
Short: "print filesystem space used",
|
|
Long: `
|
|
Print the estimated space usage in the specified files for the
|
|
inclusive-inclusive range specified by --start and --end.
|
|
`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: s.runSpace,
|
|
}
|
|
|
|
s.Root.AddCommand(s.Check, s.Layout, s.Properties, s.Scan, s.Space)
|
|
s.Root.PersistentFlags().BoolVarP(&s.verbose, "verbose", "v", false, "verbose output")
|
|
|
|
s.Check.Flags().Var(
|
|
&s.fmtKey, "key", "key formatter")
|
|
s.Layout.Flags().Var(
|
|
&s.fmtKey, "key", "key formatter")
|
|
s.Layout.Flags().Var(
|
|
&s.fmtValue, "value", "value formatter")
|
|
s.Scan.Flags().Var(
|
|
&s.fmtKey, "key", "key formatter")
|
|
s.Scan.Flags().Var(
|
|
&s.fmtValue, "value", "value formatter")
|
|
for _, cmd := range []*cobra.Command{s.Scan, s.Space} {
|
|
cmd.Flags().Var(
|
|
&s.start, "start", "start key for the range")
|
|
cmd.Flags().Var(
|
|
&s.end, "end", "end key for the range")
|
|
}
|
|
s.Scan.Flags().Var(
|
|
&s.filter, "filter", "only output records with matching prefix or overlapping range tombstones")
|
|
s.Scan.Flags().Int64Var(
|
|
&s.count, "count", 0, "key count for scan (0 is unlimited)")
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *sstableT) newReader(f vfs.File) (*sstable.Reader, error) {
|
|
readable, err := sstable.NewSimpleReadable(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o := sstable.ReaderOptions{
|
|
Cache: pebble.NewCache(128 << 20 /* 128 MB */),
|
|
Comparer: s.opts.Comparer,
|
|
Filters: s.opts.Filters,
|
|
}
|
|
defer o.Cache.Unref()
|
|
return sstable.NewReader(readable, o, s.comparers, s.mergers,
|
|
private.SSTableRawTombstonesOpt.(sstable.ReaderOption))
|
|
}
|
|
|
|
func (s *sstableT) runCheck(cmd *cobra.Command, args []string) {
|
|
stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
|
|
s.foreachSstable(stderr, args, func(arg string) {
|
|
f, err := s.opts.FS.Open(arg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(stdout, "%s\n", arg)
|
|
|
|
r, err := s.newReader(f)
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
// Update the internal formatter if this comparator has one specified.
|
|
s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
|
|
iter, err := r.NewIter(nil, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
// If a split function is defined for the comparer, verify that
|
|
// SeekPrefixGE can find every key in the table.
|
|
var prefixIter sstable.Iterator
|
|
if r.Split != nil {
|
|
var err error
|
|
prefixIter, err = r.NewIter(nil, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var lastKey base.InternalKey
|
|
for key, _ := iter.First(); key != nil; key, _ = iter.Next() {
|
|
if base.InternalCompare(r.Compare, lastKey, *key) >= 0 {
|
|
fmt.Fprintf(stdout, "WARNING: OUT OF ORDER KEYS!\n")
|
|
if s.fmtKey.spec != "null" {
|
|
fmt.Fprintf(stdout, " %s >= %s\n",
|
|
lastKey.Pretty(s.fmtKey.fn), key.Pretty(s.fmtKey.fn))
|
|
}
|
|
}
|
|
lastKey.Trailer = key.Trailer
|
|
lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...)
|
|
|
|
if prefixIter != nil {
|
|
n := r.Split(key.UserKey)
|
|
prefix := key.UserKey[:n]
|
|
key2, _ := prefixIter.SeekPrefixGE(prefix, key.UserKey, base.SeekGEFlagsNone)
|
|
if key2 == nil {
|
|
fmt.Fprintf(stdout, "WARNING: PREFIX ITERATION FAILURE!\n")
|
|
if s.fmtKey.spec != "null" {
|
|
fmt.Fprintf(stdout, " %s not found\n", key.Pretty(s.fmtKey.fn))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := iter.Close(); err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
}
|
|
if prefixIter != nil {
|
|
if err := prefixIter.Close(); err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *sstableT) runLayout(cmd *cobra.Command, args []string) {
|
|
stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
|
|
s.foreachSstable(stderr, args, func(arg string) {
|
|
f, err := s.opts.FS.Open(arg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(stdout, "%s\n", arg)
|
|
|
|
r, err := s.newReader(f)
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
// Update the internal formatter if this comparator has one specified.
|
|
s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
|
|
l, err := r.Layout()
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
fmtRecord := func(key *base.InternalKey, value []byte) {
|
|
formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, value)
|
|
}
|
|
if s.fmtKey.spec == "null" && s.fmtValue.spec == "null" {
|
|
fmtRecord = nil
|
|
}
|
|
l.Describe(stdout, s.verbose, r, fmtRecord)
|
|
})
|
|
}
|
|
|
|
func (s *sstableT) runProperties(cmd *cobra.Command, args []string) {
|
|
stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
|
|
s.foreachSstable(stderr, args, func(arg string) {
|
|
f, err := s.opts.FS.Open(arg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(stdout, "%s\n", arg)
|
|
|
|
r, err := s.newReader(f)
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
if s.verbose {
|
|
fmt.Fprintf(stdout, "%s", r.Properties.String())
|
|
return
|
|
}
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
formatNull := func(s string) string {
|
|
switch s {
|
|
case "", "nullptr":
|
|
return "-"
|
|
}
|
|
return s
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(stdout, 2, 1, 2, ' ', 0)
|
|
fmt.Fprintf(tw, "size\t\n")
|
|
fmt.Fprintf(tw, " file\t%s\n", humanize.Bytes.Int64(stat.Size()))
|
|
fmt.Fprintf(tw, " data\t%s\n", humanize.Bytes.Uint64(r.Properties.DataSize))
|
|
fmt.Fprintf(tw, " blocks\t%d\n", r.Properties.NumDataBlocks)
|
|
fmt.Fprintf(tw, " index\t%s\n", humanize.Bytes.Uint64(r.Properties.IndexSize))
|
|
fmt.Fprintf(tw, " blocks\t%d\n", 1+r.Properties.IndexPartitions)
|
|
fmt.Fprintf(tw, " top-level\t%s\n", humanize.Bytes.Uint64(r.Properties.TopLevelIndexSize))
|
|
fmt.Fprintf(tw, " filter\t%s\n", humanize.Bytes.Uint64(r.Properties.FilterSize))
|
|
fmt.Fprintf(tw, " raw-key\t%s\n", humanize.Bytes.Uint64(r.Properties.RawKeySize))
|
|
fmt.Fprintf(tw, " raw-value\t%s\n", humanize.Bytes.Uint64(r.Properties.RawValueSize))
|
|
fmt.Fprintf(tw, " pinned-key\t%d\n", r.Properties.SnapshotPinnedKeySize)
|
|
fmt.Fprintf(tw, " pinned-val\t%d\n", r.Properties.SnapshotPinnedValueSize)
|
|
fmt.Fprintf(tw, " point-del-key-size\t%d\n", r.Properties.RawPointTombstoneKeySize)
|
|
fmt.Fprintf(tw, " point-del-value-size\t%d\n", r.Properties.RawPointTombstoneValueSize)
|
|
fmt.Fprintf(tw, "records\t%d\n", r.Properties.NumEntries)
|
|
fmt.Fprintf(tw, " set\t%d\n", r.Properties.NumEntries-
|
|
(r.Properties.NumDeletions+r.Properties.NumMergeOperands))
|
|
fmt.Fprintf(tw, " delete\t%d\n", r.Properties.NumPointDeletions())
|
|
fmt.Fprintf(tw, " delete-sized\t%d\n", r.Properties.NumSizedDeletions)
|
|
fmt.Fprintf(tw, " range-delete\t%d\n", r.Properties.NumRangeDeletions)
|
|
fmt.Fprintf(tw, " range-key-set\t%d\n", r.Properties.NumRangeKeySets)
|
|
fmt.Fprintf(tw, " range-key-unset\t%d\n", r.Properties.NumRangeKeyUnsets)
|
|
fmt.Fprintf(tw, " range-key-delete\t%d\n", r.Properties.NumRangeKeyDels)
|
|
fmt.Fprintf(tw, " merge\t%d\n", r.Properties.NumMergeOperands)
|
|
fmt.Fprintf(tw, " global-seq-num\t%d\n", r.Properties.GlobalSeqNum)
|
|
fmt.Fprintf(tw, " pinned\t%d\n", r.Properties.SnapshotPinnedKeys)
|
|
fmt.Fprintf(tw, "index\t\n")
|
|
fmt.Fprintf(tw, " key\t")
|
|
fmt.Fprintf(tw, " value\t")
|
|
fmt.Fprintf(tw, "comparer\t%s\n", r.Properties.ComparerName)
|
|
fmt.Fprintf(tw, "merger\t%s\n", formatNull(r.Properties.MergerName))
|
|
fmt.Fprintf(tw, "filter\t%s\n", formatNull(r.Properties.FilterPolicyName))
|
|
fmt.Fprintf(tw, " prefix\t%t\n", r.Properties.PrefixFiltering)
|
|
fmt.Fprintf(tw, " whole-key\t%t\n", r.Properties.WholeKeyFiltering)
|
|
fmt.Fprintf(tw, "compression\t%s\n", r.Properties.CompressionName)
|
|
fmt.Fprintf(tw, " options\t%s\n", r.Properties.CompressionOptions)
|
|
fmt.Fprintf(tw, "user properties\t\n")
|
|
fmt.Fprintf(tw, " collectors\t%s\n", r.Properties.PropertyCollectorNames)
|
|
keys := make([]string, 0, len(r.Properties.UserProperties))
|
|
for key := range r.Properties.UserProperties {
|
|
keys = append(keys, key)
|
|
}
|
|
slices.Sort(keys)
|
|
for _, key := range keys {
|
|
fmt.Fprintf(tw, " %s\t%s\n", key, r.Properties.UserProperties[key])
|
|
}
|
|
tw.Flush()
|
|
})
|
|
}
|
|
|
|
func (s *sstableT) runScan(cmd *cobra.Command, args []string) {
|
|
stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
|
|
s.foreachSstable(stderr, args, func(arg string) {
|
|
f, err := s.opts.FS.Open(arg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
|
|
// In filter-mode, we prefix ever line that is output with the sstable
|
|
// filename.
|
|
var prefix string
|
|
if s.filter == nil {
|
|
fmt.Fprintf(stdout, "%s\n", arg)
|
|
} else {
|
|
prefix = fmt.Sprintf("%s: ", arg)
|
|
}
|
|
|
|
r, err := s.newReader(f)
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s%s\n", prefix, err)
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
// Update the internal formatter if this comparator has one specified.
|
|
s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
|
|
|
|
iter, err := r.NewIter(nil, s.end)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s%s\n", prefix, err)
|
|
return
|
|
}
|
|
defer iter.Close()
|
|
key, value := iter.SeekGE(s.start, base.SeekGEFlagsNone)
|
|
|
|
// We configured sstable.Reader to return raw tombstones which requires a
|
|
// bit more work here to put them in a form that can be iterated in
|
|
// parallel with the point records.
|
|
rangeDelIter, err := func() (keyspan.FragmentIterator, error) {
|
|
iter, err := r.NewRawRangeDelIter()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if iter == nil {
|
|
return keyspan.NewIter(r.Compare, nil), nil
|
|
}
|
|
defer iter.Close()
|
|
|
|
var tombstones []keyspan.Span
|
|
for t := iter.First(); t != nil; t = iter.Next() {
|
|
if s.end != nil && r.Compare(s.end, t.Start) <= 0 {
|
|
// The range tombstone lies after the scan range.
|
|
continue
|
|
}
|
|
if r.Compare(s.start, t.End) >= 0 {
|
|
// The range tombstone lies before the scan range.
|
|
continue
|
|
}
|
|
tombstones = append(tombstones, t.ShallowClone())
|
|
}
|
|
|
|
slices.SortFunc(tombstones, func(a, b keyspan.Span) int {
|
|
return r.Compare(a.Start, b.Start)
|
|
})
|
|
return keyspan.NewIter(r.Compare, tombstones), nil
|
|
}()
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s%s\n", prefix, err)
|
|
return
|
|
}
|
|
|
|
defer rangeDelIter.Close()
|
|
rangeDel := rangeDelIter.First()
|
|
count := s.count
|
|
|
|
var lastKey base.InternalKey
|
|
for key != nil || rangeDel != nil {
|
|
if key != nil && (rangeDel == nil || r.Compare(key.UserKey, rangeDel.Start) < 0) {
|
|
// The filter specifies a prefix of the key.
|
|
//
|
|
// TODO(peter): Is using prefix comparison like this kosher for all
|
|
// comparers? Probably not, but it is for common ones such as the
|
|
// Pebble default and CockroachDB's comparer.
|
|
if s.filter == nil || bytes.HasPrefix(key.UserKey, s.filter) {
|
|
fmt.Fprint(stdout, prefix)
|
|
v, _, err := value.Value(nil)
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s%s\n", prefix, err)
|
|
return
|
|
}
|
|
formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, v)
|
|
|
|
}
|
|
if base.InternalCompare(r.Compare, lastKey, *key) >= 0 {
|
|
fmt.Fprintf(stdout, "%s WARNING: OUT OF ORDER KEYS!\n", prefix)
|
|
}
|
|
lastKey.Trailer = key.Trailer
|
|
lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...)
|
|
key, value = iter.Next()
|
|
} else {
|
|
// If a filter is specified, we want to output any range tombstone
|
|
// which overlaps the prefix. The comparison on the start key is
|
|
// somewhat complex. Consider the tombstone [aaa,ccc). We want to
|
|
// output this tombstone if filter is "aa", and if it "bbb".
|
|
if s.filter == nil ||
|
|
((r.Compare(s.filter, rangeDel.Start) >= 0 ||
|
|
bytes.HasPrefix(rangeDel.Start, s.filter)) &&
|
|
r.Compare(s.filter, rangeDel.End) < 0) {
|
|
fmt.Fprint(stdout, prefix)
|
|
if err := rangedel.Encode(rangeDel, func(k base.InternalKey, v []byte) error {
|
|
formatKeyValue(stdout, s.fmtKey, s.fmtValue, &k, v)
|
|
return nil
|
|
}); err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
rangeDel = rangeDelIter.Next()
|
|
}
|
|
|
|
if count > 0 {
|
|
count--
|
|
if count == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle range keys.
|
|
rkIter, err := r.NewRawRangeKeyIter()
|
|
if err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if rkIter != nil {
|
|
defer rkIter.Close()
|
|
for span := rkIter.SeekGE(s.start); span != nil; span = rkIter.Next() {
|
|
// By default, emit the key, unless there is a filter.
|
|
emit := s.filter == nil
|
|
// Skip spans that start after the end key (if provided). End keys are
|
|
// exclusive, e.g. [a, b), so we consider the interval [b, +inf).
|
|
if s.end != nil && r.Compare(span.Start, s.end) >= 0 {
|
|
emit = false
|
|
}
|
|
// Filters override the provided start / end bounds, if provided.
|
|
if s.filter != nil && bytes.HasPrefix(span.Start, s.filter) {
|
|
// In filter mode, each line is prefixed with the filename.
|
|
fmt.Fprint(stdout, prefix)
|
|
emit = true
|
|
}
|
|
if emit {
|
|
formatSpan(stdout, s.fmtKey, s.fmtValue, span)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := iter.Close(); err != nil {
|
|
fmt.Fprintf(stdout, "%s\n", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *sstableT) runSpace(cmd *cobra.Command, args []string) {
|
|
stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
|
|
s.foreachSstable(stderr, args, func(arg string) {
|
|
f, err := s.opts.FS.Open(arg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
r, err := s.newReader(f)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
bytes, err := r.EstimateDiskUsage(s.start, s.end)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "%s\n", err)
|
|
return
|
|
}
|
|
fmt.Fprintf(stdout, "%s: %d\n", arg, bytes)
|
|
})
|
|
}
|
|
|
|
func (s *sstableT) foreachSstable(stderr io.Writer, args []string, fn func(arg string)) {
|
|
// Loop over args, invoking fn for each file. Each directory is recursively
|
|
// listed and fn is invoked on any file with an .sst or .ldb suffix.
|
|
for _, arg := range args {
|
|
info, err := s.opts.FS.Stat(arg)
|
|
if err != nil || !info.IsDir() {
|
|
fn(arg)
|
|
continue
|
|
}
|
|
walk(stderr, s.opts.FS, arg, func(path string) {
|
|
switch filepath.Ext(path) {
|
|
case ".sst", ".ldb":
|
|
fn(path)
|
|
}
|
|
})
|
|
}
|
|
}
|