1use std::path::{Path, PathBuf};
2
3use super::{
4 idempotence, integrity, selector, steps,
5 types::{RestoreOptions, SnapshotSel},
6};
7use crate::fs::backup::sidecar::read_sidecar;
8use crate::types::safepath::SafePath;
9
10pub fn restore_file(
16 target: &SafePath,
17 dry_run: bool,
18 force_best_effort: bool,
19 backup_tag: &str,
20) -> std::io::Result<()> {
21 let opts = RestoreOptions {
22 dry_run,
23 force_best_effort,
24 backup_tag: backup_tag.to_string(),
25 };
26 restore_impl(target, SnapshotSel::Latest, &opts)
27}
28
29pub fn restore_file_prev(
36 target: &SafePath,
37 dry_run: bool,
38 force_best_effort: bool,
39 backup_tag: &str,
40) -> std::io::Result<()> {
41 let opts = RestoreOptions {
42 dry_run,
43 force_best_effort,
44 backup_tag: backup_tag.to_string(),
45 };
46 restore_impl(target, SnapshotSel::Previous, &opts)
47}
48
49#[allow(
55 clippy::too_many_lines,
56 reason = "Will split into RestorePlanner plan/execute in PR6"
57)]
58pub fn restore_impl(
59 target: &SafePath,
60 sel: SnapshotSel,
61 opts: &RestoreOptions,
62) -> std::io::Result<()> {
63 let target_path = target.as_path();
64 if opts.dry_run {
67 return Ok(());
68 }
69 match RestorePlanner::plan(&target_path, sel, opts) {
70 Ok((_backup_opt, _sidecar_opt, action)) => RestorePlanner::execute(&target_path, action),
71 Err(e)
72 if e.kind() == std::io::ErrorKind::NotFound && matches!(sel, SnapshotSel::Previous) =>
73 {
74 match RestorePlanner::plan(&target_path, SnapshotSel::Latest, opts) {
77 Ok((_b2, _s2, action2)) => RestorePlanner::execute(&target_path, action2),
78 Err(e2) => Err(e2),
79 }
80 }
81 Err(e) => Err(e),
82 }
83}
84
85#[derive(Debug, Clone)]
87pub enum RestoreAction {
88 Noop,
89 FileRename {
90 backup: PathBuf,
91 mode: Option<u32>,
92 },
93 SymlinkTo {
94 dest: PathBuf,
95 cleanup_backup: Option<PathBuf>,
96 },
97 EnsureAbsent {
98 cleanup_backup: Option<PathBuf>,
99 },
100 LegacyRename {
101 backup: PathBuf,
102 },
103}
104
105struct RestorePlanner;
107
108impl RestorePlanner {
109 #[allow(
110 clippy::too_many_lines,
111 reason = "Will be split into RestorePlanner plan/execute in follow-up PR"
112 )]
113 fn plan(
114 target: &Path,
115 sel: SnapshotSel,
116 opts: &RestoreOptions,
117 ) -> std::io::Result<(
118 Option<PathBuf>,
119 Option<crate::fs::backup::sidecar::BackupSidecar>,
120 RestoreAction,
121 )> {
122 let pair = match sel {
124 SnapshotSel::Latest => selector::latest(target, &opts.backup_tag),
125 SnapshotSel::Previous => selector::previous(target, &opts.backup_tag),
126 };
127 let (backup_opt, sidecar_path): (Option<PathBuf>, PathBuf) = if let Some(p) = pair {
128 p
129 } else {
130 if !opts.force_best_effort {
131 return Err(std::io::Error::new(
132 std::io::ErrorKind::NotFound,
133 "backup missing",
134 ));
135 }
136 return Ok((None, None, RestoreAction::Noop));
137 };
138 let sc = read_sidecar(&sidecar_path).ok();
140 if let Some(ref side) = sc {
141 if idempotence::is_idempotent(
143 target,
144 side.prior_kind.as_str(),
145 side.prior_dest.as_deref(),
146 ) {
147 return Ok((backup_opt, sc, RestoreAction::Noop));
148 }
149 }
150 if let Some(side) = sc.clone() {
151 let action = match side.prior_kind.as_str() {
152 "file" => {
153 let backup: PathBuf = if let Some(p) = backup_opt.clone() {
154 p
155 } else {
156 if opts.force_best_effort {
157 return Ok((backup_opt, Some(side), RestoreAction::Noop));
158 }
159 return Err(std::io::Error::new(
160 std::io::ErrorKind::NotFound,
161 "backup payload missing",
162 ));
163 };
164 if let Some(ref expected) = side.payload_hash {
165 if !integrity::verify_payload_hash_ok(&backup, expected.as_str()) {
166 if opts.force_best_effort {
167 return Ok((backup_opt, Some(side), RestoreAction::Noop));
168 }
169 return Err(std::io::Error::new(
170 std::io::ErrorKind::NotFound,
171 "backup payload hash mismatch",
172 ));
173 }
174 }
175 let mode = side
176 .mode
177 .as_ref()
178 .and_then(|ms| u32::from_str_radix(ms, 8).ok());
179 RestoreAction::FileRename { backup, mode }
180 }
181 "symlink" => {
182 if let Some(dest) = side.prior_dest.as_ref() {
183 RestoreAction::SymlinkTo {
184 dest: PathBuf::from(dest),
185 cleanup_backup: backup_opt.clone(),
186 }
187 } else if let Some(backup) = backup_opt.clone() {
188 RestoreAction::LegacyRename { backup }
189 } else if opts.force_best_effort {
190 RestoreAction::Noop
191 } else {
192 return Err(std::io::Error::new(
193 std::io::ErrorKind::NotFound,
194 "backup payload missing",
195 ));
196 }
197 }
198 "none" => RestoreAction::EnsureAbsent {
199 cleanup_backup: backup_opt.clone(),
200 },
201 _ => {
202 if let Some(backup) = backup_opt.clone() {
203 RestoreAction::LegacyRename { backup }
204 } else if opts.force_best_effort {
205 RestoreAction::Noop
206 } else {
207 return Err(std::io::Error::new(
208 std::io::ErrorKind::NotFound,
209 "backup payload missing",
210 ));
211 }
212 }
213 };
214 return Ok((backup_opt, Some(side), action));
215 }
216 if let Some(backup) = backup_opt.clone() {
218 Ok((backup_opt, None, RestoreAction::LegacyRename { backup }))
219 } else if opts.force_best_effort {
220 Ok((None, None, RestoreAction::Noop))
221 } else {
222 Err(std::io::Error::new(
223 std::io::ErrorKind::NotFound,
224 "backup missing",
225 ))
226 }
227 }
228
229 fn execute(target: &Path, action: RestoreAction) -> std::io::Result<()> {
230 match action {
231 RestoreAction::Noop => Ok(()),
232 RestoreAction::FileRename { backup, mode } => {
233 steps::restore_file_bytes(target, &backup, mode)
234 }
235 RestoreAction::SymlinkTo {
236 dest,
237 cleanup_backup,
238 } => {
239 steps::restore_symlink_to(target, &dest)?;
240 if let Some(b) = cleanup_backup {
241 let _ = std::fs::remove_file(b);
242 }
243 Ok(())
244 }
245 RestoreAction::EnsureAbsent { cleanup_backup } => {
246 steps::ensure_absent(target)?;
247 if let Some(b) = cleanup_backup {
248 let _ = std::fs::remove_file(b);
249 }
250 Ok(())
251 }
252 RestoreAction::LegacyRename { backup } => steps::legacy_rename(target, &backup),
253 }
254 }
255}