From 3e1cb020d5449849b37874f91cadfa4a9c878747 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Fri, 6 Sep 2024 10:56:30 +0200 Subject: initial commit, can save index, no modification check --- src/backup.rs | 44 +++++++++ src/config.rs | 33 +++++++ src/error.rs | 19 ++++ src/main.rs | 32 +++++++ src/packages.rs | 16 ++++ src/packages/pacman.rs | 46 +++++++++ src/pathinfo.rs | 246 +++++++++++++++++++++++++++++++++++++++++++++++++ src/storage.rs | 8 ++ 8 files changed, 444 insertions(+) create mode 100644 src/backup.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/packages.rs create mode 100644 src/packages/pacman.rs create mode 100644 src/pathinfo.rs create mode 100644 src/storage.rs (limited to 'src') diff --git a/src/backup.rs b/src/backup.rs new file mode 100644 index 0000000..4e74c97 --- /dev/null +++ b/src/backup.rs @@ -0,0 +1,44 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{config::Config, pathinfo::PathInfo, packages::Package, error::Result}; + +pub type BackupId = String; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Backup { + id: String, + timestamp: u64, + packages: Vec, + files: Vec, +} + +impl Backup { + pub fn create(config: &Config, packages: Vec) -> Result { + let mut files: Vec = Vec::new(); + for dir in &config.directories { + files.push(PathInfo::from_path(config, dir)?); + } + Ok(Self { + // UUID not really needed, maybe a shorter hash + id: Uuid::new_v4().to_string(), + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + packages, + files, + }) + } + + +} + +struct BackupLocation { + id: BackupId, + rel_location: String, +} + +type BackupList = Vec; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..625118a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,33 @@ +use config::{File, Map}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct Config { + pub root: String, + pub user: Vec, + pub directories: Vec, + pub custom_directories: Map +} + +impl Default for Config { + fn default() -> Self { + Self { + root: "/mnt/backup".to_string(), + user: vec![], + directories: vec![], + custom_directories: Map::new(), + } + } +} + +impl Config { + pub fn load() -> Result { + let config = config::Config::builder() + .add_source(File::with_name("config.toml").required(false)) + .add_source(config::Environment::with_prefix("FXBAUP").separator("_")) + .build()?; + + config.try_deserialize() + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..77eab69 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +pub type Result = std::result::Result; + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("unknown custom directory '{0}'")] + CustomDirectory(String), + + #[error("invalid directory index '{0}'")] + InvalidIndex(String), + + #[error("no directory index given")] + NoIndex, + + #[error("invalid directory '{0}'")] + InvalidDirectory(String), + + #[error("Only exactly one user allowed in config")] + MultiUser, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1fdcebf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +use backup::Backup; +use config::Config; +use packages::{pacman::Pacman, PackageManager}; +use storage::save_index; + +mod backup; +mod config; +mod error; +mod pathinfo; +mod packages; +mod storage; + +fn main() -> anyhow::Result<()> { + let mut cfg = Config::load()?; + cfg.user.push("fx".to_string()); + cfg.directories.push("~/.config/nvim".to_string()); + cfg.directories.push("~/.config/hypr".to_string()); + let toml = toml::to_string(&cfg)?; + println!("{toml}"); + + let pacman = Pacman; + let pkgs = pacman.get_installed(); + + let backup = Backup::create(&cfg, pkgs)?; + // println!("{backup:#?}"); + + save_index(backup); + + // let fi = FileInfo::new("~/.config/nvim", &cfg)?; + // println!("{:?}", fi.get_absolute_path()); + Ok(()) +} diff --git a/src/packages.rs b/src/packages.rs new file mode 100644 index 0000000..9f765d6 --- /dev/null +++ b/src/packages.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +pub mod pacman; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Package { + pub id: String, + pub version: String, + pub explicit: bool +} + +pub trait PackageManager { + fn get_installed(&self) -> Vec; + + fn install(&self, pkgs: Vec); +} diff --git a/src/packages/pacman.rs b/src/packages/pacman.rs new file mode 100644 index 0000000..0a9e1ff --- /dev/null +++ b/src/packages/pacman.rs @@ -0,0 +1,46 @@ +use std::process::Command; + +use crate::packages::Package; + +use super::PackageManager; + +pub struct Pacman; + +impl PackageManager for Pacman { + fn get_installed(&self) -> Vec { + let pm_pkgs = Command::new("pacman").args(["-Q"]).output().unwrap(); + let pm_e_pkgs = Command::new("pacman") + .args(["-Q", "--explicit"]) + .output() + .unwrap(); + + let pm_pkgs_out = String::from_utf8(pm_pkgs.stdout).unwrap(); + let pm_e_pkgs_out = String::from_utf8(pm_e_pkgs.stdout).unwrap(); + + let mut pkgs: Vec = Vec::new(); + let pacman_pkgs: Vec<&str> = pm_pkgs_out.split('\n').collect(); + for pkg in pacman_pkgs { + if pkg.is_empty() { + continue; + }; + let split: Vec<&str> = pkg.split_whitespace().collect(); + if split.len() != 2 { + panic!("Unknown Pacman Output"); + }; + + let explicit = pm_e_pkgs_out.contains(pkg); + + pkgs.push(Package { + id: split[0].to_string(), + version: split[1].to_string(), + explicit + }) + } + + pkgs + } + + fn install(&self, pkgs: Vec) { + todo!(); + } +} diff --git a/src/pathinfo.rs b/src/pathinfo.rs new file mode 100644 index 0000000..b0c3be4 --- /dev/null +++ b/src/pathinfo.rs @@ -0,0 +1,246 @@ +use std::{ + fmt::Display, + path::PathBuf, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + 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 +} + +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)?) + } + + fn handle_dir( + config: &Config, + rel_location: &str, + location_root: &LocationRoot, + ) -> Result { + 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 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 Some(rl) = pathstr.split_once(&root) else { + panic!("HUH"); + }; + let handle = Self::handle_dir(config, rl.1, location_root)?; + if handle.modified { + modified = true; + }; + children.push(handle); + } + Self { + modified, + is_file: false, + rel_location: rel_location.to_string(), + location_root: location_root.clone(), + last_modified: "".to_string(), + children + } + } else { + Self::from_file(rel_location, location_root.clone())? + }) + } + + fn from_file(rel_location: &str, location_root: LocationRoot) -> Result { + println!("From file {rel_location}"); + + let modified = false; + + Ok(Self { + rel_location: rel_location.to_string(), + location_root, + modified, + last_modified: "".to_string(), + is_file: true, + children: Vec::new() + }) + } + + pub fn get_absolute_path(&self) -> PathBuf { + Self::get_abs_path(&self.location_root.to_string(), &self.rel_location) + } + + 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 { + 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 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(()) + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..b9e8de9 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,8 @@ +use std::{fs::File, io::Write}; + +use crate::backup::Backup; + +pub fn save_index(backup: Backup) { + let mut f = File::create("./index.json").unwrap(); + f.write_all(&serde_json::to_vec(&backup).unwrap()).unwrap(); +} -- cgit v1.2.3