Files
kopia/cli/command_content_verify.go
T
John Costa b16302dca9 fix(cli): reject negative values for --parallel flags (#5484)
The --parallel flag was declared with kingpin .IntVar on 12 commands,
which accepted negative values and led to a panic at runtime (e.g.
`snapshot migrate --parallel=-1`).

Declare the --parallel family as uint so kingpin rejects negative input
at parse time.
The uint->int conversion the callee APIs need is done through a small
helper .

Add CLI test that asserts the rejection message (a bare failure check passed
even without the fix since the commands also fail with no repository connected).
Parse via ParseFloat like kingpin's built-in int flag so the only behavior
change is rejecting negatives.
2026-09-09 20:07:17 -07:00

127 lines
3.4 KiB
Go

package cli
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/timetrack"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/content"
)
type commandContentVerify struct {
contentVerifyParallel uint
contentVerifyFull bool
contentVerifyIncludeDeleted bool
contentVerifyPercent float64
progressInterval time.Duration
contentRange contentRangeFlags
}
func (c *commandContentVerify) setup(svc appServices, parent commandParent) {
cmd := parent.Command("verify", "Verify that each content is backed by a valid blob")
cmd.Flag("parallel", "Parallelism").Default("16").UintVar(&c.contentVerifyParallel)
cmd.Flag("full", "Full verification (including download)").BoolVar(&c.contentVerifyFull)
cmd.Flag("include-deleted", "Include deleted contents").BoolVar(&c.contentVerifyIncludeDeleted)
cmd.Flag("download-percent", "Download a percentage of files [0.0 .. 100.0]").Float64Var(&c.contentVerifyPercent)
cmd.Flag("progress-interval", "Progress output interval").Default("3s").DurationVar(&c.progressInterval)
c.contentRange.setup(cmd)
cmd.Action(svc.directRepositoryReadAction(c.run))
}
func (c *commandContentVerify) run(ctx context.Context, rep repo.DirectRepository) error {
var (
totalCount atomic.Int32
wg sync.WaitGroup
)
subctx, cancel := context.WithCancel(ctx)
// ensure we cancel estimation goroutine and wait for it before returning
defer func() {
cancel()
wg.Wait()
}()
// start a goroutine that will populate totalCount
wg.Go(func() {
c.getTotalContentCount(subctx, rep, &totalCount)
})
rep.DisableIndexRefresh()
var throttle timetrack.Throttle
est := timetrack.Start()
if c.contentVerifyFull {
c.contentVerifyPercent = 100.0
}
opts := content.VerifyOptions{
ContentIDRange: c.contentRange.contentIDRange(),
ContentReadPercentage: c.contentVerifyPercent,
IncludeDeletedContents: c.contentVerifyIncludeDeleted,
ContentIterateParallelism: parallelismAsInt(c.contentVerifyParallel),
ProgressCallbackInterval: 1,
ProgressCallback: func(vps content.VerifyProgressStats) {
if !throttle.ShouldOutput(c.progressInterval) {
return
}
verifiedCount := vps.SuccessCount + vps.ErrorCount
timings, ok := est.Estimate(float64(verifiedCount), float64(totalCount.Load()))
if ok {
log(ctx).Infof(" Verified %v of %v contents (%.1f%%), %v errors, remaining %v, ETA %v",
verifiedCount,
totalCount.Load(),
timings.PercentComplete,
vps.ErrorCount,
timings.Remaining,
formatTimestamp(timings.EstimatedEndTime),
)
} else {
log(ctx).Infof(" Verified %v contents, %v errors, estimating...", verifiedCount, vps.ErrorCount)
}
},
}
if err := rep.ContentReader().VerifyContents(ctx, opts); err != nil {
return errors.Wrap(err, "verify contents")
}
return nil
}
func (c *commandContentVerify) getTotalContentCount(ctx context.Context, rep repo.DirectRepository, totalCount *atomic.Int32) {
var tc int32
if err := rep.ContentReader().IterateContents(ctx, content.IterateOptions{
Range: c.contentRange.contentIDRange(),
IncludeDeleted: c.contentVerifyIncludeDeleted,
}, func(_ content.Info) error {
if err := ctx.Err(); err != nil {
return errors.Wrap(err, "context error")
}
tc++
return nil
}); err != nil {
log(ctx).Debugf("error estimating content count: %v", err)
return
}
totalCount.Store(tc)
}