package restic import ( "context" "encoding/json" "errors" "fmt" "os/exec" "time" ) // Snapshot mirrors a single entry in `restic snapshots --json`. We // decode only the fields we project to the server; restic's full // shape has more (parent, tree, program version) that we don't need. // // Summary is only populated by restic 0.16+ (which embeds the backup // summary inside each snapshot). Older clients leave it nil and the // agent reports zero size/file-count — the UI degrades to "—". type Snapshot struct { ID string `json:"id"` ShortID string `json:"short_id"` Time time.Time `json:"time"` Hostname string `json:"hostname"` Paths []string `json:"paths"` Tags []string `json:"tags,omitempty"` Summary *SnapshotSummary `json:"summary,omitempty"` } // SnapshotSummary mirrors the embedded summary block restic 0.16+ // writes into each snapshot record. The naming follows restic's JSON. type SnapshotSummary struct { TotalFilesProcessed int64 `json:"total_files_processed"` TotalBytesProcessed int64 `json:"total_bytes_processed"` } // ListSnapshots calls `restic snapshots --json` and returns the // parsed list. Output is a single JSON array — small even on large // repos (each entry is ~200 bytes), so we read it all into memory. func (e Env) ListSnapshots(ctx context.Context) ([]Snapshot, error) { cmd := exec.CommandContext(ctx, e.Bin, "snapshots", "--json") cmd.Env = e.envSlice() cmd.Dir = e.WorkDir out, err := cmd.Output() if err != nil { var ee *exec.ExitError if errors.As(err, &ee) { return nil, fmt.Errorf("restic snapshots: exit %d: %s", ee.ExitCode(), string(ee.Stderr)) } return nil, fmt.Errorf("restic snapshots: %w", err) } var snaps []Snapshot if err := json.Unmarshal(out, &snaps); err != nil { return nil, fmt.Errorf("restic snapshots: parse json: %w", err) } return snaps, nil }