From a8d1be9536bce6d6be2cf1586c8bac049e820d31 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Sun, 8 Sep 2024 17:21:27 +0200 Subject: save files, real last modified check (doesn't work correctly) --- .gitignore | 1 + src/backup.rs | 126 +++++++++------- src/config.rs | 6 +- src/error.rs | 7 +- src/main.rs | 15 +- src/packages.rs | 5 +- src/packages/pacman.rs | 14 +- src/packages/portage.rs | 13 ++ src/pathinfo.rs | 383 +++++++++++++++++++++++++++++++++--------------- 9 files changed, 388 insertions(+), 182 deletions(-) create mode 100644 src/packages/portage.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..e39d245 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/backup-test* diff --git a/src/backup.rs b/src/backup.rs index 8cc94f1..a643cb2 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -5,7 +5,6 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use gethostname::gethostname; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -20,10 +19,11 @@ pub type BackupId = String; #[derive(Debug, Serialize, Deserialize)] pub struct Backup { - id: String, + pub id: String, timestamp: u64, packages: Vec, - files: Vec, + pub files: Vec, + device: String, } impl Backup { @@ -33,63 +33,82 @@ impl Backup { files.push(PathInfo::from_path(config, dir)?); } Ok(Self { - // UUID not really needed, maybe a shorter hash + // TODO: UUID not really needed, maybe a shorter hash id: Uuid::new_v4().to_string(), timestamp: Self::get_timestamp(), packages, files, + device: config.device.clone(), }) } pub fn save(&self, config: &Config) -> Result<()> { - let rel_location = format!( - "{}_{}", - gethostname() - .into_string() - .map_err(|_| Error::InvalidOsString)?, - Self::get_timestamp() - ); - - let bl = BackupLocation { - id: self.id.to_string(), - rel_location, - }; - - Self::append_to_root_index(config, bl.clone())?; + println!("Save Backup {:?}", self.get_location(config)); + // println!("{self:#?}"); + self.get_location(config).append_to_root(config)?; - let backup_root = format!("{}/{}", config.root, bl.rel_location); + let backup_root = self.get_location(config).get_absolute_dir(config); create_dir_all(&backup_root).unwrap(); let path = format!("{}/index.json", backup_root); let mut f = File::create(path).unwrap(); f.write_all(&serde_json::to_vec(self).unwrap()).unwrap(); + for path in &self.files { + path.save(&backup_root)?; + } + Ok(()) } - pub fn get_index(config: &Config, id: Option) -> Result { + pub fn get_last(config: &Config) -> Result> { let backup_index_root = format!("{}/index.json", config.root); - let list: Vec = Self::get_json_content(&backup_index_root)?; - println!("{list:#?}"); - - let index_loc = if let Some(id) = id { - list.iter() - .find(|bl| bl.id == id) - .ok_or(Error::BackupNotFound)? - .rel_location - .clone() - } else { - list.last() - .ok_or(Error::BackupNotFound)? - .rel_location - .clone() + let list: Vec = match Self::get_json_content(&backup_index_root) { + Ok(list) => list, + Err(err) => { + if err.to_string() == "io: No such file or directory (os error 2)" { + return Ok(None); + }; + return Err(err); + } }; + Ok(Some(Self::from_index( + config, + list.last().ok_or(Error::BackupNotFound)?.id.clone(), + )?)) + } + + pub fn from_index(config: &Config, id: BackupId) -> Result { + let backup_index_root = format!("{}/index.json", config.root); + let list: Vec = Self::get_json_content(&backup_index_root)?; + let index_loc = list + .iter() + .find(|bl| bl.id == id) + .ok_or(Error::BackupNotFound)? + .rel_location + .clone(); + let path = format!("{}/{index_loc}/index.json", config.root); let index_file: Self = Self::get_json_content(&path)?; Ok(index_file) } + pub fn get_location(&self, config: &Config) -> BackupLocation { + let rel_location = format!("{}_{}", config.device, self.timestamp); + + BackupLocation { + id: self.id.to_string(), + rel_location, + } + } + + pub fn get_absolute_file_location(&self, config: &Config, rel_location: &str) -> String { + let loc = self.get_location(config).get_absolute_dir(config); + + format!("{}/{}", loc, rel_location) + } + fn get_json_content Deserialize<'a>>(path: &str) -> Result { let mut file = File::open(path)?; let mut content = String::new(); @@ -97,7 +116,26 @@ impl Backup { Ok(serde_json::from_str(&content)?) } - fn append_to_root_index(config: &Config, new_backup: BackupLocation) -> Result<()> { + fn get_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupLocation { + id: BackupId, + rel_location: String, +} + +impl BackupLocation { + pub fn get_absolute_dir(&self, config: &Config) -> String { + format!("{}/{}", config.root, self.rel_location) + } + + pub fn append_to_root(&self, config: &Config) -> Result<()> { let backup_index_root = format!("{}/index.json", config.root); let path = PathBuf::from(&backup_index_root); if path.exists() { @@ -107,27 +145,15 @@ impl Backup { let mut loc: Vec = serde_json::from_str(&content)?; let mut f = File::create(path)?; - loc.push(new_backup); + loc.push(self.clone()); f.write_all(&serde_json::to_vec(&loc)?)?; } else { + create_dir_all(&config.root).unwrap(); let mut f = File::create(backup_index_root)?; - f.write_all(&serde_json::to_vec(&vec![new_backup])?)?; + f.write_all(&serde_json::to_vec(&vec![self])?)?; }; Ok(()) } - - fn get_timestamp() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BackupLocation { - id: BackupId, - rel_location: String, } diff --git a/src/config.rs b/src/config.rs index 625118a..439c17c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,8 @@ pub struct Config { pub root: String, pub user: Vec, pub directories: Vec, - pub custom_directories: Map + pub custom_directories: Map, + pub device: String, } impl Default for Config { @@ -17,6 +18,9 @@ impl Default for Config { user: vec![], directories: vec![], custom_directories: Map::new(), + device: gethostname::gethostname() + .into_string() + .expect("invalid hostname string"), } } } diff --git a/src/error.rs b/src/error.rs index 6afa3d0..c43c1fc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,12 +17,13 @@ pub enum Error { #[error("Only exactly one user allowed in config")] MultiUser, - #[error("OsString couldn't be converted to string")] - InvalidOsString, - #[error("Requested backup not found")] BackupNotFound, + // Packages + #[error("Unknown Package Manger Output")] + UnknownOutput, + #[error("json: {source}")] SerdeJson { #[from] diff --git a/src/main.rs b/src/main.rs index d5ccb75..acb728f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,18 +16,23 @@ fn main() -> color_eyre::Result<()> { cfg.directories.push("~/.config/nvim".to_string()); cfg.directories.push("~/.config/hypr".to_string()); cfg.root = "./backup".to_string(); + // cfg.root = "./backup-test".to_string(); + // cfg.directories.push("u:/code/proj/fxbaup/backup-test-dir".to_string()); let pacman = Pacman; - let pkgs = pacman.get_installed(); + let pkgs = pacman.get_installed()?; let backup = Backup::create(&cfg, pkgs)?; - println!("{backup:#?}"); + // println!("{backup:#?}"); - // backup.save(&cfg)?; + backup.save(&cfg)?; - let index = Backup::get_index(&cfg, None)?; + // PathInfo::compare_to_last_modified(&cfg, &LocationRoot::User("fx".to_string()), "code/proj/fxbaub/backup-test-dir/size.txt")?; + // PathInfo::compare_to_last_modified(&cfg, &LocationRoot::User("fx".to_string()), "code/proj/fxbaub/backup-test-dir/content.txt")?; - println!("{index:#?}"); + // let index = Backup::get_index(&cfg, None)?; + + // println!("{index:#?}"); // let fi = FileInfo::new("~/.config/nvim", &cfg)?; // println!("{:?}", fi.get_absolute_path()); diff --git a/src/packages.rs b/src/packages.rs index 9f765d6..7ac1736 100644 --- a/src/packages.rs +++ b/src/packages.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; +use crate::error::Result; + pub mod pacman; +pub mod portage; #[derive(Debug, Serialize, Deserialize)] pub struct Package { @@ -10,7 +13,7 @@ pub struct Package { } pub trait PackageManager { - fn get_installed(&self) -> Vec; + fn get_installed(&self) -> Result>; fn install(&self, pkgs: Vec); } diff --git a/src/packages/pacman.rs b/src/packages/pacman.rs index 0a9e1ff..b5be4c0 100644 --- a/src/packages/pacman.rs +++ b/src/packages/pacman.rs @@ -1,13 +1,13 @@ use std::process::Command; -use crate::packages::Package; +use super::{Package, PackageManager}; -use super::PackageManager; +use crate::error::{Error, Result}; pub struct Pacman; impl PackageManager for Pacman { - fn get_installed(&self) -> Vec { + fn get_installed(&self) -> Result> { let pm_pkgs = Command::new("pacman").args(["-Q"]).output().unwrap(); let pm_e_pkgs = Command::new("pacman") .args(["-Q", "--explicit"]) @@ -25,7 +25,7 @@ impl PackageManager for Pacman { }; let split: Vec<&str> = pkg.split_whitespace().collect(); if split.len() != 2 { - panic!("Unknown Pacman Output"); + return Err(Error::UnknownOutput); }; let explicit = pm_e_pkgs_out.contains(pkg); @@ -33,14 +33,14 @@ impl PackageManager for Pacman { pkgs.push(Package { id: split[0].to_string(), version: split[1].to_string(), - explicit + explicit, }) } - pkgs + Ok(pkgs) } - fn install(&self, pkgs: Vec) { + fn install(&self, _pkgs: Vec) { todo!(); } } diff --git a/src/packages/portage.rs b/src/packages/portage.rs new file mode 100644 index 0000000..6b9e508 --- /dev/null +++ b/src/packages/portage.rs @@ -0,0 +1,13 @@ +use super::PackageManager; + +pub struct Portage; + +impl PackageManager for Portage { + fn get_installed(&self) -> crate::error::Result> { + todo!() + } + + fn install(&self, pkgs: Vec) { + todo!() + } +} diff --git a/src/pathinfo.rs b/src/pathinfo.rs index be43b6e..641e7ef 100644 --- a/src/pathinfo.rs +++ b/src/pathinfo.rs @@ -1,31 +1,32 @@ use std::{ fmt::Display, - path::PathBuf, + fs::{create_dir_all, File}, + io::Read, + path::{Path, PathBuf}, }; use serde::{Deserialize, Serialize}; use crate::{ - backup::BackupId, + backup::{Backup, BackupId}, config::Config, error::{Error, Result}, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PathInfo { - pub modified: bool, pub is_file: bool, rel_location: String, location_root: LocationRoot, - last_modified: BackupId, - children: Vec + last_modified: Option, + pub children: Vec, } impl PathInfo { pub fn from_path(config: &Config, path: &str) -> Result { let locations = Self::parse_location(path, config)?; - Ok(Self::handle_dir(config, &locations.0, &locations.1)?) + Self::handle_dir(config, &locations.0, &locations.1) } fn handle_dir( @@ -36,54 +37,129 @@ impl PathInfo { println!("Handling {rel_location}"); let path = Self::get_abs_path(&location_root.to_string(), rel_location); Ok(if path.is_dir() { - let mut modified = false; + let mut last_modified = None; 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.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 handle.modified { - modified = true; + if handle.last_modified.is_some() { + // FIX: Check if new last modified is newer than old one + last_modified = handle.last_modified.clone(); }; children.push(handle); } Self { - modified, is_file: false, rel_location: rel_location.to_string(), location_root: location_root.clone(), - last_modified: "".to_string(), - children + last_modified, + children, } } else { - Self::from_file(rel_location, location_root.clone())? + Self::from_file(config, rel_location, location_root)? }) } - fn from_file(rel_location: &str, location_root: LocationRoot) -> Result { - println!("From file {rel_location}"); + 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)?; - let modified = false; + println!("From file {rel_location} ({:?})", last_modified); Ok(Self { rel_location: rel_location.to_string(), - location_root, - modified, - last_modified: "".to_string(), + location_root: location_root.clone(), + last_modified, is_file: true, - children: Vec::new() + 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 = files.iter().find(|file| file.rel_location == rel_location && file.location_root == *location_root); + + let Some(last_file) = last_file_opt else { + // File didn't exist last Backup + println!("File didn't exist last Backup"); + return Ok(None); + }; + + let modified_backup = if let Some(modified_backup_id) = last_file.last_modified.clone() { + 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 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(()); + } + println!("Save File {:?}", self.rel_location); + if !self.is_file { + for child in &self.children { + child.save(backup_root)?; + } + } else { + let new_path = format!("{}/{}", backup_root, self.rel_location); + // println!("New Path: {new_path}"); + // println!("Old Path: {:?}", self.get_absolute_path()); + 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)?; + }; + + Ok(()) + } + fn get_abs_path(location_root: &str, rel_location: &str) -> PathBuf { let path = format!("{}/{}", location_root, rel_location); PathBuf::from(path) @@ -150,97 +226,174 @@ impl LocationRoot { } } -// #[cfg(test)] -// mod tests { -// use crate::{ -// config::Config, -// error::{Error, Result}, -// pathinfo::PathInfo, -// }; -// -// use super::LocationRoot; -// -// #[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: Vec<(&str, Result)> = Vec::new(); -// values.push(("u:test", Ok(LocationRoot::User("test".to_string())))); -// values.push(("s:", Ok(LocationRoot::SystemSettings))); -// values.push(("r:", Ok(LocationRoot::Root))); -// values.push(( -// "c:test", -// Ok(LocationRoot::Custom("/usr/local/test".to_string())), -// )); -// values.push(("c:rest", Err(Error::CustomDirectory("rest".to_string())))); -// values.push(("t:test/", Err(Error::InvalidIndex("t".to_string())))); -// values.push(( -// "test:test/usr", -// Err(Error::InvalidIndex("test".to_string())), -// )); -// values.push(("/usr/local/test", Err(Error::NoIndex))); -// values.push(("c/usr/local/test", Err(Error::NoIndex))); -// -// for value in values { -// print!("Testing {value:?}"); -// assert_eq!(LocationRoot::from_op_str(value.0, &config), value.1); -// println!("\rTesting {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: Vec<(&str, Result<(String, LocationRoot)>)> = Vec::new(); -// values.push(( -// "~/.config/nvim", -// Ok(( -// ".config/nvim".to_string(), -// LocationRoot::User("test".to_string()), -// )), -// )); -// values.push(( -// "u:test/.config/nvim", -// Ok(( -// ".config/nvim".to_string(), -// LocationRoot::User("test".to_string()), -// )), -// )); -// values.push(( -// "r:/.config/nvim", -// Ok((".config/nvim".to_string(), LocationRoot::Root)), -// )); -// values.push(( -// "r:/.config/nvim", -// Ok((".config/nvim".to_string(), LocationRoot::Root)), -// )); -// values.push(( -// "s:/.config/nvim", -// Ok((".config/nvim".to_string(), LocationRoot::SystemSettings)), -// )); -// values.push(( -// "c:test/.config/nvim", -// Ok(( -// ".config/nvim".to_string(), -// LocationRoot::Custom("/usr/local/test".to_string()), -// )), -// )); -// -// for value in values { -// print!("Testing {value:?}"); -// assert_eq!(PathInfo::parse_location(&value.0, &config), value.1); -// println!("\rTesting {value:?} ✓"); -// } -// Ok(()) -// } -// } +#[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/fxbaup/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/fxbaup/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/fxbaup/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(()) + } +} -- cgit v1.2.3