use regex::Regex; use std::fs; use std::time::SystemTime; use std::vec::Vec; use structopt::StructOpt; /// A simple backup program #[derive(StructOpt, Debug)] #[structopt(name = "Bekape")] struct Opt { /// The folder to backup #[structopt(name = "source")] source: String, /// The folder to store the backup #[structopt(name = "destination")] destination: String, } fn browse_recursively(dir: &str) -> (Vec, Vec) { let mut content = Vec::new(); let mut errors = Vec::new(); let ls = fs::read_dir(dir); if let Ok(ls) = ls { for entry in ls { if let Ok(entry) = entry { let full_path = entry.path().to_string_lossy().to_string(); if let Ok(file_type) = entry.file_type() { if file_type.is_dir() { let mut result = browse_recursively(&full_path); content.append(&mut result.0); errors.append(&mut result.1); } else { content.push(full_path); } } else { errors.push(full_path); } } } } else { errors.push(dir.to_string()); } (content, errors) } fn get_file_metadata(file: &str) -> (SystemTime, u64) { if let Ok(metadata) = fs::metadata(file) { let time = metadata.modified().unwrap_or(std::time::UNIX_EPOCH); let size = metadata.len(); return (time, size); }; (std::time::UNIX_EPOCH, 0) } fn backup_element(path: &str, src_root: &str, bkp_root: &str, prev_bkp_root: &Option) { let src_file = format!("{}{}", src_root, path); let bkp_file = format!("{}{}", bkp_root, path); let p = bkp_file.rfind('/').unwrap(); // TODO: Is unwrap okay here? let bkp_folder = bkp_file[0..p].to_string(); let _ = fs::create_dir_all(&bkp_folder); let mut backup_done = false; if let Some(prev_bkp_root) = prev_bkp_root { let prev_bkp_file = format!("{}{}", prev_bkp_root, path); if std::path::Path::new(&prev_bkp_file).exists() { let prev_bkp_metadata = get_file_metadata(&prev_bkp_file); let src_metadata = get_file_metadata(&src_file); backup_done = prev_bkp_metadata.0 > src_metadata.0 && prev_bkp_metadata.1 == src_metadata.1 && fs::hard_link(&prev_bkp_file, &bkp_file).is_ok(); } } if !backup_done && fs::copy(&src_file, &bkp_file).is_err() { eprintln!("Could not copy: {} -> {}", src_file, bkp_file); } } fn main() { let opt = Opt::from_args(); let dt = chrono::offset::Utc::now().format("%Y-%m-%d_%H-%M-%S"); let backup_folder = format!("{}/backup_{}_wip", opt.destination, dt); if fs::create_dir(&backup_folder).is_err() { eprintln!("Could not create backup directory: {}", &backup_folder); std::process::exit(1); } // find previous backup let backup_regex = Regex::new(r"backup_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$").unwrap(); let ls = fs::read_dir(&opt.destination); let prev_backup = if let Ok(ls) = ls { let mut ls = ls .filter_map(|s| s.ok()) .map(|s| s.path().to_string_lossy().trim().to_owned()) .filter(|s| backup_regex.is_match(&s)) .collect::>(); if ls.is_empty() { None } else { ls.sort(); ls.last().cloned() } } else { None }; let result = browse_recursively(&opt.source); let content = result.0.iter().map(|s| { let l = opt.source.len(); s[l..].to_string() }); for err in result.1 { eprintln!("Could not read {}.", err); } for elem in content { backup_element(&elem, &opt.source, &backup_folder, &prev_backup); } let new_name = backup_folder.replace("_wip", ""); if fs::rename(&backup_folder, &new_name).is_err() { eprintln!("Could not rename folder."); } }