use std::{ fmt::Display, fs::{create_dir_all, File}, io::Read, path::{Path, PathBuf}, }; use serde::{Deserialize, Serialize}; use tracing::info; use crate::{ backup::{Backup, Id}, config::Config, error::{Error, Result}, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PathInfo { is_file: bool, rel_location: String, location_root: LocationRoot, last_modified: Option, children: Vec, } impl PathInfo { pub fn from_path(config: &Config, path: &str) -> Result { let locations = Self::parse_location(path, config)?; Self::handle_dir(config, &locations.0, &locations.1) } fn handle_dir( config: &Config, rel_location: &str, location_root: &LocationRoot, ) -> Result { info!("Handling {rel_location}"); let path = Self::get_abs_path(&location_root.to_string(), rel_location); Ok(if path.is_dir() { let mut last_modified = Some(String::new()); let mut last_modified_timestamp = 0; let mut children: Vec = Vec::new(); let paths = std::fs::read_dir(path).unwrap(); for path in paths { let pathstr = path.unwrap().path().to_string_lossy().to_string(); let root = format!("{location_root}/"); let Some(rl) = pathstr.split_once(&root) else { panic!("HUH"); }; let handle = Self::handle_dir(config, rl.1, location_root)?; if let Some(lm) = handle.last_modified.clone() { if last_modified.is_some() { let ts = Backup::from_index(config, &lm)?.timestamp; if ts > last_modified_timestamp { last_modified_timestamp = ts; last_modified = Some(lm); }; } } else { last_modified = None; }; children.push(handle); } Self { is_file: false, rel_location: rel_location.to_string(), location_root: location_root.clone(), last_modified, children, } } else { Self::from_file(config, rel_location, location_root)? }) } fn from_file( config: &Config, rel_location: &str, location_root: &LocationRoot, ) -> Result { let last_modified = Self::compare_to_last_modified(config, location_root, rel_location)?; info!("From file {rel_location} ({last_modified:?})"); Ok(Self { rel_location: rel_location.to_string(), location_root: location_root.clone(), last_modified, is_file: true, children: Vec::new(), }) } pub fn compare_to_last_modified( config: &Config, location_root: &LocationRoot, rel_location: &str, ) -> Result> { let Some(last_backup) = Backup::get_last(config)? else { // First Backup return Ok(None); }; let files = last_backup.files.clone(); let last_file_opt = Self::find_last_modified(files, rel_location, location_root); let Some(last_file) = last_file_opt else { // File didn't exist last Backup return Ok(None); }; let modified_backup = if let Some(modified_backup_id) = &last_file.last_modified { Backup::from_index(config, modified_backup_id)? } else { last_backup }; let old_path = modified_backup.get_absolute_file_location(config, &last_file.rel_location); let new_path = format!("{location_root}/{rel_location}"); let mut old = File::open(old_path)?; let mut new = File::open(new_path)?; let old_len = old.metadata()?.len(); let new_len = new.metadata()?.len(); if old_len != new_len { return Ok(None); } let mut old_content = String::new(); old.read_to_string(&mut old_content)?; let mut new_content = String::new(); new.read_to_string(&mut new_content)?; if old_content != new_content { return Ok(None); } Ok(Some(modified_backup.id.clone())) } pub fn find_last_modified( files: Vec, rel_location: &str, location_root: &LocationRoot, ) -> Option { for path in files { if path.is_file { if path.rel_location == rel_location && path.location_root == *location_root { return Some(path); }; } else { let is_modified = PathInfo::find_last_modified(path.children, rel_location, location_root); if is_modified.is_some() { return is_modified; }; } } None } pub fn get_absolute_path(&self) -> PathBuf { Self::get_abs_path(&self.location_root.to_string(), &self.rel_location) } pub fn save(&self, backup_root: &str) -> Result<()> { if self.last_modified.is_some() { return Ok(()); } info!("Save File {:?}", self.rel_location); if self.is_file { let new_path = format!("{}/{}", backup_root, self.rel_location); let np = Path::new(&new_path); if let Some(parent) = np.parent() { create_dir_all(parent)?; } std::fs::copy(self.get_absolute_path(), new_path)?; } else { for child in &self.children { child.save(backup_root)?; } }; Ok(()) } fn get_abs_path(location_root: &str, rel_location: &str) -> PathBuf { let path = format!("{location_root}/{rel_location}"); PathBuf::from(path) } fn parse_location(value: &str, config: &Config) -> Result<(String, LocationRoot)> { let Some(split) = value.split_once('/') else { return Err(Error::InvalidDirectory(value.to_string())); }; if split.0.starts_with('~') { return Ok(( split.1.to_string(), LocationRoot::User, )); }; Ok(( split.1.to_string(), LocationRoot::from_op_str(split.0, config)?, )) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LocationRoot { User, Custom(String), SystemConfig, UserConfig, Root, } impl Display for LocationRoot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LocationRoot::User => write!(f, "{}", dirs::home_dir().unwrap().to_string_lossy()), LocationRoot::Custom(loc) => write!(f, "{loc}"), LocationRoot::SystemConfig => write!(f, "/etc"), LocationRoot::UserConfig => { write!(f, "{}", dirs::config_local_dir().unwrap().to_string_lossy()) } LocationRoot::Root => write!(f, "/"), } } } impl LocationRoot { fn from_op_str(value: &str, config: &Config) -> Result { let split_str = value.split_once(':'); let Some(split_op) = split_str else { return Err(Error::NoIndex); }; match split_op.0 { "u" => Ok(Self::User), "s" => Ok(Self::SystemConfig), "d" => Ok(Self::UserConfig), "r" => Ok(Self::Root), "c" => Ok(Self::Custom( config .custom_directories .get(split_op.1) .ok_or_else(|| Error::CustomDirectory(split_op.1.to_string()))? .to_string(), )), _ => Err(Error::InvalidIndex(split_op.0.to_string())), } } } #[cfg(test)] mod tests { use std::{ fs::{create_dir_all, remove_dir_all, File}, io::Write, }; use crate::{ backup::Backup, config::Config, error::{Error, Result}, }; use super::LocationRoot; use super::PathInfo; #[test] fn from_op_str() -> Result<()> { let mut config = Config::default(); config .custom_directories .insert("test".to_string(), "/usr/local/test".to_string()); let mut values_ok: Vec<(&str, LocationRoot)> = Vec::new(); values_ok.push(("u:test", LocationRoot::User)); values_ok.push(("s:", LocationRoot::SystemConfig)); values_ok.push(("r:", LocationRoot::Root)); values_ok.push(( "c:test", LocationRoot::Custom("/usr/local/test".to_string()), )); for value in values_ok { println!("Testing {value:?}"); assert_eq!(LocationRoot::from_op_str(value.0, &config)?, value.1); println!("\x1B[FTesting {value:?} ✓"); } let mut values_err: Vec<(&str, String)> = Vec::new(); values_err.push(( "c:rest", Error::CustomDirectory("rest".to_string()).to_string(), )); values_err.push(("t:test/", Error::InvalidIndex("t".to_string()).to_string())); values_err.push(( "test:test/usr", Error::InvalidIndex("test".to_string()).to_string(), )); values_err.push(("/usr/local/test", Error::NoIndex.to_string())); values_err.push(("c/usr/local/test", Error::NoIndex.to_string())); for value in values_err { println!("Testing {value:?}"); assert_eq!( LocationRoot::from_op_str(value.0, &config) .err() .unwrap() .to_string(), value.1 ); println!("\x1B[FTesting {value:?} ✓"); } Ok(()) } #[test] fn parse_location() -> Result<()> { let mut config = Config::default(); config .custom_directories .insert("test".to_string(), "/usr/local/test".to_string()); let mut values_ok: Vec<(&str, (String, LocationRoot))> = Vec::new(); values_ok.push(( "~/.config/nvim", ( ".config/nvim".to_string(), LocationRoot::User, ), )); values_ok.push(( "u:test/.config/nvim", ( ".config/nvim".to_string(), LocationRoot::User, ), )); values_ok.push(( "r:/.config/nvim", (".config/nvim".to_string(), LocationRoot::Root), )); values_ok.push(( "r:/.config/nvim", (".config/nvim".to_string(), LocationRoot::Root), )); values_ok.push(( "s:/.config/nvim", (".config/nvim".to_string(), LocationRoot::SystemConfig), )); values_ok.push(( "c:test/.config/nvim", ( ".config/nvim".to_string(), LocationRoot::Custom("/usr/local/test".to_string()), ), )); for value in values_ok { print!("Testing {value:?}"); assert_eq!(PathInfo::parse_location(&value.0, &config)?, value.1); println!("\x1B[FTesting {value:?} ✓"); } Ok(()) } #[test] fn compare_to_last_modified() -> color_eyre::Result<()> { let mut config = Config::default(); config.root = "./backup-test".to_string(); config .directories .push("u:fx/code/proj/arbs/backup-test-dir".to_string()); create_dir_all("./backup-test-dir")?; let mut f = File::create("./backup-test-dir/size.txt")?; f.write_all("unmodified".as_bytes())?; let mut f = File::create("./backup-test-dir/content.txt")?; f.write_all("unmodified".as_bytes())?; let mut f = File::create("./backup-test-dir/nothing.txt")?; f.write_all("unmodified".as_bytes())?; let backup = Backup::create(&config, None)?; backup.save(&config)?; let mut f = File::create("./backup-test-dir/size.txt")?; f.write_all("modified".as_bytes())?; let mut f = File::create("./backup-test-dir/content.txt")?; f.write_all("unmodefied".as_bytes())?; let pi = PathInfo::from_path(&config, "u:fx/code/proj/arbs/backup-test-dir")?; let last_backup = Backup::get_last(&config)?.unwrap(); for file in pi.children { println!("test rel: {}", file.rel_location); let res = if file.rel_location == "code/proj/arbs/backup-test-dir/nothing.txt" { Some(last_backup.id.clone()) } else { None }; println!("Testing {file:?}"); assert_eq!( PathInfo::compare_to_last_modified( &config, &file.location_root, &file.rel_location )?, res ); println!("\x1B[FTesting {file:?} ✓"); } remove_dir_all("./backup-test-dir")?; remove_dir_all("./backup-test")?; Ok(()) } }