summaryrefslogblamecommitdiff
path: root/src/pathinfo.rs
blob: 80614cd5f228e80c22b0f8027402e43bea06211d (plain) (tree)
1
2
3
4
5
6
7
8
9

                 


                               


                                    
                                  

            
                         





                                               
                  

                                
                              
                            





                                                                   
                                                            






                                     
                                          

                                                                                


                                                        




                                                                                 
                                                       



                                                                            
                                                                
                                                







                                                                            



                                      


                                                       

                              

                
                                                                 


          





                                                                                                 
 
                                                               


                                                   

                                                 
                          
                                 


          









                                                                
                                              
                                                                                         

                                                  


                            
                                                                                          







                                                                                                   

                                            

















                                              






                                     
                                                                                              


                                      









                                                                                             



                                                                               



                                                         
                                                   
                         
                                                                            




                                                               



                                         




              


























                                                                                       
                                                                         
                                                             







                                                                                       
                                                                 









                                                              
         
                   

                 





                                                                        
                                                                                               
                                                            



                                                                                    











                                                                  


                                          












                                                                                   










                                                   











                                                                       








                                                                    






                                                                              












                                                                          


















                                                           



                                                                       
                             
             





                                                                 
              



















                                                                         


                                        
                                                                             






                                                             



                                                                    



                                                  
                                           








                                                                   
                                              






                                                                   



                                                                                 



                                                              

                                                       



                                            
                                            







                                                   
                                                      






                                             
use std::{
    fmt::Display,
    fs::{create_dir_all, File},
    io::Read,
    path::{Path, PathBuf},
};

use serde::{Deserialize, Serialize};
use tracing::{debug, info, trace};

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<Id>,
    children: Vec<PathInfo>,
}

impl PathInfo {
    pub fn from_path(config: &Config, path: &str) -> Result<Self> {
        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<Self> {
        trace!("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<PathInfo> = 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<Self> {
        let last_modified = Self::compare_to_last_modified(config, location_root, rel_location)?;

        debug!("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<Option<String>> {
        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<Self>,
        rel_location: &str,
        location_root: &LocationRoot,
    ) -> Option<PathInfo> {
        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(())
    }

    pub fn restore(&self, config: &Config, backup_root: &str) -> Result<()> {
        if self.is_file {
            info!(?self.rel_location, "Restore File");
            let backup_path = if let Some(last_modified) = self.last_modified.clone() {
                let backup = Backup::from_index(config, &last_modified)?;
                &backup.get_location(config).get_absolute_dir(config)
            } else {
                backup_root
            };
            let backup_loc = format!("{}/{}", backup_path, self.rel_location);
            let system_loc = self.get_absolute_path();
            debug!(?backup_loc, ?system_loc, "copy");

            if let Some(parents) = system_loc.parent() {
                create_dir_all(parents)?;
            }

            std::fs::copy(backup_loc, system_loc)?;
        } else {
            for path in &self.children {
                path.restore(config, 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<Self> {
        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 values_ok = vec![
            ("u:test", LocationRoot::User),
            ("s:", LocationRoot::SystemConfig),
            ("r:", LocationRoot::Root),
            (
                "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 values_err = vec![
            (
                "c:rest",
                Error::CustomDirectory("rest".to_string()).to_string(),
            ),
            ("t:test/", Error::InvalidIndex("t".to_string()).to_string()),
            (
                "test:test/usr",
                Error::InvalidIndex("test".to_string()).to_string(),
            ),
            ("/usr/local/test", Error::NoIndex.to_string()),
            ("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 values_ok = vec![
            (
                "~/.config/nvim",
                (".config/nvim".to_string(), LocationRoot::User),
            ),
            (
                "u:test/.config/nvim",
                (".config/nvim".to_string(), LocationRoot::User),
            ),
            (
                "r:/.config/nvim",
                (".config/nvim".to_string(), LocationRoot::Root),
            ),
            (
                "r:/.config/nvim",
                (".config/nvim".to_string(), LocationRoot::Root),
            ),
            (
                "s:/.config/nvim",
                (".config/nvim".to_string(), LocationRoot::SystemConfig),
            ),
            (
                "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 cwd = std::env::current_dir()?;
        let test_dir = format!("{}/backup-test-dir", cwd.display());

        let mut config = Config::default();
        config.root = "./backup-test".to_string();
        config
            .directories
            .push(format!("r:{test_dir}"));

        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)?;
        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, format!("r:{test_dir}").as_str())?;

        let nothing_full = format!("{test_dir}/nothing.txt");
        let nothing = &nothing_full[1..nothing_full.len()];

        let last_backup = Backup::get_last(&config)?.unwrap();
        for file in pi.children {
            println!("test rel: {}", file.rel_location);
            println!("nothing: {}", nothing);
            let res = if file.rel_location == nothing {
                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(())
    }
}