use std::time::Instant;
use serde_json::json;
use uuid::Uuid;
use crate::api::apply::perf::PerfAgg;
use crate::api::errors::map::map_restore_error_kind;
use crate::api::errors::{exit_code_for, id_str};
use crate::api::Switchyard;
use crate::fs::meta::{kind_of, sha256_hex_of};
use crate::logging::audit::{ensure_provenance, AuditCtx};
use crate::logging::StageLogger;
use crate::logging::{AuditSink, FactsEmitter};
use crate::types::{ids::action_id, Action};
use super::ActionExecutor;
pub(crate) struct RestoreFromBackupExec;
impl<E: FactsEmitter, A: AuditSink> ActionExecutor<E, A> for RestoreFromBackupExec {
#[allow(
clippy::too_many_lines,
reason = "Will be split in PR6; executor remains verbose for parity"
)]
fn execute(
&self,
api: &Switchyard<E, A>,
tctx: &AuditCtx<'_>,
pid: &Uuid,
act: &Action,
idx: usize,
dry: bool,
) -> (Option<Action>, Option<String>, PerfAgg) {
let Action::RestoreFromBackup { target } = act else {
return (
None,
Some("expected RestoreFromBackup".to_string()),
PerfAgg::default(),
);
};
let aid = action_id(pid, act, idx);
StageLogger::new(tctx)
.apply_attempt()
.merge(&json!({
"action_id": aid.to_string(),
"path": target.as_path().display().to_string(),
"safepath_validation": "success",
"backup_durable": api.policy.durability.backup_durability,
}))
.emit_success();
let before_kind = kind_of(&target.as_path());
let mut backup_ms = 0u64;
let force =
api.policy.apply.best_effort_restore || !api.policy.durability.sidecar_integrity;
let th0 = Instant::now();
let integrity_verified = (|| {
let pair = crate::fs::backup::find_latest_backup_and_sidecar(
&target.as_path(),
&api.policy.backup.tag,
)?;
let (backup_opt, sc_path) = pair;
let sc = crate::fs::backup::read_sidecar(&sc_path).ok()?;
if let (Some(backup), Some(hash)) = (backup_opt, sc.payload_hash) {
let actual = sha256_hex_of(&backup)?;
Some(actual == hash)
} else {
None
}
})();
let hash_ms = u64::try_from(th0.elapsed().as_millis()).unwrap_or(u64::MAX);
let will_use_prev = !dry && api.policy.apply.capture_restore_snapshot;
if !dry && !will_use_prev {
if let Some((_bopt, sc_path)) = crate::fs::backup::find_latest_backup_and_sidecar(
&target.as_path(),
&api.policy.backup.tag,
) {
if let Ok(sc) = crate::fs::backup::read_sidecar(&sc_path) {
if crate::fs::restore::idempotence::is_idempotent(
&target.as_path(),
sc.prior_kind.as_str(),
sc.prior_dest.as_deref(),
) {
let mut extra = json!({
"action_id": aid.to_string(),
"path": target.as_path().display().to_string(),
"before_kind": before_kind,
"after_kind": before_kind,
"idempotent": true,
"backup_durable": api.policy.durability.backup_durability,
});
ensure_provenance(&mut extra);
StageLogger::new(tctx)
.apply_result()
.merge(&extra)
.emit_success();
return (
Some(act.clone()),
None,
PerfAgg {
hash: hash_ms,
backup: 0,
swap: 0,
},
);
}
}
}
}
if !dry && api.policy.apply.capture_restore_snapshot {
let t_backup_start = Instant::now();
let _ = crate::fs::backup::create_snapshot(&target.as_path(), &api.policy.backup.tag);
backup_ms = backup_ms.saturating_add(
u64::try_from(t_backup_start.elapsed().as_millis()).unwrap_or(u64::MAX),
);
}
let restore_res = if !dry && api.policy.apply.capture_restore_snapshot {
crate::fs::restore::restore_file_prev(target, dry, force, &api.policy.backup.tag)
} else {
crate::fs::restore::restore_file(target, dry, force, &api.policy.backup.tag)
};
match restore_res {
Ok(()) => {}
Err(e) => {
let id = map_restore_error_kind(e.kind());
let msg = format!("restore {} failed: {}", target.as_path().display(), e);
let mut extra = json!({
"action_id": aid.to_string(),
"path": target.as_path().display().to_string(),
"before_kind": before_kind,
"after_kind": if dry { before_kind } else { kind_of(&target.as_path()) },
});
if let Some(owner) = &api.owner {
if let Ok(info) = owner.owner_of(target) {
if let Some(obj) = extra.as_object_mut() {
let prov = obj.entry("provenance".to_string()).or_insert(json!({}));
if let Some(pobj) = prov.as_object_mut() {
pobj.insert("uid".to_string(), json!(info.uid));
pobj.insert("gid".to_string(), json!(info.gid));
pobj.insert("pkg".to_string(), json!(info.pkg));
}
}
}
}
if let Some(iv) = integrity_verified {
if let Some(obj) = extra.as_object_mut() {
obj.insert("sidecar_integrity_verified".into(), json!(iv));
}
}
ensure_provenance(&mut extra);
if let Some(obj) = extra.as_object_mut() {
obj.insert("error_id".to_string(), json!(id_str(id)));
obj.insert("exit_code".to_string(), json!(exit_code_for(id)));
}
StageLogger::new(tctx)
.apply_result()
.merge(&extra)
.emit_failure();
return (
None,
Some(msg),
PerfAgg {
hash: hash_ms,
backup: backup_ms,
swap: 0,
},
);
}
}
let mut extra = json!({
"action_id": aid.to_string(),
"path": target.as_path().display().to_string(),
"before_kind": before_kind,
"after_kind": if dry { before_kind } else { kind_of(&target.as_path()) },
"backup_durable": api.policy.durability.backup_durability,
});
if let Some(owner) = &api.owner {
if let Ok(info) = owner.owner_of(target) {
if let Some(obj) = extra.as_object_mut() {
let prov = obj.entry("provenance".to_string()).or_insert(json!({}));
if let Some(pobj) = prov.as_object_mut() {
pobj.insert("uid".to_string(), json!(info.uid));
pobj.insert("gid".to_string(), json!(info.gid));
pobj.insert("pkg".to_string(), json!(info.pkg));
}
}
}
}
if let Some(iv) = integrity_verified {
if let Some(obj) = extra.as_object_mut() {
obj.insert("sidecar_integrity_verified".into(), json!(iv));
}
}
ensure_provenance(&mut extra);
StageLogger::new(tctx)
.apply_result()
.merge(&extra)
.emit_success();
(
Some(act.clone()),
None,
PerfAgg {
hash: hash_ms,
backup: backup_ms,
swap: 0,
},
)
}
}