restic: RunUnlock + RunStats (raw-data mode)
Add RunUnlock (delegates straight to runWithPump) and RunStats which runs `restic stats --json --mode raw-data`, captures the single JSON line from stdout into RepoStats, and returns an error if no JSON arrives. Tests cover arg plumbing for unlock, JSON parsing, and the no-JSON error path.
This commit is contained in:
@@ -293,6 +293,53 @@ func runWithPump(cmd *exec.Cmd, handle LineHandler) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunUnlock executes `restic unlock`. Returns nil on a clean exit.
|
||||
func (e Env) RunUnlock(ctx context.Context, handle LineHandler) error {
|
||||
cmd := exec.CommandContext(ctx, e.Bin, "unlock")
|
||||
cmd.Env = e.envSlice()
|
||||
cmd.Dir = e.WorkDir
|
||||
return runWithPump(cmd, handle)
|
||||
}
|
||||
|
||||
// RepoStats mirrors `restic stats --json --mode raw-data` output.
|
||||
type RepoStats struct {
|
||||
TotalSize int64 `json:"total_size"`
|
||||
TotalUncompressed int64 `json:"total_uncompressed_size"`
|
||||
SnapshotsCount int64 `json:"snapshots_count"`
|
||||
TotalFileCount int64 `json:"total_file_count"`
|
||||
TotalBlobCount int64 `json:"total_blob_count"`
|
||||
}
|
||||
|
||||
// RunStats executes `restic stats --json --mode raw-data` and parses
|
||||
// the (single-line) JSON response. Tees raw output to handle so the
|
||||
// caller can still log it. Returns an error if no JSON-shaped line
|
||||
// arrived on stdout.
|
||||
func (e Env) RunStats(ctx context.Context, handle LineHandler) (*RepoStats, error) {
|
||||
cmd := exec.CommandContext(ctx, e.Bin, "stats", "--json", "--mode", "raw-data")
|
||||
cmd.Env = e.envSlice()
|
||||
cmd.Dir = e.WorkDir
|
||||
var out *RepoStats
|
||||
capture := func(stream, line string, ev any) {
|
||||
if stream == "stdout" && strings.HasPrefix(line, "{") {
|
||||
var s RepoStats
|
||||
if json.Unmarshal([]byte(line), &s) == nil {
|
||||
cp := s
|
||||
out = &cp
|
||||
}
|
||||
}
|
||||
if handle != nil {
|
||||
handle(stream, line, ev)
|
||||
}
|
||||
}
|
||||
if err := runWithPump(cmd, capture); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out == nil {
|
||||
return nil, fmt.Errorf("restic stats: no JSON in output")
|
||||
}
|
||||
return out, 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).
|
||||
|
||||
@@ -96,3 +96,56 @@ func TestRunCheckSubsetArg(t *testing.T) {
|
||||
}
|
||||
t.Fatalf("expected %q in captured output; got: %v", want, *lines)
|
||||
}
|
||||
|
||||
// --- B3: RunUnlock + RunStats ---
|
||||
|
||||
func TestRunUnlockInvokesUnlock(t *testing.T) {
|
||||
bin := setupScriptBin(t, `echo "$@"`)
|
||||
env := Env{Bin: bin}
|
||||
lines, h := captureLines()
|
||||
if err := env.RunUnlock(context.Background(), h); err != nil {
|
||||
t.Fatalf("RunUnlock: %v", err)
|
||||
}
|
||||
for _, l := range *lines {
|
||||
if strings.Contains(l, "unlock") {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected 'unlock' in captured output; got: %v", *lines)
|
||||
}
|
||||
|
||||
func TestRunStatsParsesJSON(t *testing.T) {
|
||||
bin := setupScriptBin(t, `echo '{"total_size":1234,"total_uncompressed_size":5678,"snapshots_count":3,"total_file_count":100,"total_blob_count":50}'`)
|
||||
env := Env{Bin: bin}
|
||||
stats, err := env.RunStats(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("RunStats: %v", err)
|
||||
}
|
||||
if stats.TotalSize != 1234 {
|
||||
t.Fatalf("TotalSize: got %d, want 1234", stats.TotalSize)
|
||||
}
|
||||
if stats.TotalUncompressed != 5678 {
|
||||
t.Fatalf("TotalUncompressed: got %d, want 5678", stats.TotalUncompressed)
|
||||
}
|
||||
if stats.SnapshotsCount != 3 {
|
||||
t.Fatalf("SnapshotsCount: got %d, want 3", stats.SnapshotsCount)
|
||||
}
|
||||
if stats.TotalFileCount != 100 {
|
||||
t.Fatalf("TotalFileCount: got %d, want 100", stats.TotalFileCount)
|
||||
}
|
||||
if stats.TotalBlobCount != 50 {
|
||||
t.Fatalf("TotalBlobCount: got %d, want 50", stats.TotalBlobCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStatsErrorsWithoutJSON(t *testing.T) {
|
||||
bin := setupScriptBin(t, `echo "no json here"`)
|
||||
env := Env{Bin: bin}
|
||||
_, err := env.RunStats(context.Background(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no JSON in output")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no JSON in output") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user