P2 redesign Phase 5 — prune/check/unlock + maintenance ticker + repo stats + pending-runs queue #3
@@ -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)
|
||||
|
||||
@@ -34,7 +34,7 @@ func captureLines() (*[]string, LineHandler) {
|
||||
return &lines, h
|
||||
}
|
||||
|
||||
// --- B1: RunPrune ---
|
||||
// --- B1: RunPrune + B2: RunCheck ---
|
||||
|
||||
func TestRunPruneInvokesPrune(t *testing.T) {
|
||||
// Shell script that echoes its args; "prune" should appear in output.
|
||||
@@ -51,3 +51,48 @@ func TestRunPruneInvokesPrune(t *testing.T) {
|
||||
}
|
||||
t.Fatalf("expected 'prune' in captured output; got: %v", *lines)
|
||||
}
|
||||
|
||||
// --- B2: RunCheck ---
|
||||
|
||||
func TestRunCheckParsesLock(t *testing.T) {
|
||||
bin := setupScriptBin(t, `echo "Found stale lock" >&2`)
|
||||
env := Env{Bin: bin}
|
||||
res, err := env.RunCheck(context.Background(), 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("RunCheck returned unexpected error: %v", err)
|
||||
}
|
||||
if !res.LockPresent {
|
||||
t.Fatal("expected LockPresent=true")
|
||||
}
|
||||
if res.ErrorsFound {
|
||||
t.Fatal("expected ErrorsFound=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckErrorsFoundOnExit1(t *testing.T) {
|
||||
bin := setupScriptBin(t, `exit 1`)
|
||||
env := Env{Bin: bin}
|
||||
res, err := env.RunCheck(context.Background(), 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("RunCheck returned unexpected error (should have absorbed exit 1): %v", err)
|
||||
}
|
||||
if !res.ErrorsFound {
|
||||
t.Fatal("expected ErrorsFound=true for exit 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckSubsetArg(t *testing.T) {
|
||||
bin := setupScriptBin(t, `echo "$@"`)
|
||||
env := Env{Bin: bin}
|
||||
lines, h := captureLines()
|
||||
if _, err := env.RunCheck(context.Background(), 25, h); err != nil {
|
||||
t.Fatalf("RunCheck: %v", err)
|
||||
}
|
||||
want := "--read-data-subset 25%"
|
||||
for _, l := range *lines {
|
||||
if strings.Contains(l, want) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected %q in captured output; got: %v", want, *lines)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user