Line data Source code
1 : // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2 : // of this source code is governed by a BSD-style license that can be found in
3 : // the LICENSE file.
4 :
5 : package tool
6 :
7 : import (
8 : "bytes"
9 : "fmt"
10 : "io"
11 : "os"
12 : "path/filepath"
13 : "slices"
14 : "text/tabwriter"
15 :
16 : "github.com/cockroachdb/pebble"
17 : "github.com/cockroachdb/pebble/internal/base"
18 : "github.com/cockroachdb/pebble/internal/humanize"
19 : "github.com/cockroachdb/pebble/internal/keyspan"
20 : "github.com/cockroachdb/pebble/internal/private"
21 : "github.com/cockroachdb/pebble/internal/rangedel"
22 : "github.com/cockroachdb/pebble/sstable"
23 : "github.com/cockroachdb/pebble/vfs"
24 : "github.com/spf13/cobra"
25 : )
26 :
27 : // sstableT implements sstable-level tools, including both configuration state
28 : // and the commands themselves.
29 : type sstableT struct {
30 : Root *cobra.Command
31 : Check *cobra.Command
32 : Layout *cobra.Command
33 : Properties *cobra.Command
34 : Scan *cobra.Command
35 : Space *cobra.Command
36 :
37 : // Configuration and state.
38 : opts *pebble.Options
39 : comparers sstable.Comparers
40 : mergers sstable.Mergers
41 :
42 : // Flags.
43 : fmtKey keyFormatter
44 : fmtValue valueFormatter
45 : start key
46 : end key
47 : filter key
48 : count int64
49 : verbose bool
50 : }
51 :
52 : func newSSTable(
53 : opts *pebble.Options, comparers sstable.Comparers, mergers sstable.Mergers,
54 1 : ) *sstableT {
55 1 : s := &sstableT{
56 1 : opts: opts,
57 1 : comparers: comparers,
58 1 : mergers: mergers,
59 1 : }
60 1 : s.fmtKey.mustSet("quoted")
61 1 : s.fmtValue.mustSet("[%x]")
62 1 :
63 1 : s.Root = &cobra.Command{
64 1 : Use: "sstable",
65 1 : Short: "sstable introspection tools",
66 1 : }
67 1 : s.Check = &cobra.Command{
68 1 : Use: "check <sstables>",
69 1 : Short: "verify checksums and metadata",
70 1 : Long: ``,
71 1 : Args: cobra.MinimumNArgs(1),
72 1 : Run: s.runCheck,
73 1 : }
74 1 : s.Layout = &cobra.Command{
75 1 : Use: "layout <sstables>",
76 1 : Short: "print sstable block and record layout",
77 1 : Long: `
78 1 : Print the layout for the sstables. The -v flag controls whether record layout
79 1 : is displayed or omitted.
80 1 : `,
81 1 : Args: cobra.MinimumNArgs(1),
82 1 : Run: s.runLayout,
83 1 : }
84 1 : s.Properties = &cobra.Command{
85 1 : Use: "properties <sstables>",
86 1 : Short: "print sstable properties",
87 1 : Long: `
88 1 : Print the properties for the sstables. The -v flag controls whether the
89 1 : properties are pretty-printed or displayed in a verbose/raw format.
90 1 : `,
91 1 : Args: cobra.MinimumNArgs(1),
92 1 : Run: s.runProperties,
93 1 : }
94 1 : s.Scan = &cobra.Command{
95 1 : Use: "scan <sstables>",
96 1 : Short: "print sstable records",
97 1 : Long: `
98 1 : Print the records in the sstables. The sstables are scanned in command line
99 1 : order which means the records will be printed in that order. Raw range
100 1 : tombstones are displayed interleaved with point records.
101 1 : `,
102 1 : Args: cobra.MinimumNArgs(1),
103 1 : Run: s.runScan,
104 1 : }
105 1 : s.Space = &cobra.Command{
106 1 : Use: "space <sstables>",
107 1 : Short: "print filesystem space used",
108 1 : Long: `
109 1 : Print the estimated space usage in the specified files for the
110 1 : inclusive-inclusive range specified by --start and --end.
111 1 : `,
112 1 : Args: cobra.MinimumNArgs(1),
113 1 : Run: s.runSpace,
114 1 : }
115 1 :
116 1 : s.Root.AddCommand(s.Check, s.Layout, s.Properties, s.Scan, s.Space)
117 1 : s.Root.PersistentFlags().BoolVarP(&s.verbose, "verbose", "v", false, "verbose output")
118 1 :
119 1 : s.Check.Flags().Var(
120 1 : &s.fmtKey, "key", "key formatter")
121 1 : s.Layout.Flags().Var(
122 1 : &s.fmtKey, "key", "key formatter")
123 1 : s.Layout.Flags().Var(
124 1 : &s.fmtValue, "value", "value formatter")
125 1 : s.Scan.Flags().Var(
126 1 : &s.fmtKey, "key", "key formatter")
127 1 : s.Scan.Flags().Var(
128 1 : &s.fmtValue, "value", "value formatter")
129 1 : for _, cmd := range []*cobra.Command{s.Scan, s.Space} {
130 1 : cmd.Flags().Var(
131 1 : &s.start, "start", "start key for the range")
132 1 : cmd.Flags().Var(
133 1 : &s.end, "end", "end key for the range")
134 1 : }
135 1 : s.Scan.Flags().Var(
136 1 : &s.filter, "filter", "only output records with matching prefix or overlapping range tombstones")
137 1 : s.Scan.Flags().Int64Var(
138 1 : &s.count, "count", 0, "key count for scan (0 is unlimited)")
139 1 :
140 1 : return s
141 : }
142 :
143 1 : func (s *sstableT) newReader(f vfs.File) (*sstable.Reader, error) {
144 1 : readable, err := sstable.NewSimpleReadable(f)
145 1 : if err != nil {
146 0 : return nil, err
147 0 : }
148 1 : o := sstable.ReaderOptions{
149 1 : Cache: pebble.NewCache(128 << 20 /* 128 MB */),
150 1 : Comparer: s.opts.Comparer,
151 1 : Filters: s.opts.Filters,
152 1 : }
153 1 : defer o.Cache.Unref()
154 1 : return sstable.NewReader(readable, o, s.comparers, s.mergers,
155 1 : private.SSTableRawTombstonesOpt.(sstable.ReaderOption))
156 : }
157 :
158 1 : func (s *sstableT) runCheck(cmd *cobra.Command, args []string) {
159 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
160 1 : s.foreachSstable(stderr, args, func(arg string) {
161 1 : f, err := s.opts.FS.Open(arg)
162 1 : if err != nil {
163 0 : fmt.Fprintf(stderr, "%s\n", err)
164 0 : return
165 0 : }
166 :
167 1 : fmt.Fprintf(stdout, "%s\n", arg)
168 1 :
169 1 : r, err := s.newReader(f)
170 1 :
171 1 : if err != nil {
172 1 : fmt.Fprintf(stdout, "%s\n", err)
173 1 : return
174 1 : }
175 1 : defer r.Close()
176 1 :
177 1 : // Update the internal formatter if this comparator has one specified.
178 1 : s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
179 1 : s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
180 1 :
181 1 : iter, err := r.NewIter(sstable.NoTransforms, nil, nil)
182 1 : if err != nil {
183 0 : fmt.Fprintf(stderr, "%s\n", err)
184 0 : return
185 0 : }
186 :
187 : // Verify that SeekPrefixGE can find every key in the table.
188 1 : prefixIter, err := r.NewIter(sstable.NoTransforms, nil, nil)
189 1 : if err != nil {
190 0 : fmt.Fprintf(stderr, "%s\n", err)
191 0 : return
192 0 : }
193 :
194 1 : var lastKey base.InternalKey
195 1 : for kv := iter.First(); kv != nil; kv = iter.Next() {
196 1 : if base.InternalCompare(r.Compare, lastKey, kv.K) >= 0 {
197 1 : fmt.Fprintf(stdout, "WARNING: OUT OF ORDER KEYS!\n")
198 1 : if s.fmtKey.spec != "null" {
199 1 : fmt.Fprintf(stdout, " %s >= %s\n",
200 1 : lastKey.Pretty(s.fmtKey.fn), kv.K.Pretty(s.fmtKey.fn))
201 1 : }
202 : }
203 1 : lastKey.Trailer = kv.K.Trailer
204 1 : lastKey.UserKey = append(lastKey.UserKey[:0], kv.K.UserKey...)
205 1 :
206 1 : n := r.Split(kv.K.UserKey)
207 1 : prefix := kv.K.UserKey[:n]
208 1 : kv2 := prefixIter.SeekPrefixGE(prefix, kv.K.UserKey, base.SeekGEFlagsNone)
209 1 : if kv2 == nil {
210 0 : fmt.Fprintf(stdout, "WARNING: PREFIX ITERATION FAILURE!\n")
211 0 : if s.fmtKey.spec != "null" {
212 0 : fmt.Fprintf(stdout, " %s not found\n", kv.K.Pretty(s.fmtKey.fn))
213 0 : }
214 : }
215 : }
216 :
217 1 : if err := iter.Close(); err != nil {
218 0 : fmt.Fprintf(stdout, "%s\n", err)
219 0 : }
220 1 : if err := prefixIter.Close(); err != nil {
221 0 : fmt.Fprintf(stdout, "%s\n", err)
222 0 : }
223 : })
224 : }
225 :
226 1 : func (s *sstableT) runLayout(cmd *cobra.Command, args []string) {
227 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
228 1 : s.foreachSstable(stderr, args, func(arg string) {
229 1 : f, err := s.opts.FS.Open(arg)
230 1 : if err != nil {
231 0 : fmt.Fprintf(stderr, "%s\n", err)
232 0 : return
233 0 : }
234 :
235 1 : fmt.Fprintf(stdout, "%s\n", arg)
236 1 :
237 1 : r, err := s.newReader(f)
238 1 : if err != nil {
239 0 : fmt.Fprintf(stdout, "%s\n", err)
240 0 : return
241 0 : }
242 1 : defer r.Close()
243 1 :
244 1 : // Update the internal formatter if this comparator has one specified.
245 1 : s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
246 1 : s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
247 1 :
248 1 : l, err := r.Layout()
249 1 : if err != nil {
250 0 : fmt.Fprintf(stderr, "%s\n", err)
251 0 : return
252 0 : }
253 1 : fmtRecord := func(key *base.InternalKey, value []byte) {
254 1 : formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, value)
255 1 : }
256 1 : if s.fmtKey.spec == "null" && s.fmtValue.spec == "null" {
257 0 : fmtRecord = nil
258 0 : }
259 1 : l.Describe(stdout, s.verbose, r, fmtRecord)
260 : })
261 : }
262 :
263 1 : func (s *sstableT) runProperties(cmd *cobra.Command, args []string) {
264 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
265 1 : s.foreachSstable(stderr, args, func(arg string) {
266 1 : f, err := s.opts.FS.Open(arg)
267 1 : if err != nil {
268 1 : fmt.Fprintf(stderr, "%s\n", err)
269 1 : return
270 1 : }
271 :
272 1 : fmt.Fprintf(stdout, "%s\n", arg)
273 1 :
274 1 : r, err := s.newReader(f)
275 1 : if err != nil {
276 1 : fmt.Fprintf(stdout, "%s\n", err)
277 1 : return
278 1 : }
279 1 : defer r.Close()
280 1 :
281 1 : if s.verbose {
282 1 : fmt.Fprintf(stdout, "%s", r.Properties.String())
283 1 : return
284 1 : }
285 :
286 1 : stat, err := f.Stat()
287 1 : if err != nil {
288 0 : fmt.Fprintf(stderr, "%s\n", err)
289 0 : return
290 0 : }
291 :
292 1 : formatNull := func(s string) string {
293 1 : switch s {
294 1 : case "", "nullptr":
295 1 : return "-"
296 : }
297 1 : return s
298 : }
299 :
300 1 : tw := tabwriter.NewWriter(stdout, 2, 1, 2, ' ', 0)
301 1 : fmt.Fprintf(tw, "size\t\n")
302 1 : fmt.Fprintf(tw, " file\t%s\n", humanize.Bytes.Int64(stat.Size()))
303 1 : fmt.Fprintf(tw, " data\t%s\n", humanize.Bytes.Uint64(r.Properties.DataSize))
304 1 : fmt.Fprintf(tw, " blocks\t%d\n", r.Properties.NumDataBlocks)
305 1 : fmt.Fprintf(tw, " index\t%s\n", humanize.Bytes.Uint64(r.Properties.IndexSize))
306 1 : fmt.Fprintf(tw, " blocks\t%d\n", 1+r.Properties.IndexPartitions)
307 1 : fmt.Fprintf(tw, " top-level\t%s\n", humanize.Bytes.Uint64(r.Properties.TopLevelIndexSize))
308 1 : fmt.Fprintf(tw, " filter\t%s\n", humanize.Bytes.Uint64(r.Properties.FilterSize))
309 1 : fmt.Fprintf(tw, " raw-key\t%s\n", humanize.Bytes.Uint64(r.Properties.RawKeySize))
310 1 : fmt.Fprintf(tw, " raw-value\t%s\n", humanize.Bytes.Uint64(r.Properties.RawValueSize))
311 1 : fmt.Fprintf(tw, " pinned-key\t%d\n", r.Properties.SnapshotPinnedKeySize)
312 1 : fmt.Fprintf(tw, " pinned-val\t%d\n", r.Properties.SnapshotPinnedValueSize)
313 1 : fmt.Fprintf(tw, " point-del-key-size\t%d\n", r.Properties.RawPointTombstoneKeySize)
314 1 : fmt.Fprintf(tw, " point-del-value-size\t%d\n", r.Properties.RawPointTombstoneValueSize)
315 1 : fmt.Fprintf(tw, "records\t%d\n", r.Properties.NumEntries)
316 1 : fmt.Fprintf(tw, " set\t%d\n", r.Properties.NumEntries-
317 1 : (r.Properties.NumDeletions+r.Properties.NumMergeOperands))
318 1 : fmt.Fprintf(tw, " delete\t%d\n", r.Properties.NumPointDeletions())
319 1 : fmt.Fprintf(tw, " delete-sized\t%d\n", r.Properties.NumSizedDeletions)
320 1 : fmt.Fprintf(tw, " range-delete\t%d\n", r.Properties.NumRangeDeletions)
321 1 : fmt.Fprintf(tw, " range-key-set\t%d\n", r.Properties.NumRangeKeySets)
322 1 : fmt.Fprintf(tw, " range-key-unset\t%d\n", r.Properties.NumRangeKeyUnsets)
323 1 : fmt.Fprintf(tw, " range-key-delete\t%d\n", r.Properties.NumRangeKeyDels)
324 1 : fmt.Fprintf(tw, " merge\t%d\n", r.Properties.NumMergeOperands)
325 1 : fmt.Fprintf(tw, " pinned\t%d\n", r.Properties.SnapshotPinnedKeys)
326 1 : fmt.Fprintf(tw, "index\t\n")
327 1 : fmt.Fprintf(tw, " key\t")
328 1 : fmt.Fprintf(tw, " value\t")
329 1 : fmt.Fprintf(tw, "comparer\t%s\n", r.Properties.ComparerName)
330 1 : fmt.Fprintf(tw, "merger\t%s\n", formatNull(r.Properties.MergerName))
331 1 : fmt.Fprintf(tw, "filter\t%s\n", formatNull(r.Properties.FilterPolicyName))
332 1 : fmt.Fprintf(tw, "compression\t%s\n", r.Properties.CompressionName)
333 1 : fmt.Fprintf(tw, " options\t%s\n", r.Properties.CompressionOptions)
334 1 : fmt.Fprintf(tw, "user properties\t\n")
335 1 : fmt.Fprintf(tw, " collectors\t%s\n", r.Properties.PropertyCollectorNames)
336 1 : keys := make([]string, 0, len(r.Properties.UserProperties))
337 1 : for key := range r.Properties.UserProperties {
338 1 : keys = append(keys, key)
339 1 : }
340 1 : slices.Sort(keys)
341 1 : for _, key := range keys {
342 1 : fmt.Fprintf(tw, " %s\t%s\n", key, r.Properties.UserProperties[key])
343 1 : }
344 1 : tw.Flush()
345 : })
346 : }
347 :
348 1 : func (s *sstableT) runScan(cmd *cobra.Command, args []string) {
349 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
350 1 : s.foreachSstable(stderr, args, func(arg string) {
351 1 : f, err := s.opts.FS.Open(arg)
352 1 : if err != nil {
353 0 : fmt.Fprintf(stderr, "%s\n", err)
354 0 : return
355 0 : }
356 :
357 : // In filter-mode, we prefix ever line that is output with the sstable
358 : // filename.
359 1 : var prefix string
360 1 : if s.filter == nil {
361 1 : fmt.Fprintf(stdout, "%s\n", arg)
362 1 : } else {
363 1 : prefix = fmt.Sprintf("%s: ", arg)
364 1 : }
365 :
366 1 : r, err := s.newReader(f)
367 1 : if err != nil {
368 0 : fmt.Fprintf(stdout, "%s%s\n", prefix, err)
369 0 : return
370 0 : }
371 1 : defer r.Close()
372 1 :
373 1 : // Update the internal formatter if this comparator has one specified.
374 1 : s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers)
375 1 : s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers)
376 1 :
377 1 : iter, err := r.NewIter(sstable.NoTransforms, nil, s.end)
378 1 : if err != nil {
379 0 : fmt.Fprintf(stderr, "%s%s\n", prefix, err)
380 0 : return
381 0 : }
382 1 : iterCloser := base.CloseHelper(iter)
383 1 : defer iterCloser.Close()
384 1 : kv := iter.SeekGE(s.start, base.SeekGEFlagsNone)
385 1 :
386 1 : // We configured sstable.Reader to return raw tombstones which requires a
387 1 : // bit more work here to put them in a form that can be iterated in
388 1 : // parallel with the point records.
389 1 : rangeDelIter, err := func() (keyspan.FragmentIterator, error) {
390 1 : iter, err := r.NewRawRangeDelIter(sstable.NoTransforms)
391 1 : if err != nil {
392 0 : return nil, err
393 0 : }
394 1 : if iter == nil {
395 1 : return keyspan.NewIter(r.Compare, nil), nil
396 1 : }
397 1 : defer iter.Close()
398 1 :
399 1 : var tombstones []keyspan.Span
400 1 : t, err := iter.First()
401 1 : for ; t != nil; t, err = iter.Next() {
402 1 : if s.end != nil && r.Compare(s.end, t.Start) <= 0 {
403 1 : // The range tombstone lies after the scan range.
404 1 : continue
405 : }
406 1 : if r.Compare(s.start, t.End) >= 0 {
407 1 : // The range tombstone lies before the scan range.
408 1 : continue
409 : }
410 1 : tombstones = append(tombstones, t.ShallowClone())
411 : }
412 1 : if err != nil {
413 0 : return nil, err
414 0 : }
415 :
416 1 : slices.SortFunc(tombstones, func(a, b keyspan.Span) int {
417 1 : return r.Compare(a.Start, b.Start)
418 1 : })
419 1 : return keyspan.NewIter(r.Compare, tombstones), nil
420 : }()
421 1 : if err != nil {
422 0 : fmt.Fprintf(stdout, "%s%s\n", prefix, err)
423 0 : return
424 0 : }
425 :
426 1 : defer rangeDelIter.Close()
427 1 : rangeDel, err := rangeDelIter.First()
428 1 : if err != nil {
429 0 : fmt.Fprintf(stdout, "%s%s\n", prefix, err)
430 0 : return
431 0 : }
432 1 : count := s.count
433 1 :
434 1 : var lastKey base.InternalKey
435 1 : for kv != nil || rangeDel != nil {
436 1 : if kv != nil && (rangeDel == nil || r.Compare(kv.K.UserKey, rangeDel.Start) < 0) {
437 1 : // The filter specifies a prefix of the key.
438 1 : //
439 1 : // TODO(peter): Is using prefix comparison like this kosher for all
440 1 : // comparers? Probably not, but it is for common ones such as the
441 1 : // Pebble default and CockroachDB's comparer.
442 1 : if s.filter == nil || bytes.HasPrefix(kv.K.UserKey, s.filter) {
443 1 : fmt.Fprint(stdout, prefix)
444 1 : v, _, err := kv.Value(nil)
445 1 : if err != nil {
446 0 : fmt.Fprintf(stdout, "%s%s\n", prefix, err)
447 0 : return
448 0 : }
449 1 : formatKeyValue(stdout, s.fmtKey, s.fmtValue, &kv.K, v)
450 :
451 : }
452 1 : if base.InternalCompare(r.Compare, lastKey, kv.K) >= 0 {
453 1 : fmt.Fprintf(stdout, "%s WARNING: OUT OF ORDER KEYS!\n", prefix)
454 1 : }
455 1 : lastKey.Trailer = kv.K.Trailer
456 1 : lastKey.UserKey = append(lastKey.UserKey[:0], kv.K.UserKey...)
457 1 : kv = iter.Next()
458 1 : } else {
459 1 : // If a filter is specified, we want to output any range tombstone
460 1 : // which overlaps the prefix. The comparison on the start key is
461 1 : // somewhat complex. Consider the tombstone [aaa,ccc). We want to
462 1 : // output this tombstone if filter is "aa", and if it "bbb".
463 1 : if s.filter == nil ||
464 1 : ((r.Compare(s.filter, rangeDel.Start) >= 0 ||
465 1 : bytes.HasPrefix(rangeDel.Start, s.filter)) &&
466 1 : r.Compare(s.filter, rangeDel.End) < 0) {
467 1 : fmt.Fprint(stdout, prefix)
468 1 : if err := rangedel.Encode(rangeDel, func(k base.InternalKey, v []byte) error {
469 1 : formatKeyValue(stdout, s.fmtKey, s.fmtValue, &k, v)
470 1 : return nil
471 1 : }); err != nil {
472 0 : fmt.Fprintf(stdout, "%s\n", err)
473 0 : os.Exit(1)
474 0 : }
475 : }
476 1 : rangeDel, err = rangeDelIter.Next()
477 1 : if err != nil {
478 0 : fmt.Fprintf(stdout, "%s\n", err)
479 0 : os.Exit(1)
480 0 : }
481 : }
482 :
483 1 : if count > 0 {
484 1 : count--
485 1 : if count == 0 {
486 1 : break
487 : }
488 : }
489 : }
490 :
491 : // Handle range keys.
492 1 : rkIter, err := r.NewRawRangeKeyIter(sstable.NoTransforms)
493 1 : if err != nil {
494 0 : fmt.Fprintf(stdout, "%s\n", err)
495 0 : os.Exit(1)
496 0 : }
497 1 : if rkIter != nil {
498 1 : defer rkIter.Close()
499 1 : span, err := rkIter.SeekGE(s.start)
500 1 : for ; span != nil; span, err = rkIter.Next() {
501 1 : // By default, emit the key, unless there is a filter.
502 1 : emit := s.filter == nil
503 1 : // Skip spans that start after the end key (if provided). End keys are
504 1 : // exclusive, e.g. [a, b), so we consider the interval [b, +inf).
505 1 : if s.end != nil && r.Compare(span.Start, s.end) >= 0 {
506 0 : emit = false
507 0 : }
508 : // Filters override the provided start / end bounds, if provided.
509 1 : if s.filter != nil && bytes.HasPrefix(span.Start, s.filter) {
510 1 : // In filter mode, each line is prefixed with the filename.
511 1 : fmt.Fprint(stdout, prefix)
512 1 : emit = true
513 1 : }
514 1 : if emit {
515 1 : formatSpan(stdout, s.fmtKey, s.fmtValue, span)
516 1 : }
517 : }
518 1 : if err != nil {
519 0 : fmt.Fprintf(stdout, "%s\n", err)
520 0 : os.Exit(1)
521 0 : }
522 : }
523 :
524 1 : if err := iterCloser.Close(); err != nil {
525 0 : fmt.Fprintf(stdout, "%s\n", err)
526 0 : }
527 : })
528 : }
529 :
530 1 : func (s *sstableT) runSpace(cmd *cobra.Command, args []string) {
531 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
532 1 : s.foreachSstable(stderr, args, func(arg string) {
533 1 : f, err := s.opts.FS.Open(arg)
534 1 : if err != nil {
535 0 : fmt.Fprintf(stderr, "%s\n", err)
536 0 : return
537 0 : }
538 1 : r, err := s.newReader(f)
539 1 : if err != nil {
540 0 : fmt.Fprintf(stderr, "%s\n", err)
541 0 : return
542 0 : }
543 1 : defer r.Close()
544 1 :
545 1 : bytes, err := r.EstimateDiskUsage(s.start, s.end)
546 1 : if err != nil {
547 0 : fmt.Fprintf(stderr, "%s\n", err)
548 0 : return
549 0 : }
550 1 : fmt.Fprintf(stdout, "%s: %d\n", arg, bytes)
551 : })
552 : }
553 :
554 1 : func (s *sstableT) foreachSstable(stderr io.Writer, args []string, fn func(arg string)) {
555 1 : // Loop over args, invoking fn for each file. Each directory is recursively
556 1 : // listed and fn is invoked on any file with an .sst or .ldb suffix.
557 1 : for _, arg := range args {
558 1 : info, err := s.opts.FS.Stat(arg)
559 1 : if err != nil || !info.IsDir() {
560 1 : fn(arg)
561 1 : continue
562 : }
563 1 : walk(stderr, s.opts.FS, arg, func(path string) {
564 1 : switch filepath.Ext(path) {
565 1 : case ".sst", ".ldb":
566 1 : fn(path)
567 : }
568 : })
569 : }
570 : }
|