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