1use crate::render::manifest::SourceRoot;
2use crate::restore_backups::{
3 self, BackupRunSelector, Mode, RestoreError, RestoreOptions, RestoredChange,
4};
5use clap::Args;
6use std::path::PathBuf;
7
8#[derive(Args, Debug)]
9pub struct RestoreBackupsArgs {
10 #[arg(long)]
13 pub source_root: Option<PathBuf>,
14 #[arg(long)]
16 pub product: String,
17 #[arg(long)]
21 pub live_home: PathBuf,
22 #[arg(long)]
24 pub state_home: PathBuf,
25 #[arg(long)]
30 pub from: Option<String>,
31 #[arg(long)]
34 pub surface: Option<String>,
35 #[arg(long, default_value_t = false)]
39 pub no_overlay: bool,
40 #[arg(long, conflicts_with = "no_overlay")]
43 pub overlay_path: Option<PathBuf>,
44 #[arg(long, conflicts_with = "apply")]
46 pub dry_run: bool,
47 #[arg(long, conflicts_with = "dry_run")]
49 pub apply: bool,
50}
51
52pub fn run(args: RestoreBackupsArgs) -> anyhow::Result<u8> {
53 if !args.live_home.is_absolute() {
54 anyhow::bail!(
55 "agent-runtime restore-backups: --live-home must be absolute (got: {})",
56 args.live_home.display()
57 );
58 }
59 if !args.state_home.is_absolute() {
60 anyhow::bail!(
61 "agent-runtime restore-backups: --state-home must be absolute (got: {})",
62 args.state_home.display()
63 );
64 }
65 if !args.dry_run && !args.apply {
66 anyhow::bail!("agent-runtime restore-backups: pass --dry-run or --apply");
67 }
68 let from = match args.from.as_deref() {
69 Some(s) => s,
70 None => {
71 print_available_timestamps(&args.state_home, &args.product);
72 anyhow::bail!(
73 "agent-runtime restore-backups: --from is required (use `latest` or one of the timestamps printed above)"
74 );
75 }
76 };
77 let selector: BackupRunSelector = from.parse().map_err(|err: String| anyhow::anyhow!(err))?;
78
79 let mode = if args.apply {
80 Mode::Apply
81 } else {
82 Mode::DryRun
83 };
84
85 let root = SourceRoot::from_arg_or_cwd(args.source_root.as_deref())?;
86 let options = RestoreOptions {
87 selector: selector.clone(),
88 surface: args.surface.clone(),
89 overlay_enabled: !args.no_overlay,
90 overlay_path: args.overlay_path.clone(),
91 };
92
93 let outcome = match restore_backups::run(
94 &args.product,
95 root.path(),
96 &args.live_home,
97 &args.state_home,
98 mode,
99 &options,
100 ) {
101 Ok(o) => o,
102 Err(RestoreError::NoBackupRun {
103 root,
104 selector: _,
105 available,
106 }) => {
107 print_timestamps_list(&args.product, &available);
108 anyhow::bail!(
109 "agent-runtime restore-backups: no backup run matches --from {from} under {}",
110 root.display()
111 );
112 }
113 Err(err) => return Err(err.into()),
114 };
115
116 if let Some(s) = outcome.overlay.as_ref() {
117 eprintln!(
118 "agent-runtime restore-backups: overlay merged (dropped={} replaced={} added={})",
119 s.dropped, s.replaced, s.added,
120 );
121 }
122
123 eprintln!(
124 "agent-runtime restore-backups: product={} mode={} from={} actions={} restored={}",
125 outcome.plan.product,
126 if matches!(mode, Mode::Apply) {
127 "apply"
128 } else {
129 "dry-run"
130 },
131 outcome.backup_run.display(),
132 outcome.plan.actions.len(),
133 outcome
134 .changes
135 .iter()
136 .filter(|c| matches!(c, RestoredChange::FileRestored { .. }))
137 .count(),
138 );
139
140 for change in &outcome.changes {
141 print_change(change);
142 }
143
144 Ok(0)
145}
146
147fn print_available_timestamps(state_home: &std::path::Path, product: &str) {
148 let timestamps = restore_backups::list_available_timestamps(state_home, product);
149 print_timestamps_list(product, ×tamps);
150}
151
152fn print_timestamps_list(product: &str, timestamps: &[u64]) {
153 if timestamps.is_empty() {
154 eprintln!(
155 "agent-runtime restore-backups: no backup runs found under <state_home>/backups/{product}/"
156 );
157 } else {
158 eprintln!("agent-runtime restore-backups: available --from values for product={product}:");
159 for ts in timestamps {
160 eprintln!(" - {ts}");
161 }
162 eprintln!(" - latest (resolves to {})", timestamps.last().unwrap());
163 }
164}
165
166fn print_change(c: &RestoredChange) {
167 match c {
168 RestoredChange::FileRestored {
169 entry_id,
170 dest,
171 from_backup,
172 } => eprintln!(
173 " + restore {} <- {} ({})",
174 dest.display(),
175 from_backup.display(),
176 entry_id
177 ),
178 RestoredChange::SkippedDestRegularFile {
179 entry_id,
180 dest,
181 from_backup: _,
182 } => eprintln!(
183 " ? skip {} (regular file at dest; not overwriting; {})",
184 dest.display(),
185 entry_id
186 ),
187 RestoredChange::SkippedDestDirectory {
188 entry_id,
189 dest,
190 from_backup: _,
191 } => eprintln!(
192 " ? skip {} (directory at dest; refuse to destroy; {})",
193 dest.display(),
194 entry_id
195 ),
196 RestoredChange::SkippedNoMatch {
197 entry_id,
198 from_backup,
199 } => eprintln!(
200 " ? skip {} (no link-map match for entry; {})",
201 from_backup.display(),
202 entry_id
203 ),
204 RestoredChange::SkippedAmbiguous {
205 entry_id,
206 from_backup,
207 candidates,
208 } => {
209 eprintln!(
210 " ? skip {} (ambiguous: {} candidates; {})",
211 from_backup.display(),
212 candidates.len(),
213 entry_id
214 );
215 for cand in candidates {
216 eprintln!(" candidate: {}", cand.display());
217 }
218 }
219 RestoredChange::SkippedSymlinkForeign {
220 entry_id,
221 dest,
222 actual_target,
223 expected_install_source,
224 from_backup: _,
225 } => eprintln!(
226 " ? skip {} (foreign target: {}; expected: {}; {})",
227 dest.display(),
228 actual_target.display(),
229 expected_install_source.display(),
230 entry_id
231 ),
232 }
233}