1use anyhow::bail;
5use clap::Subcommand;
6use std::{
7 fs::File,
8 path::{Path, PathBuf},
9};
10
11use crate::{cli_shared::read_config, networks::NetworkChain};
12
13#[derive(Subcommand)]
14pub enum BackupCommands {
15 Create {
18 #[arg(long)]
20 backup_file: Option<PathBuf>,
21 #[arg(long)]
23 all: bool,
24 #[arg(long)]
26 no_keypair: bool,
27 #[arg(long)]
29 no_keystore: bool,
30 #[arg(long)]
32 backup_chain: Option<NetworkChain>,
33 #[arg(long)]
35 include_proof_params: bool,
36 #[arg(short, long)]
38 daemon_config: Option<PathBuf>,
39 },
40 Restore {
42 backup_file: PathBuf,
44 #[arg(short, long)]
46 daemon_config: Option<PathBuf>,
47 #[arg(long)]
50 force: bool,
51 },
52}
53
54impl BackupCommands {
55 pub fn run(self) -> anyhow::Result<()> {
56 match self {
57 BackupCommands::Create {
58 backup_file,
59 all,
60 no_keypair,
61 no_keystore,
62 backup_chain,
63 include_proof_params,
64 daemon_config,
65 } => {
66 let (_, config) = read_config(daemon_config.as_ref(), backup_chain.clone())?;
67
68 let data_dir = &config.client.data_dir;
69
70 let backup_entries = if all {
71 std::fs::read_dir(data_dir)?
72 .filter_map(Result::ok)
73 .map(|e| e.path())
74 .collect()
75 } else {
76 validate_and_add_entries(
77 data_dir,
78 no_keypair,
79 no_keystore,
80 backup_chain,
81 include_proof_params,
82 )?
83 };
84
85 let backup_file_path = if let Some(backup_file) = backup_file {
86 backup_file
87 } else {
88 let path = PathBuf::from(format!(
89 "forest-backup-{}.tar",
90 chrono::Utc::now().format("%Y-%m-%d_%H-%M-%S")
91 ));
92 if path.exists() {
93 bail!("Backup file already exists at {}", path.display());
94 }
95 path
96 };
97
98 archive_entries(data_dir, backup_entries, &backup_file_path)?;
99 println!("Backup complete: {}", backup_file_path.display());
100
101 Ok(())
102 }
103 BackupCommands::Restore {
104 backup_file,
105 daemon_config,
106 force,
107 } => {
108 let (_, config) = read_config(daemon_config.as_ref(), None)?;
109 let data_dir = &config.client.data_dir;
110
111 extract_entries(data_dir, &backup_file, force)?;
112 println!("Restore complete");
113
114 Ok(())
115 }
116 }
117 }
118}
119
120fn extract_entries(data_dir: &Path, backup_file: &Path, force: bool) -> anyhow::Result<()> {
121 let backup_file = File::open(backup_file)?;
122 let mut archive = tar::Archive::new(backup_file);
123 for file in archive.entries()? {
124 let mut file = file?;
125 let path = file.path()?;
126 let path = data_dir.join(path);
127 if path.exists() && !force {
128 bail!(
129 "File already exists at {}. Use --force to overwrite.",
130 path.display()
131 );
132 }
133 if let Some(parent) = path.parent() {
134 std::fs::create_dir_all(parent)?;
135 }
136 println!("Restoring {}", path.display());
137 file.unpack(path)?;
138 }
139
140 Ok(())
141}
142
143fn archive_entries(
144 data_dir: &PathBuf,
145 backup_entries: Vec<PathBuf>,
146 backup_file_path: &Path,
147) -> anyhow::Result<()> {
148 let backup_file = File::create(backup_file_path)?;
149 let mut archive = tar::Builder::new(backup_file);
150 for entry in backup_entries {
151 let entry_canonicalized = entry.canonicalize()?;
152 let name = entry.strip_prefix(data_dir)?;
153
154 println!("Adding {} to backup", entry_canonicalized.display());
155 if entry_canonicalized.is_dir() {
156 archive.append_dir_all(name, entry_canonicalized)?;
157 } else {
158 archive.append_path_with_name(entry_canonicalized, name)?;
159 }
160 }
161 archive.into_inner()?;
162 Ok(())
163}
164
165fn validate_and_add_entries(
166 data_dir: &Path,
167 no_keypair: bool,
168 no_keystore: bool,
169 backup_chain: Option<NetworkChain>,
170 include_proof_params: bool,
171) -> anyhow::Result<Vec<PathBuf>> {
172 if no_keypair && no_keystore && backup_chain.is_none() && !include_proof_params {
173 bail!("Nothing to backup!");
174 }
175
176 let mut valid = true;
177 let mut backup_entries = vec![];
178
179 if !no_keypair {
180 let keypair_path = data_dir.join("libp2p").join("keypair");
181 if keypair_path.exists() {
182 backup_entries.push(keypair_path);
183 } else {
184 println!("Keypair not found at {}", keypair_path.display());
185 valid = false;
186 }
187 }
188
189 if !no_keystore {
190 let mut any_keystore_found = false;
191 for keystore in ["keystore.json", "keystore"].iter() {
192 let keystore_path = data_dir.join(keystore);
193 if keystore_path.exists() {
194 backup_entries.push(keystore_path);
195 any_keystore_found = true;
196 }
197 }
198 if !any_keystore_found {
199 println!("No keystore found at {}", data_dir.display());
200 valid = false;
201 }
202 }
203
204 if let Some(chain) = backup_chain {
205 let chain_path = data_dir.join(chain.to_string());
206 if chain_path.exists() {
207 backup_entries.push(chain_path);
208 } else {
209 println!("Chain data not found at {}", chain_path.display());
210 valid = false;
211 }
212 }
213
214 if include_proof_params {
215 let proof_params_path = data_dir.join("filecoin-proof-parameters");
216 if proof_params_path.exists() {
217 backup_entries.push(proof_params_path);
218 } else {
219 println!(
220 "Proof parameters not found at {}",
221 proof_params_path.display()
222 );
223 valid = false;
224 }
225 }
226
227 if !valid {
228 bail!("Backup aborted. Some files were not found.");
229 }
230
231 Ok(backup_entries)
232}
233
234#[cfg(test)]
235mod test {
236 use itertools::Itertools;
237 use tempfile::TempDir;
238 use walkdir::WalkDir;
239
240 use super::*;
241
242 #[test]
243 fn validate_and_add_entries_no_entries() {
244 let data_dir = PathBuf::from("test");
245 let result = validate_and_add_entries(
246 &data_dir, true, true, None, false, );
251 assert!(result.is_err());
252 }
253
254 fn create_test_data() -> (TempDir, Vec<PathBuf>) {
255 let temp_dir = tempfile::tempdir().unwrap();
256 let data_dir = temp_dir.path().to_path_buf();
257 let mut entries = vec![
258 data_dir.join("libp2p").join("keypair"),
259 data_dir.join("keystore.json"),
260 data_dir.join("keystore"),
261 ];
262
263 entries.iter().for_each(|entry| {
264 std::fs::create_dir_all(entry.parent().unwrap()).unwrap();
265 File::create(entry).unwrap();
266 });
267
268 let chain_path = data_dir.join("calibnet");
269 std::fs::create_dir_all(&chain_path).unwrap();
270 entries.push(chain_path);
271
272 let proof_params_path = data_dir.join("filecoin-proof-parameters");
273 std::fs::create_dir_all(&proof_params_path).unwrap();
274 entries.push(proof_params_path);
275
276 (temp_dir, entries)
277 }
278
279 #[test]
280 fn validate_and_add_entries_all() {
281 let (temp_dir, entries) = create_test_data();
282 let data_dir = temp_dir.path().to_path_buf();
283
284 let result = validate_and_add_entries(
285 &data_dir,
286 false, false, Some(NetworkChain::Calibnet), true, );
291
292 let backup_entries = result.unwrap();
293 itertools::assert_equal(entries.iter().sorted(), backup_entries.iter().sorted());
294 }
295
296 #[test]
297 fn validate_and_add_entries_no_keypair() {
298 let (temp_dir, _) = create_test_data();
299 let data_dir = temp_dir.path().to_path_buf();
300
301 std::fs::remove_file(data_dir.join("libp2p").join("keypair")).unwrap();
302
303 let result = validate_and_add_entries(
304 &data_dir,
305 false, false, Some(NetworkChain::Calibnet), true, );
310 assert!(result.is_err());
311
312 let result = validate_and_add_entries(
313 &data_dir,
314 true, false, Some(NetworkChain::Calibnet), true, );
319 assert!(result.is_ok());
320 }
321
322 #[test]
323 fn validate_and_add_entries_no_keystore() {
324 let (temp_dir, _) = create_test_data();
325 let data_dir = temp_dir.path().to_path_buf();
326
327 std::fs::remove_file(data_dir.join("keystore.json")).unwrap();
328 let result = validate_and_add_entries(
329 &data_dir,
330 false, false, Some(NetworkChain::Calibnet), true, );
335 assert!(result.is_ok());
337
338 std::fs::remove_file(data_dir.join("keystore")).unwrap();
339 let result = validate_and_add_entries(
340 &data_dir,
341 false, false, Some(NetworkChain::Calibnet), true, );
346 assert!(result.is_err());
347
348 let result = validate_and_add_entries(
349 &data_dir,
350 false, true, Some(NetworkChain::Calibnet), true, );
355 assert!(result.is_ok());
356 }
357
358 #[test]
359 fn validate_and_add_entries_proof_params() {
360 let (temp_dir, _) = create_test_data();
361 let data_dir = temp_dir.path().to_path_buf();
362
363 std::fs::remove_dir_all(data_dir.join("filecoin-proof-parameters")).unwrap();
364 let result = validate_and_add_entries(
365 &data_dir,
366 false, false, Some(NetworkChain::Calibnet), true, );
371 assert!(result.is_err());
372
373 let result = validate_and_add_entries(
374 &data_dir,
375 false, false, Some(NetworkChain::Calibnet), false, );
380 assert!(result.is_ok());
381 }
382
383 #[test]
384 fn validate_and_add_entries_no_chain() {
385 let (temp_dir, _) = create_test_data();
386 let data_dir = temp_dir.path().to_path_buf();
387
388 std::fs::remove_dir_all(data_dir.join("calibnet")).unwrap();
389 let result = validate_and_add_entries(
390 &data_dir,
391 false, false, Some(NetworkChain::Calibnet), true, );
396 assert!(result.is_err());
397
398 let result = validate_and_add_entries(
399 &data_dir, false, false, None, true, );
404 assert!(result.is_ok());
405 }
406
407 #[test]
408 fn archive_extract_roundtrip() {
409 let (temp_dir, entries) = create_test_data();
410 let data_dir = temp_dir.path().to_path_buf();
411
412 let backup_file = tempfile::Builder::new().suffix(".tar").tempfile().unwrap();
413 archive_entries(&data_dir, entries.clone(), backup_file.path()).unwrap();
414
415 let restore_dir = tempfile::tempdir().unwrap();
416 extract_entries(restore_dir.path(), backup_file.path(), true).unwrap();
417
418 let get_entries_recurse = |dir| {
420 WalkDir::new(dir)
421 .into_iter()
422 .filter_map(Result::ok)
423 .map(|entry| entry.path().strip_prefix(dir).unwrap().to_path_buf())
424 .sorted()
425 .collect::<Vec<PathBuf>>()
426 };
427 let restored = get_entries_recurse(restore_dir.path());
428 let original = get_entries_recurse(&data_dir);
429
430 assert!(restored.len() > entries.len());
431 itertools::assert_equal(original.iter(), restored.iter());
432 }
433}