restic: RunCheck with subset% + lock-state sniffing
Add CheckResult (LockPresent, ErrorsFound) and RunCheck. subsetPct>0 passes --read-data-subset N% to limit data reads. Stderr is sniffed for "Found stale lock"/"locked" to set LockPresent; a non-zero exit from restic is absorbed as ErrorsFound=true rather than an error so the caller can always persist last_check_status. Tests cover lock detection, exit-1 absorption, and subset-arg plumbing.
This commit is contained in:
@@ -293,6 +293,59 @@ func runWithPump(cmd *exec.Cmd, handle LineHandler) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckResult summarizes a `restic check` invocation. LockPresent is
|
||||
// true if the stderr stream contained a stale-lock signal (caller is
|
||||
// expected to surface this in the UI so the operator can run unlock).
|
||||
// ErrorsFound is true if check exited with a non-zero status (errors
|
||||
// detected in repo metadata).
|
||||
type CheckResult struct {
|
||||
LockPresent bool
|
||||
ErrorsFound bool
|
||||
}
|
||||
|
||||
// RunCheck executes `restic check` with optional --read-data-subset.
|
||||
// subsetPct of 0 omits the flag (full data check); >0 passes
|
||||
// --read-data-subset N%. Returns a CheckResult summarizing what was
|
||||
// sniffed from stderr; the result is set even if check itself
|
||||
// returns an error (so the caller can persist last_check_status).
|
||||
func (e Env) RunCheck(ctx context.Context, subsetPct int, handle LineHandler) (CheckResult, error) {
|
||||
args := []string{"check"}
|
||||
if subsetPct > 0 {
|
||||
args = append(args, "--read-data-subset", fmt.Sprintf("%d%%", subsetPct))
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, e.Bin, args...)
|
||||
cmd.Env = e.envSlice()
|
||||
cmd.Dir = e.WorkDir
|
||||
|
||||
var res CheckResult
|
||||
sniff := func(stream, line string, ev any) {
|
||||
if stream == "stderr" {
|
||||
if strings.Contains(line, "Found stale lock") || strings.Contains(line, "locked") {
|
||||
res.LockPresent = true
|
||||
}
|
||||
}
|
||||
if handle != nil {
|
||||
handle(stream, line, ev)
|
||||
}
|
||||
}
|
||||
|
||||
err := runWithPump(cmd, sniff)
|
||||
if err != nil {
|
||||
// restic check exits non-zero when corruption is found; that's
|
||||
// a CheckResult, not a wrapper failure. Treat ExitError as
|
||||
// "errors found" but still return the result so the caller can
|
||||
// persist last_check_status='errors_found'. Reserve the error
|
||||
// return for actually-broken invocations (binary missing, etc).
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) {
|
||||
res.ErrorsFound = true
|
||||
return res, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func pumpPlain(r io.Reader, stream string, handle LineHandler) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
Reference in New Issue
Block a user