From 553bbac36bdc483135a7053ca64507e01397e5e1 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Mon, 9 Sep 2024 23:03:49 +0200 Subject: add package manager recognition --- src/backup.rs | 17 +++++++--- src/cli.rs | 25 +++++++++++++++ src/config.rs | 30 ++++++++++++++--- src/error.rs | 12 +++++-- src/main.rs | 42 ++++++++++++++---------- src/packages.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++--- src/packages/pacman.rs | 13 +++++--- src/packages/portage.rs | 4 +-- src/pathinfo.rs | 41 ++++++++++++------------ 9 files changed, 208 insertions(+), 61 deletions(-) create mode 100644 src/cli.rs (limited to 'src') diff --git a/src/backup.rs b/src/backup.rs index e463593..f9de139 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use crate::{ config::Config, error::{Error, Result}, - packages::Package, + packages::{Manager, PackageList}, pathinfo::PathInfo, }; @@ -22,13 +22,13 @@ pub type Id = String; pub struct Backup { pub id: String, pub timestamp: u64, - packages: Vec, + pub packages: PackageList, pub files: Vec, - device: String, + pub device: String, } impl Backup { - pub fn create(config: &Config, packages: Vec) -> Result { + pub fn create(config: &Config, manager: Option) -> Result { let mut files: Vec = Vec::new(); for dir in &config.directories { files.push(PathInfo::from_path(config, dir)?); @@ -37,7 +37,7 @@ impl Backup { // TODO: UUID not really needed, maybe a shorter hash id: Uuid::new_v4().to_string(), timestamp: Self::get_timestamp(), - packages, + packages: Manager::get_manager(manager)?.get_installed()?, files, device: config.device.clone(), }) @@ -62,6 +62,7 @@ impl Backup { pub fn get_last(config: &Config) -> Result> { let backup_index_root = format!("{}/index.json", config.root); + info!(?backup_index_root, "backup index location:"); let list: Vec = match Self::get_json_content(&backup_index_root) { Ok(list) => list, Err(err) => { @@ -72,6 +73,8 @@ impl Backup { } }; + info!(?list, "backup index:"); + Ok(Some(Self::from_index( config, &list.last().ok_or(Error::BackupNotFound)?.id, @@ -109,6 +112,10 @@ impl Backup { format!("{loc}/{rel_location}") } + pub fn restore(&self) { + todo!() + } + fn get_json_content Deserialize<'a>>(path: &str) -> Result { let mut file = File::open(path)?; let mut content = String::new(); diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..6ffe03f --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +use crate::packages::Manager; + +#[derive(Parser)] +pub struct Cli { + #[arg(short, long)] + pub config: Option, + + #[command(subcommand)] + pub subcommand: Subcommands, +} + +#[derive(Subcommand)] +pub enum Subcommands { + GenerateConfig, + Save { + #[arg(short, long)] + package_manager: Option, + }, + Restore, +} + diff --git a/src/config.rs b/src/config.rs index 13dd0e4..46d2204 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,5 @@ +use std::{fs::create_dir_all, io::Write, path::PathBuf}; + use config::{File, Map}; use serde::{Deserialize, Serialize}; use tracing::{debug, trace}; @@ -6,7 +8,6 @@ use tracing::{debug, trace}; #[serde(default)] pub struct Config { pub root: String, - pub user: Vec, pub directories: Vec, pub custom_directories: Map, pub device: String, @@ -16,7 +17,6 @@ impl Default for Config { fn default() -> Self { Self { root: "/mnt/backup".to_string(), - user: vec![], directories: vec![], custom_directories: Map::new(), device: gethostname::gethostname() @@ -27,10 +27,16 @@ impl Default for Config { } impl Config { - pub fn load() -> Result { + pub fn load(path: Option) -> core::result::Result { debug!("load config"); + let source = if let Some(source) = path { + source + } else { + Self::get_location() + }; + let config = config::Config::builder() - .add_source(File::with_name("config.toml").required(false)) + .add_source(File::with_name(&source.to_string_lossy()).required(false)) .add_source(config::Environment::with_prefix("FXBAUP").separator("_")) .build()?; @@ -39,4 +45,20 @@ impl Config { cfg } + + pub fn generate() -> crate::error::Result<()> { + let loc = Self::get_location(); + create_dir_all(loc.parent().unwrap())?; + let mut f = std::fs::File::create(loc)?; + f.write_all(toml::to_string(&Self::default())?.as_bytes())?; + + Ok(()) + } + + fn get_location() -> PathBuf { + let mut conf_dir = dirs::config_local_dir().unwrap(); + conf_dir.push("arbs"); + conf_dir.push("config.toml"); + conf_dir + } } diff --git a/src/error.rs b/src/error.rs index 0cf4dca..e24c3b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,9 +14,6 @@ pub enum Error { #[error("invalid directory '{0}'")] InvalidDirectory(String), - #[error("Only exactly one user allowed in config")] - MultiUser, - #[error("Requested backup not found")] BackupNotFound, @@ -24,12 +21,21 @@ pub enum Error { #[error("Unknown Package Manger Output")] UnknownOutput, + #[error("Unsupported os/distro")] + Unsupported, + #[error("json: {source}")] SerdeJson { #[from] source: serde_json::Error, }, + #[error("toml serializer: {source}")] + TomlSerialize { + #[from] + source: toml::ser::Error, + }, + #[error("io: {source}")] Io { #[from] diff --git a/src/main.rs b/src/main.rs index 1284e0c..7393af9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ use backup::Backup; +use clap::Parser; +use cli::Subcommands; use config::Config; -use packages::{pacman::Pacman, PackageManager}; -use tracing::{debug, info, level_filters::LevelFilter}; +use error::Error; +use tracing::{debug, level_filters::LevelFilter}; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; mod backup; +mod cli; mod config; mod error; mod packages; @@ -13,7 +16,7 @@ mod pathinfo; fn main() -> color_eyre::Result<()> { color_eyre::install()?; - let file_appender = tracing_appender::rolling::never("./", "arps.log"); + let file_appender = tracing_appender::rolling::never("./", "arbs.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::registry() @@ -33,19 +36,24 @@ fn main() -> color_eyre::Result<()> { .init(); debug!("logging initialized"); - let mut cfg = Config::load()?; - cfg.user.push("fx".to_string()); - cfg.directories.push("~/.config/nvim".to_string()); - cfg.root = "./backup".to_string(); - - let pacman = Pacman; - let pkgs = pacman.get_installed()?; - let backup = Backup::create(&cfg, pkgs); - // info!(?backup); - // pacman.install(vec![Package { - // id: "lapce".to_string(), - // version: "0.4.2-1".to_string(), - // explicit: true, - // }])?; + let cli = cli::Cli::parse(); + + let config = Config::load(cli.config)?; + + match cli.subcommand { + Subcommands::GenerateConfig => Config::generate()?, + Subcommands::Save { package_manager } => { + let backup = Backup::create(&config, package_manager)?; + backup.save(&config)?; + } + Subcommands::Restore => { + let Some(last_backup) = Backup::get_last(&config)? else { + return Err(Error::BackupNotFound)?; + }; + + last_backup.packages.install()?; + last_backup.restore(); + } + }; Ok(()) } diff --git a/src/packages.rs b/src/packages.rs index 5ee5664..2eadcfc 100644 --- a/src/packages.rs +++ b/src/packages.rs @@ -1,19 +1,96 @@ +use std::{fs::File, io::Read}; + +use pacman::Pacman; +use portage::Portage; use serde::{Deserialize, Serialize}; -use crate::error::Result; +use crate::error::{Error, Result}; -pub mod pacman; -pub mod portage; +#[cfg(feature = "pacman")] +mod pacman; +#[cfg(feature = "portage")] +mod portage; #[derive(Debug, Serialize, Deserialize)] +pub struct PackageList { + packages: Vec, + manager: Manager, +} + +impl PackageList { + pub fn install(&self) -> Result<()> { + self.manager.to_package_manager().install(self.packages.clone()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Package { pub id: String, pub version: String, pub explicit: bool, } +#[derive(Debug, Clone, clap::ValueEnum, Serialize, Deserialize)] +pub enum Manager { + #[cfg(feature = "pacman")] + Pacman, + #[cfg(feature = "portage")] + Portage, +} + + +impl Manager { + pub fn get_manager(manager: Option) -> Result> { + #[cfg(not(target_os = "linux"))] + { + return Err(Error::Unsupported); + } + + #[cfg(target_os = "linux")] + { + if let Some(man) = manager { + return Ok(man.to_package_manager()); + } + let mut os_release = File::open("/etc/os-release")?; + let mut content = String::new(); + os_release.read_to_string(&mut content)?; + + let lines: Vec<&str> = content.split('\n').collect(); + for line in lines { + let Some((key, value)) = line.split_once('=') else { + continue; + }; + if key == "ID" { + return Self::from_str(value); + } + } + Err(Error::Unsupported) + } + } + + fn from_str(value: &str) -> Result> { + Ok(match value { + #[cfg(feature = "pacman")] + "arch" => Box::new(Pacman), + #[cfg(feature = "portage")] + "gentoo" => Box::new(Portage), + _ => return Err(Error::Unsupported), + }) + } + + fn to_package_manager(&self) -> Box { + match self { + #[cfg(feature = "pacman")] + Self::Pacman => Box::new(Pacman), + #[cfg(feature = "portage")] + Self::Portage => Box::new(Portage), + } + } +} + + pub trait PackageManager { - fn get_installed(&self) -> Result>; + fn get_installed(&self) -> Result; fn install(&self, pkgs: Vec) -> Result<()>; } diff --git a/src/packages/pacman.rs b/src/packages/pacman.rs index e10c6fb..0ad463b 100644 --- a/src/packages/pacman.rs +++ b/src/packages/pacman.rs @@ -1,13 +1,13 @@ use std::process::{Command, Stdio}; -use super::{Package, PackageManager}; +use super::{Package, PackageList, PackageManager}; use crate::error::{Error, Result}; pub struct Pacman; impl PackageManager for Pacman { - fn get_installed(&self) -> Result> { + 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"]) @@ -37,16 +37,19 @@ impl PackageManager for Pacman { }); } - Ok(pkgs) + Ok(PackageList { + packages: pkgs, + manager: super::Manager::Pacman, + }) } fn install(&self, pkgs: Vec) -> Result<()> { - let mut args = vec!["--noconfirm".to_string(), "-S".to_string()]; + let mut args = vec!["pacman".to_string(), "--noconfirm".to_string(), "-S".to_string()]; for pkg in pkgs { args.push(pkg.id); } - Command::new("pacman") + Command::new("doas") .stdout(Stdio::inherit()) .args(args) .spawn()? diff --git a/src/packages/portage.rs b/src/packages/portage.rs index f9a760b..7fa09a8 100644 --- a/src/packages/portage.rs +++ b/src/packages/portage.rs @@ -1,11 +1,11 @@ use tracing::error; -use super::PackageManager; +use super::{PackageList, PackageManager}; pub struct Portage; impl PackageManager for Portage { - fn get_installed(&self) -> crate::error::Result> { + fn get_installed(&self) -> crate::error::Result { todo!() } diff --git a/src/pathinfo.rs b/src/pathinfo.rs index 8b1ca2f..03b8a6b 100644 --- a/src/pathinfo.rs +++ b/src/pathinfo.rs @@ -120,7 +120,8 @@ impl PathInfo { 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 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(); @@ -195,12 +196,9 @@ impl PathInfo { 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()), + LocationRoot::User, )); }; Ok(( @@ -212,18 +210,22 @@ impl PathInfo { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LocationRoot { - User(String), + User, Custom(String), - SystemSettings, + SystemConfig, + UserConfig, 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::User => write!(f, "{}", dirs::home_dir().unwrap().to_string_lossy()), LocationRoot::Custom(loc) => write!(f, "{loc}"), - LocationRoot::SystemSettings => write!(f, "/etc"), + LocationRoot::SystemConfig => write!(f, "/etc"), + LocationRoot::UserConfig => { + write!(f, "{}", dirs::config_local_dir().unwrap().to_string_lossy()) + } LocationRoot::Root => write!(f, "/"), } } @@ -236,8 +238,9 @@ impl LocationRoot { return Err(Error::NoIndex); }; match split_op.0 { - "u" => Ok(Self::User(split_op.1.to_string())), - "s" => Ok(Self::SystemSettings), + "u" => Ok(Self::User), + "s" => Ok(Self::SystemConfig), + "d" => Ok(Self::UserConfig), "r" => Ok(Self::Root), "c" => Ok(Self::Custom( config @@ -262,7 +265,6 @@ mod tests { backup::Backup, config::Config, error::{Error, Result}, - packages::{pacman::Pacman, PackageManager}, }; use super::LocationRoot; @@ -276,8 +278,8 @@ mod tests { .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(("u:test", LocationRoot::User)); + values_ok.push(("s:", LocationRoot::SystemConfig)); values_ok.push(("r:", LocationRoot::Root)); values_ok.push(( "c:test", @@ -321,7 +323,6 @@ mod tests { #[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()); @@ -331,14 +332,14 @@ mod tests { "~/.config/nvim", ( ".config/nvim".to_string(), - LocationRoot::User("test".to_string()), + LocationRoot::User, ), )); values_ok.push(( "u:test/.config/nvim", ( ".config/nvim".to_string(), - LocationRoot::User("test".to_string()), + LocationRoot::User, ), )); values_ok.push(( @@ -351,7 +352,7 @@ mod tests { )); values_ok.push(( "s:/.config/nvim", - (".config/nvim".to_string(), LocationRoot::SystemSettings), + (".config/nvim".to_string(), LocationRoot::SystemConfig), )); values_ok.push(( "c:test/.config/nvim", @@ -385,9 +386,7 @@ mod tests { 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)?; + let backup = Backup::create(&config, None)?; backup.save(&config)?; let mut f = File::create("./backup-test-dir/size.txt")?; -- cgit v1.2.3