1use std::io::Write;
7use std::path::Path;
8
9use crate::cli::{csv_escape, open_tablespace, setup_decryption, wprintln};
10use crate::innodb::backup;
11use crate::IdbError;
12
13pub struct BackupDiffOptions {
15 pub base: String,
16 pub current: String,
17 pub verbose: bool,
18 pub json: bool,
19 pub csv: bool,
20 pub page_size: Option<u32>,
21 pub keyring: Option<String>,
22 pub mmap: bool,
23}
24
25pub struct BackupChainOptions {
27 pub dir: String,
28 pub verbose: bool,
29 pub json: bool,
30 pub csv: bool,
31}
32
33pub fn execute_diff(opts: &BackupDiffOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
35 let mut base = open_tablespace(&opts.base, opts.page_size, opts.mmap)?;
36 if let Some(ref kp) = opts.keyring {
37 setup_decryption(&mut base, kp)?;
38 }
39 let mut current = open_tablespace(&opts.current, opts.page_size, opts.mmap)?;
40 if let Some(ref kp) = opts.keyring {
41 setup_decryption(&mut current, kp)?;
42 }
43
44 let report = backup::diff_backup_lsn(
45 &mut base,
46 &mut current,
47 &opts.base,
48 &opts.current,
49 opts.verbose,
50 )?;
51
52 if opts.json {
53 let json =
54 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?;
55 wprintln!(writer, "{}", json)?;
56 } else if opts.csv {
57 print_diff_csv(writer, &report)?;
58 } else {
59 print_diff_text(writer, &report)?;
60 }
61
62 Ok(())
63}
64
65pub fn execute_chain(opts: &BackupChainOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
67 let report = backup::scan_backup_chain(Path::new(&opts.dir))?;
68
69 if opts.json {
70 let json =
71 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?;
72 wprintln!(writer, "{}", json)?;
73 } else if opts.csv {
74 print_chain_csv(writer, &report)?;
75 } else {
76 print_chain_text(writer, &report, opts.verbose)?;
77 }
78
79 Ok(())
80}
81
82fn print_diff_text(
87 writer: &mut dyn Write,
88 report: &backup::BackupDiffReport,
89) -> Result<(), IdbError> {
90 wprintln!(
91 writer,
92 "Backup Delta: {} -> {} (Space ID: {})",
93 report.base_file,
94 report.current_file,
95 report.space_id
96 )?;
97 wprintln!(
98 writer,
99 " Base max LSN: {:>14} -> Current max LSN: {:>14}",
100 report.base_max_lsn,
101 report.current_max_lsn
102 )?;
103 if report.current_max_lsn > report.base_max_lsn {
104 wprintln!(
105 writer,
106 " LSN advance: {}",
107 report.current_max_lsn - report.base_max_lsn
108 )?;
109 }
110 wprintln!(writer)?;
111
112 let total = report.base_page_count.max(report.current_page_count);
113 let s = &report.summary;
114 wprintln!(writer, " Pages: {} total", total)?;
115 wprintln!(
116 writer,
117 " Unchanged: {:>6} ({:>5.1}%)",
118 s.unchanged,
119 pct(s.unchanged, total)
120 )?;
121 wprintln!(
122 writer,
123 " Modified: {:>6} ({:>5.1}%)",
124 s.modified,
125 pct(s.modified, total)
126 )?;
127 if s.added > 0 {
128 wprintln!(
129 writer,
130 " Added: {:>6} ({:>5.1}%)",
131 s.added,
132 pct(s.added, total)
133 )?;
134 }
135 if s.removed > 0 {
136 wprintln!(
137 writer,
138 " Removed: {:>6} ({:>5.1}%)",
139 s.removed,
140 pct(s.removed, total)
141 )?;
142 }
143 if s.regressed > 0 {
144 wprintln!(
145 writer,
146 " Regressed: {:>6} ({:>5.1}%)",
147 s.regressed,
148 pct(s.regressed, total)
149 )?;
150 }
151 wprintln!(writer)?;
152
153 if !report.modified_page_types.is_empty() {
154 wprintln!(writer, " Modified by type:")?;
155 for (pt, count) in &report.modified_page_types {
156 wprintln!(writer, " {:<16} {}", pt, count)?;
157 }
158 wprintln!(writer)?;
159 }
160
161 if !report.pages.is_empty() {
163 wprintln!(
164 writer,
165 " {:>8} {:>12} {:>14} {:>14} {:>10}",
166 "Page",
167 "Type",
168 "Base LSN",
169 "Current LSN",
170 "Status"
171 )?;
172 wprintln!(
173 writer,
174 " {:>8} {:>12} {:>14} {:>14} {:>10}",
175 "--------",
176 "------------",
177 "--------------",
178 "--------------",
179 "----------"
180 )?;
181 for p in &report.pages {
182 if p.status == backup::PageChangeStatus::Unchanged {
183 continue; }
185 let status_str = match p.status {
186 backup::PageChangeStatus::Unchanged => "unchanged",
187 backup::PageChangeStatus::Modified => "modified",
188 backup::PageChangeStatus::Added => "added",
189 backup::PageChangeStatus::Removed => "removed",
190 backup::PageChangeStatus::Regressed => "regressed",
191 };
192 wprintln!(
193 writer,
194 " {:>8} {:>12} {:>14} {:>14} {:>10}",
195 p.page_number,
196 p.page_type,
197 p.base_lsn
198 .map(|l| l.to_string())
199 .unwrap_or_else(|| "-".to_string()),
200 p.current_lsn
201 .map(|l| l.to_string())
202 .unwrap_or_else(|| "-".to_string()),
203 status_str,
204 )?;
205 }
206 wprintln!(writer)?;
207 }
208
209 Ok(())
210}
211
212fn print_chain_text(
213 writer: &mut dyn Write,
214 report: &backup::BackupChainReport,
215 verbose: bool,
216) -> Result<(), IdbError> {
217 wprintln!(writer, "Backup Chain: {}", report.chain_dir)?;
218 wprintln!(writer, " Backups found: {}", report.backups.len())?;
219 wprintln!(writer)?;
220
221 if !report.backups.is_empty() {
222 wprintln!(
223 writer,
224 " {:>3} {:<16} {:>14} {:>14} {:>8}",
225 "#",
226 "Type",
227 "From LSN",
228 "To LSN",
229 "Status"
230 )?;
231 wprintln!(
232 writer,
233 " {:>3} {:<16} {:>14} {:>14} {:>8}",
234 "---",
235 "----------------",
236 "--------------",
237 "--------------",
238 "--------"
239 )?;
240
241 for (i, b) in report.backups.iter().enumerate() {
242 let status = chain_entry_status(report, i);
243 wprintln!(
244 writer,
245 " {:>3} {:<16} {:>14} {:>14} {:>8}",
246 i + 1,
247 b.backup_type,
248 b.from_lsn,
249 b.to_lsn,
250 status,
251 )?;
252 if verbose {
253 wprintln!(writer, " Path: {}", b.path.display())?;
254 if let Some(last) = b.last_lsn {
255 wprintln!(writer, " Last LSN: {}", last)?;
256 }
257 }
258 }
259 wprintln!(writer)?;
260 }
261
262 if report.chain_valid {
264 if let Some((min, max)) = report.total_lsn_range {
265 wprintln!(writer, " Chain: VALID (LSN range {} -> {})", min, max)?;
266 } else {
267 wprintln!(writer, " Chain: VALID")?;
268 }
269 } else {
270 wprintln!(writer, " Chain: INVALID")?;
271 }
272
273 if !report.anomalies.is_empty() {
275 wprintln!(writer)?;
276 wprintln!(writer, " Anomalies:")?;
277 for a in &report.anomalies {
278 let kind = match a.kind {
279 backup::ChainAnomalyKind::Gap => "GAP",
280 backup::ChainAnomalyKind::Overlap => "OVERLAP",
281 backup::ChainAnomalyKind::MissingFull => "MISSING_FULL",
282 };
283 wprintln!(writer, " [{}] {}", kind, a.message)?;
284 }
285 }
286
287 wprintln!(writer)?;
288 Ok(())
289}
290
291fn chain_entry_status(report: &backup::BackupChainReport, index: usize) -> &'static str {
293 for a in &report.anomalies {
297 if a.kind == backup::ChainAnomalyKind::MissingFull {
298 continue;
299 }
300 if a.between.0 == index || a.between.1 == index {
301 return match a.kind {
302 backup::ChainAnomalyKind::Gap => "GAP",
303 backup::ChainAnomalyKind::Overlap => "OVERLAP",
304 backup::ChainAnomalyKind::MissingFull => unreachable!(),
305 };
306 }
307 }
308 "OK"
309}
310
311fn print_diff_csv(
316 writer: &mut dyn Write,
317 report: &backup::BackupDiffReport,
318) -> Result<(), IdbError> {
319 wprintln!(
320 writer,
321 "page_number,status,page_type,base_lsn,current_lsn,checksum_valid"
322 )?;
323 for p in &report.pages {
324 let status = match p.status {
325 backup::PageChangeStatus::Unchanged => "unchanged",
326 backup::PageChangeStatus::Modified => "modified",
327 backup::PageChangeStatus::Added => "added",
328 backup::PageChangeStatus::Removed => "removed",
329 backup::PageChangeStatus::Regressed => "regressed",
330 };
331 wprintln!(
332 writer,
333 "{},{},{},{},{},{}",
334 p.page_number,
335 status,
336 csv_escape(&p.page_type),
337 p.base_lsn.map(|l| l.to_string()).unwrap_or_default(),
338 p.current_lsn.map(|l| l.to_string()).unwrap_or_default(),
339 p.checksum_valid,
340 )?;
341 }
342 Ok(())
343}
344
345fn print_chain_csv(
346 writer: &mut dyn Write,
347 report: &backup::BackupChainReport,
348) -> Result<(), IdbError> {
349 wprintln!(writer, "index,backup_type,from_lsn,to_lsn,last_lsn,path")?;
350 for (i, b) in report.backups.iter().enumerate() {
351 wprintln!(
352 writer,
353 "{},{},{},{},{},{}",
354 i + 1,
355 csv_escape(&b.backup_type),
356 b.from_lsn,
357 b.to_lsn,
358 b.last_lsn.map(|l| l.to_string()).unwrap_or_default(),
359 csv_escape(&b.path.to_string_lossy()),
360 )?;
361 }
362 Ok(())
363}
364
365fn pct(part: u64, total: u64) -> f64 {
366 if total == 0 {
367 0.0
368 } else {
369 (part as f64 / total as f64) * 100.0
370 }
371}