summaryrefslogblamecommitdiff
path: root/src/pathinfo.rs
blob: 8b1ca2f03cfb8d7ab17726a6766742e24e7790e2 (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::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<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> {
        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<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)?;

        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<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(())
    }

    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('~') {
            if config.user.len() != 1 {
                return Err(Error::MultiUser);
            }
            return Ok((
                split.1.to_string(),
                LocationRoot::User(config.user[0].clone()),
            ));
        };
        Ok((
            split.1.to_string(),
            LocationRoot::from_op_str(split.0, config)?,
        ))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum LocationRoot {
    User(String),
    Custom(String),
    SystemSettings,
    Root,
}

impl Display for LocationRoot {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LocationRoot::User(user) => write!(f, "/home/{user}"),
            LocationRoot::Custom(loc) => write!(f, "{loc}"),
            LocationRoot::SystemSettings => write!(f, "/etc"),
            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(split_op.1.to_string())),
            "s" => Ok(Self::SystemSettings),
            "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},
        packages::{pacman::Pacman, PackageManager},
    };

    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("test".to_string())));
        values_ok.push(("s:", LocationRoot::SystemSettings));
        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.user.push("test".to_string());
        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("test".to_string()),
            ),
        ));
        values_ok.push((
            "u:test/.config/nvim",
            (
                ".config/nvim".to_string(),
                LocationRoot::User("test".to_string()),
            ),
        ));
        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::SystemSettings),
        ));
        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 pacman = Pacman;
        let pkgs = pacman.get_installed()?;
        let backup = Backup::create(&config, pkgs)?;
        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(())
    }
}