1use {
4 crate::{
5 error::BackupError,
6 manifest::{CAPTURE_ROOT, Manifest},
7 },
8 krabka_remote_storage::ChainHead,
9 std::io::{self, Write},
10};
11
12#[derive(Debug)]
17#[must_use]
18pub struct OperationReport<T> {
19 pub result: Result<T, BackupError>,
21 pub events: Vec<BackupEvent>,
23}
24
25#[derive(Debug)]
27pub enum BackupEvent {
28 DisklessSealed {
30 head: ChainHead,
32 },
33 Capture {
35 manifest: Manifest,
37 notes: Vec<String>,
39 missing: Vec<String>,
41 },
42 EmptyList,
44 Listed {
46 id: String,
48 manifest: Result<Manifest, BackupError>,
50 },
51 ArtifactVerified {
53 name: String,
55 size_bytes: u64,
57 },
58 CaptureVerified {
60 id: String,
62 artifacts: usize,
64 },
65 WouldCommit {
67 group: String,
69 topic: String,
71 partition: i32,
73 offset: i64,
75 },
76 OffsetsCommitted {
78 group: String,
80 offsets: usize,
82 },
83}
84
85pub(crate) fn render(events: &[BackupEvent], output: &mut impl Write) -> io::Result<()> {
86 for event in events {
87 match event {
88 BackupEvent::DisklessSealed { head } => {
89 writeln!(output, " diskless-capture WORM head {head}")?;
90 }
91 BackupEvent::Capture {
92 manifest,
93 notes,
94 missing,
95 } => {
96 writeln!(output, "capture {}", manifest.capture_id)?;
97 for artifact in &manifest.artifacts {
98 writeln!(
99 output,
100 " {} <- {} ({} bytes, sha256 {})",
101 artifact.name, artifact.source, artifact.size_bytes, artifact.sha256
102 )?;
103 }
104 for note in notes {
105 writeln!(output, " NOTE: {note}")?;
106 }
107 for absent in missing {
108 writeln!(output, " WARNING: {absent}")?;
109 }
110 }
111 BackupEvent::EmptyList => writeln!(output, "no captures under {CAPTURE_ROOT}")?,
112 BackupEvent::Listed { id, manifest } => match manifest {
113 Ok(manifest) => {
114 let names: Vec<&str> = manifest
115 .artifacts
116 .iter()
117 .map(|artifact| artifact.name.as_str())
118 .collect();
119 writeln!(output, "{id} {}", names.join(" "))?;
120 }
121 Err(error) => writeln!(output, "{id} UNREADABLE: {error}")?,
122 },
123 BackupEvent::ArtifactVerified { name, size_bytes } => {
124 writeln!(output, " {name} ok ({size_bytes} bytes)")?;
125 }
126 BackupEvent::CaptureVerified { id, artifacts } => {
127 writeln!(output, "capture {id} verified: {artifacts} artifacts")?;
128 }
129 BackupEvent::WouldCommit {
130 group,
131 topic,
132 partition,
133 offset,
134 } => writeln!(
135 output,
136 "would commit {group}: {topic}-{partition} = {offset}"
137 )?,
138 BackupEvent::OffsetsCommitted { group, offsets } => {
139 writeln!(output, "{group}: {offsets} offsets committed")?;
140 }
141 }
142 }
143 Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148 use {
149 super::{BackupEvent, render},
150 crate::{
151 error::BackupError,
152 manifest::{Artifact, MANIFEST_VERSION, Manifest},
153 },
154 assert2::check,
155 krabka_remote_storage::ChainHead,
156 };
157
158 fn manifest() -> Manifest {
159 Manifest {
160 version: MANIFEST_VERSION,
161 capture_id: "capture-id".to_owned(),
162 captured_at_ms: 0,
163 log_dir: None,
164 bootstrap_server: None,
165 artifacts: vec![Artifact {
166 name: "rlmm-snapshot".to_owned(),
167 source: "/node/snapshot".to_owned(),
168 size_bytes: 12,
169 sha256: "digest".to_owned(),
170 }],
171 }
172 }
173
174 #[test]
175 fn terminal_output_preserves_operation_order_and_text() {
176 let events = [
177 BackupEvent::DisklessSealed {
178 head: ChainHead([0xab; 32]),
179 },
180 BackupEvent::Capture {
181 manifest: manifest(),
182 notes: vec!["checkpoint age".to_owned()],
183 missing: vec!["no checkpoint".to_owned()],
184 },
185 BackupEvent::EmptyList,
186 BackupEvent::Listed {
187 id: "capture-id".to_owned(),
188 manifest: Ok(manifest()),
189 },
190 BackupEvent::Listed {
191 id: "broken-id".to_owned(),
192 manifest: Err(BackupError::Integrity("bad manifest".to_owned())),
193 },
194 BackupEvent::ArtifactVerified {
195 name: "rlmm-snapshot".to_owned(),
196 size_bytes: 12,
197 },
198 BackupEvent::CaptureVerified {
199 id: "capture-id".to_owned(),
200 artifacts: 1,
201 },
202 BackupEvent::WouldCommit {
203 group: "readers".to_owned(),
204 topic: "orders".to_owned(),
205 partition: 2,
206 offset: 9,
207 },
208 BackupEvent::OffsetsCommitted {
209 group: "readers".to_owned(),
210 offsets: 1,
211 },
212 ];
213 let mut output = Vec::new();
214 render(&events, &mut output).expect("render the operation events");
215 check!(
216 String::from_utf8(output).unwrap()
217 == concat!(
218 " diskless-capture WORM head abababababababababababababababababababababababababababababababab\n",
219 "capture capture-id\n",
220 " rlmm-snapshot <- /node/snapshot (12 bytes, sha256 digest)\n",
221 " NOTE: checkpoint age\n",
222 " WARNING: no checkpoint\n",
223 "no captures under restore-inputs\n",
224 "capture-id rlmm-snapshot\n",
225 "broken-id UNREADABLE: integrity failure: bad manifest\n",
226 " rlmm-snapshot ok (12 bytes)\n",
227 "capture capture-id verified: 1 artifacts\n",
228 "would commit readers: orders-2 = 9\n",
229 "readers: 1 offsets committed\n",
230 )
231 );
232 }
233
234 #[test]
235 fn an_empty_event_stream_writes_nothing() {
236 let mut output = Vec::new();
237 render(&[], &mut output).expect("render no events");
238 check!(output.is_empty());
239 }
240}