restic: RunPrune + runWithPump helper, refactor Forget/Init onto it
Add RunPrune for admin-credential prune invocations. Extract runWithPump to DRY the stdout+stderr pump pattern; refactor RunForget and RunInit to delegate to it (RunInit preserves the "config file already exists" soft-success sniff by wrapping the handler before the call). Add runner_test.go with TestRunPruneInvokesPrune.
This commit is contained in:
+51
-49
@@ -204,32 +204,7 @@ func (e Env) RunForget(ctx context.Context, policy ForgetPolicy, handle LineHand
|
|||||||
cmd := exec.CommandContext(ctx, e.Bin, args...)
|
cmd := exec.CommandContext(ctx, e.Bin, args...)
|
||||||
cmd.Env = e.envSlice()
|
cmd.Env = e.envSlice()
|
||||||
cmd.Dir = e.WorkDir
|
cmd.Dir = e.WorkDir
|
||||||
|
return runWithPump(cmd, handle)
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("restic forget: stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
stderr, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("restic forget: stderr pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("restic forget: start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan error, 2)
|
|
||||||
go func() { done <- pumpPlain(stdout, "stdout", handle) }()
|
|
||||||
go func() { done <- pumpPlain(stderr, "stderr", handle) }()
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if err := <-done; err != nil && handle != nil {
|
|
||||||
handle("event", fmt.Sprintf("pump error: %v", err), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if werr := cmd.Wait(); werr != nil {
|
|
||||||
return fmt.Errorf("restic forget: %w", werr)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunInit executes `restic init` against the configured repo. Returns
|
// RunInit executes `restic init` against the configured repo. Returns
|
||||||
@@ -243,19 +218,6 @@ func (e Env) RunInit(ctx context.Context, handle LineHandler) error {
|
|||||||
cmd.Env = e.envSlice()
|
cmd.Env = e.envSlice()
|
||||||
cmd.Dir = e.WorkDir
|
cmd.Dir = e.WorkDir
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("restic init: stdout pipe: %w", err)
|
|
||||||
}
|
|
||||||
stderr, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("restic init: stderr pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("restic init: start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sniff for "config file already exists" on stderr; if we see it
|
// Sniff for "config file already exists" on stderr; if we see it
|
||||||
// we'll treat the non-zero exit as a soft success — running init
|
// we'll treat the non-zero exit as a soft success — running init
|
||||||
// against an already-initialized repo is a no-op semantically,
|
// against an already-initialized repo is a no-op semantically,
|
||||||
@@ -271,22 +233,62 @@ func (e Env) RunInit(ctx context.Context, handle LineHandler) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan error, 2)
|
if err := runWithPump(cmd, sniff); err != nil {
|
||||||
go func() { done <- pumpPlain(stdout, "stdout", sniff) }()
|
|
||||||
go func() { done <- pumpPlain(stderr, "stderr", sniff) }()
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if err := <-done; err != nil && handle != nil {
|
|
||||||
handle("event", fmt.Sprintf("pump error: %v", err), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if werr := cmd.Wait(); werr != nil {
|
|
||||||
if alreadyInited {
|
if alreadyInited {
|
||||||
if handle != nil {
|
if handle != nil {
|
||||||
handle("event", "repo already initialized — treating as success", nil)
|
handle("event", "repo already initialized — treating as success", nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("restic init: %w", werr)
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPrune executes `restic prune` against the configured repo.
|
||||||
|
// Requires the *admin* credentials (delete access on the rest-server
|
||||||
|
// repo) — the caller is responsible for populating Env.RepoUsername
|
||||||
|
// and Env.RepoPassword with the admin pair before calling this.
|
||||||
|
//
|
||||||
|
// Prune emits human-readable progress on stdout/stderr (no --json
|
||||||
|
// support that's useful for our purposes). We tee everything to the
|
||||||
|
// handler so the live log is the operator's progress bar.
|
||||||
|
func (e Env) RunPrune(ctx context.Context, handle LineHandler) error {
|
||||||
|
cmd := exec.CommandContext(ctx, e.Bin, "prune")
|
||||||
|
cmd.Env = e.envSlice()
|
||||||
|
cmd.Dir = e.WorkDir
|
||||||
|
return runWithPump(cmd, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWithPump starts the configured cmd, fans stdout+stderr into
|
||||||
|
// pumpPlain via the supplied handler, waits, and wraps any error
|
||||||
|
// with the cmd's verb (e.g., "restic prune") for context.
|
||||||
|
func runWithPump(cmd *exec.Cmd, handle LineHandler) error {
|
||||||
|
label := "restic"
|
||||||
|
if len(cmd.Args) > 1 {
|
||||||
|
label = "restic " + cmd.Args[1]
|
||||||
|
}
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: stdout pipe: %w", label, err)
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: stderr pipe: %w", label, err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("%s: start: %w", label, err)
|
||||||
|
}
|
||||||
|
done := make(chan error, 2)
|
||||||
|
go func() { done <- pumpPlain(stdout, "stdout", handle) }()
|
||||||
|
go func() { done <- pumpPlain(stderr, "stderr", handle) }()
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := <-done; err != nil && handle != nil {
|
||||||
|
handle("event", fmt.Sprintf("pump error: %v", err), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if werr := cmd.Wait(); werr != nil {
|
||||||
|
return fmt.Errorf("%s: %w", label, werr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupScriptBin writes a small shell script to a temp directory,
|
||||||
|
// makes it executable, and returns its path. scriptBody is the
|
||||||
|
// complete script content (without the shebang line — that's added
|
||||||
|
// automatically).
|
||||||
|
func setupScriptBin(t *testing.T, scriptBody string) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "restic")
|
||||||
|
content := "#!/bin/sh\n" + scriptBody + "\n"
|
||||||
|
if err := os.WriteFile(p, []byte(content), 0o755); err != nil {
|
||||||
|
t.Fatalf("setupScriptBin: %v", err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureLines returns a LineHandler that appends "stream:line" into
|
||||||
|
// the returned slice pointer (safe for single-goroutine test use).
|
||||||
|
func captureLines() (*[]string, LineHandler) {
|
||||||
|
var lines []string
|
||||||
|
h := func(stream, line string, _ any) {
|
||||||
|
lines = append(lines, fmt.Sprintf("%s:%s", stream, line))
|
||||||
|
}
|
||||||
|
return &lines, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- B1: RunPrune ---
|
||||||
|
|
||||||
|
func TestRunPruneInvokesPrune(t *testing.T) {
|
||||||
|
// Shell script that echoes its args; "prune" should appear in output.
|
||||||
|
bin := setupScriptBin(t, `echo "$@"`)
|
||||||
|
env := Env{Bin: bin}
|
||||||
|
lines, h := captureLines()
|
||||||
|
if err := env.RunPrune(context.Background(), h); err != nil {
|
||||||
|
t.Fatalf("RunPrune returned error: %v", err)
|
||||||
|
}
|
||||||
|
for _, l := range *lines {
|
||||||
|
if strings.Contains(l, "prune") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("expected 'prune' in captured output; got: %v", *lines)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user