use anyhow::{Context, Result};
use heddle_core::oplog_plan::{
OplogRecoverFacts, oplog_recover_detail_fields, oplog_recover_headline_from_facts,
oplog_recover_shows_detail, plan_oplog_recover,
};
use oplog::OplogRecoveryReport;
use repo::Repository;
use serde::Serialize;
use crate::cli::{Cli, OplogCommands, should_output_json, style};
pub fn cmd_oplog(cli: &Cli, command: OplogCommands) -> Result<()> {
match command {
OplogCommands::Recover => cmd_oplog_recover(cli),
}
}
#[derive(Serialize)]
struct RecoverOutput {
output_kind: &'static str,
already_healthy: bool,
prior_recovery: bool,
#[serde(skip_serializing_if = "Option::is_none")]
strategy: Option<String>,
entries_recovered: u64,
#[serde(skip_serializing_if = "Option::is_none")]
entries_lost: Option<u64>,
damaged_byte_start: u64,
damaged_byte_end: u64,
#[serde(skip_serializing_if = "Option::is_none")]
quarantine_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sidecar_path: Option<String>,
suffix_segments_discarded: u64,
suffix_entries_discarded: u64,
}
impl From<&OplogRecoveryReport> for RecoverOutput {
fn from(report: &OplogRecoveryReport) -> Self {
Self {
output_kind: "oplog_recover",
already_healthy: report.already_healthy,
prior_recovery: report.prior_recovery,
strategy: report.strategy.clone(),
entries_recovered: report.entries_recovered,
entries_lost: report.entries_lost,
damaged_byte_start: report.damaged_byte_start,
damaged_byte_end: report.damaged_byte_end,
quarantine_path: report
.quarantine_path
.as_ref()
.map(|p| p.display().to_string()),
sidecar_path: report
.sidecar_path
.as_ref()
.map(|p| p.display().to_string()),
suffix_segments_discarded: report.suffix_segments_discarded,
suffix_entries_discarded: report.suffix_entries_discarded,
}
}
}
fn recover_facts(report: &OplogRecoveryReport) -> OplogRecoverFacts {
OplogRecoverFacts {
already_healthy: report.already_healthy,
prior_recovery: report.prior_recovery,
strategy: report.strategy.clone(),
entries_recovered: report.entries_recovered,
entries_lost: report.entries_lost,
damaged_byte_start: report.damaged_byte_start,
damaged_byte_end: report.damaged_byte_end,
quarantine_path: report
.quarantine_path
.as_ref()
.map(|p| p.display().to_string()),
sidecar_path: report
.sidecar_path
.as_ref()
.map(|p| p.display().to_string()),
}
}
fn cmd_oplog_recover(cli: &Cli) -> Result<()> {
let cwd;
let repo_path = match cli.repo.as_ref() {
Some(path) => path,
None => {
cwd = std::env::current_dir().context("get current working directory")?;
&cwd
}
};
let repo = Repository::open_for_oplog_recovery(repo_path)
.context("open Heddle repository for oplog recovery")?;
let report = repo.oplog().recover()?;
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&RecoverOutput::from(&report))?);
return Ok(());
}
let facts = recover_facts(&report);
let status = plan_oplog_recover(&facts);
println!(
"{} {}",
style::ok_marker(),
oplog_recover_headline_from_facts(&facts)
);
if !oplog_recover_shows_detail(status) {
return Ok(());
}
for (label, value) in oplog_recover_detail_fields(&facts) {
println!(" {}", style::field(label, &value));
}
if report.suffix_segments_discarded > 0 {
println!(
" {}",
style::field(
"Suffix segments discarded",
&report.suffix_segments_discarded.to_string(),
)
);
println!(
" {}",
style::field(
"Suffix entries discarded",
&report.suffix_entries_discarded.to_string(),
)
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn report_with_suffix(segments: u64, entries: u64) -> OplogRecoveryReport {
OplogRecoveryReport {
already_healthy: segments == 0,
prior_recovery: false,
strategy: None,
entries_recovered: 0,
entries_lost: None,
damaged_byte_start: 0,
damaged_byte_end: 0,
quarantine_path: None,
sidecar_path: None,
suffix_segments_discarded: segments,
suffix_entries_discarded: entries,
}
}
#[test]
fn recover_json_always_reports_zero_or_nonzero_suffix_loss() {
let zero = serde_json::to_value(RecoverOutput::from(&report_with_suffix(0, 0))).unwrap();
assert_eq!(zero["suffix_segments_discarded"], 0);
assert_eq!(zero["suffix_entries_discarded"], 0);
let loss = serde_json::to_value(RecoverOutput::from(&report_with_suffix(2, 7))).unwrap();
assert_eq!(loss["suffix_segments_discarded"], 2);
assert_eq!(loss["suffix_entries_discarded"], 7);
}
}