diff --git a/Cargo.toml b/Cargo.toml index b72dfce..40fa9fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ name = "libpacstall" figment = { version = "0.10.6", features = ["env", "test", "toml" ] } num_cpus = "1.13.1" serde = { version = "1.0.144", features = ["derive"] } +chrono = { version = "0.4.22", features = ["serde"] } +serde_derive = "1.0.144" +serde_json = "1.0.85" [dev-dependencies] rstest = "0.15.0" diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..4950e78 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,5 @@ +mod pacbuild; +mod repository; + +pub use crate::model::pacbuild::*; +pub use crate::model::repository::Repository; diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs new file mode 100644 index 0000000..268c029 --- /dev/null +++ b/src/model/pacbuild.rs @@ -0,0 +1,51 @@ +use chrono::NaiveDateTime as DateTime; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PacBuild { + pub name: PackageId, + pub last_updated: DateTime, + pub repository: URL, + pub maintainer: String, + pub package_name: String, + pub description: String, + pub homepage: URL, + pub repology_version: Version, + pub repology: URL, + pub install_state: InstallState, + pub dependencies: Vec, + pub optional_dependencies: Vec, + pub license: String, + pub url: URL, + pub kind: Kind, +} + +pub type Version = String; +pub type PackageId = String; +pub type URL = String; +pub type Hash = String; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum InstallState { + Direct(DateTime, Version), + Indirect(DateTime, Version), + None, +} + +impl InstallState { + pub fn is_installed(&self) -> bool { + match self { + Self::None => false, + _ => true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Kind { + AppImage(Hash), + Binary(Hash), + DebFile(Hash), + GitBranch, + GitRelease, +} diff --git a/src/model/repository.rs b/src/model/repository.rs new file mode 100644 index 0000000..077a4d3 --- /dev/null +++ b/src/model/repository.rs @@ -0,0 +1,13 @@ +use chrono::NaiveDateTime as DateTime; +use serde_derive::{Deserialize, Serialize}; + +use crate::model::pacbuild::PacBuild; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repository { + pub name: String, + pub last_updated: DateTime, + pub url: String, + pub pacbuilds: Vec, + pub priority: u8, +} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..261d3e0 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,9 @@ +use self::storable::Storable; + +mod error; +mod filesystem; +pub mod filters; +pub mod storable; + +pub use error::StoreError; +pub use filesystem::FileSystemStore; diff --git a/src/store/error.rs b/src/store/error.rs new file mode 100644 index 0000000..f395f04 --- /dev/null +++ b/src/store/error.rs @@ -0,0 +1,24 @@ +#[derive(Clone)] +pub struct StoreError { + pub message: String, +} + +impl StoreError { + pub fn new(message: &str) -> StoreError { + StoreError { + message: message.to_string(), + } + } +} + +impl std::fmt::Display for StoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Store error: {}", self.message) + } +} + +impl std::fmt::Debug for StoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}:{}] Store error: {}", file!(), line!(), self.message) + } +} diff --git a/src/store/filesystem.rs b/src/store/filesystem.rs new file mode 100644 index 0000000..12884d6 --- /dev/null +++ b/src/store/filesystem.rs @@ -0,0 +1,303 @@ +use std::collections::HashMap; + +use super::StoreError; +use crate::model::{PacBuild, Repository}; +use crate::store::filters::{InstallState, Kind}; +use crate::store::storable::{Storable, UnitStoreResult}; +pub struct FileSystemStore { + repositories: Vec, + packages: HashMap>, + + allow_data_save: bool, +} + +impl FileSystemStore { + pub fn new() -> Box { + Box::new(FileSystemStore { + repositories: vec![], + packages: HashMap::new(), + allow_data_save: true, + }) + } + + fn get_packages_by_repository( + &self, + repository_url: &str, + ) -> Result<&Vec, StoreError> { + self.packages.get(&repository_url.to_owned()).map_or_else( + || { + Err(StoreError::new( + format!("Repository \"{}\" does not exist.", repository_url).as_str(), + )) + }, + |it| Ok(it), + ) + } + + fn save_to_disk(&self) { + if self.allow_data_save { + todo!() + } + } +} + +impl Storable for FileSystemStore { + fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { + self.packages + .iter() + .filter(|(repo_url, _)| (*repo_url).to_owned() == url.to_owned()) + .flat_map(|(_, pkgs)| pkgs) + .find(|p| p.name == name.to_owned()) + } + + fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { + self.repositories + .iter() + .find(|repo| repo.name == name.to_owned()) + } + + fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { + self.repositories + .iter() + .find(|repo| repo.url == url.to_owned()) + } + + fn get_all_pacbuilds_by( + &self, + name_like: Option<&str>, + install_state: Option, + kind: Option, + repository_url: Option<&str>, + ) -> Vec<&PacBuild> { + let repos_urls = if let Some(url) = repository_url { + self.repositories + .iter() + .find(|it| it.url == url.to_string()) + .map_or_else(|| vec![], |it| vec![it.url.to_owned()]) + } else { + self.repositories + .iter() + .map(|it| it.url.to_owned()) + .collect() + }; + + self.packages + .iter() + .filter(|(repo_url, _)| repos_urls.contains(repo_url)) + .flat_map(|(_, pkgs)| pkgs) + .filter(|it| { + if let Some(kind_filter) = &kind { + kind_filter.to_owned() == Kind::from_model_kind(it.kind.clone()) + } else { + false + } + }) + .filter(|it| { + if let Some(install_state_filter) = &install_state { + install_state_filter.to_owned() + == InstallState::from_model_install_state(it.install_state.clone()) + } else { + false + } + }) + .filter(|it| { + if let Some(name_like) = name_like { + it.name.contains(name_like) + } else { + false + } + }) + .collect() + } + + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { + let new_list = self + .get_packages_by_repository(repository_url)? + .iter() + .filter(|it| it.name != name.to_owned()) + .map(|it| it.clone()) + .collect::>(); + + self.packages.insert(repository_url.to_owned(), new_list); + + self.save_to_disk(); + Ok(()) + } + + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { + let mut new_list = self.get_packages_by_repository(repository_url)?.to_owned(); + + new_list.push(pacbuild.clone()); + self.packages.insert(repository_url.to_owned(), new_list); + + self.save_to_disk(); + + Ok(()) + } + + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { + let new_list = self + .get_packages_by_repository(repository_url)? + .iter() + .map(|it| { + if it.name == pacbuild.name.to_owned() { + pacbuild.clone() + } else { + it.clone() + } + }) + .collect(); + + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn remove_all_pacbuilds( + &mut self, + names: Vec<&str>, + repository_url: &str, + ) -> Result<(), StoreError> { + let str_names: Vec = names.iter().map(|name| name.to_string()).collect(); + + let new_list: Vec = self + .get_packages_by_repository(repository_url)? + .to_owned() + .into_iter() + .filter(|it| str_names.contains(&it.name)) + .collect(); + + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn add_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult { + let mut new_list: Vec = + self.get_packages_by_repository(repository_url)?.to_owned(); + + let already_existing_pkgs: Vec<&PacBuild> = pacbuilds + .iter() + .filter(|it| { + self.get_pacbuild_by_name_and_url(it.name.as_str(), &repository_url) + .is_some() + }) + .collect(); + + if !already_existing_pkgs.is_empty() { + return Err(StoreError::new( + format!( + "The following PACBUILDs already exist: {:#?}", + already_existing_pkgs + ) + .as_str(), + )); + } + + let mut to_add = pacbuilds.to_owned(); + new_list.append(&mut to_add); + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn update_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult { + self.allow_data_save = false; + let errors: Vec = pacbuilds + .iter() + .map(|it| self.update_pacbuild(it.to_owned(), repository_url)) + .filter(|it| it.is_err()) + .collect(); + + self.allow_data_save = true; + + if errors.is_empty() { + self.save_to_disk(); + Ok(()) + } else { + let e = errors.first().unwrap().clone().expect_err("unreachable"); + Err(StoreError::new(e.message.as_str())) + } + } + + fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { + let repo_exists = self + .repositories + .iter() + .any(|it| it.url.as_str() == repository_url); + + if !repo_exists { + return Err(StoreError::new( + format!("Repository {} does not exist.", repository_url).as_str(), + )); + } + + self.repositories = self + .repositories + .iter() + .filter(|repo| repo.url != repository_url) + .map(|it| it.to_owned()) + .collect(); + + self.packages.remove(&repository_url.to_owned()); + self.save_to_disk(); + + Ok(()) + } + + fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { + let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); + + if repo_exists { + return Err(StoreError::new( + format!("Repository {} already exists.", repository.url).as_str(), + )); + } + + self.packages.insert(repository.url.clone(), Vec::new()); + self.repositories.push(repository); + self.save_to_disk(); + + Ok(()) + } + + fn update_repository(&mut self, repository: Repository) -> UnitStoreResult { + let repo_exists = self + .repositories + .iter() + .any(|it| it.url == repository.url.to_owned()); + + if !repo_exists { + return Err(StoreError::new( + format!("Repository {} does not exist.", repository.url).as_str(), + )); + } + + self.repositories = self + .repositories + .iter() + .map(|it| { + if it.url == repository.url.to_owned() { + repository.to_owned() + } else { + it.to_owned() + } + }) + .collect(); + + self.save_to_disk(); + + Ok(()) + } +} diff --git a/src/store/filters.rs b/src/store/filters.rs new file mode 100644 index 0000000..9b0c6ef --- /dev/null +++ b/src/store/filters.rs @@ -0,0 +1,37 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum InstallState { + Direct, + Indirect, + None, +} + +impl InstallState { + pub fn from_model_install_state(other: crate::model::InstallState) -> InstallState { + match other { + crate::model::InstallState::Indirect(..) => InstallState::Indirect, + crate::model::InstallState::Direct(..) => InstallState::Direct, + crate::model::InstallState::None => InstallState::None, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Kind { + AppImage, + Binary, + DebFile, + GitBranch, + GitRelease, +} + +impl Kind { + pub fn from_model_kind(other: crate::model::Kind) -> Kind { + match other { + crate::model::Kind::GitRelease => Kind::GitRelease, + crate::model::Kind::GitBranch => Kind::GitBranch, + crate::model::Kind::AppImage(_) => Kind::AppImage, + crate::model::Kind::Binary(_) => Kind::Binary, + crate::model::Kind::DebFile(_) => Kind::DebFile, + } + } +} diff --git a/src/store/storable.rs b/src/store/storable.rs new file mode 100644 index 0000000..1e7d879 --- /dev/null +++ b/src/store/storable.rs @@ -0,0 +1,145 @@ +use crate::model::{PacBuild, Repository}; +use crate::store::filters::{InstallState, Kind}; +use crate::store::StoreError; + +pub type UnitStoreResult = Result<(), StoreError>; + +pub trait Storable { + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> UnitStoreResult; + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; + + fn remove_all_pacbuilds(&mut self, name: Vec<&str>, repository_url: &str) -> UnitStoreResult; + fn add_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult; + fn update_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult; + + fn remove_repository(&mut self, repository_url: &str) -> UnitStoreResult; + fn add_repository(&mut self, repository: Repository) -> UnitStoreResult; + fn update_repository(&mut self, repository: Repository) -> UnitStoreResult; + + fn get_pacbuild_by_name_and_url(&self, name: &str, repository_url: &str) -> Option<&PacBuild>; + fn get_repository_by_name(&self, name: &str) -> Option<&Repository>; + fn get_repository_by_url(&self, url: &str) -> Option<&Repository>; + + fn get_all_pacbuilds_by( + &self, + name_like: Option<&str>, + install_state: Option, + kind: Option, + repository_url: Option<&str>, + ) -> Vec<&PacBuild>; +} + +impl dyn Storable { + pub fn get_all_pacbuilds_by_name_like(&self, name_like: &str) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, None, None) + } + + pub fn get_all_pacbuilds_by_name_like_and_kind( + &self, + name_like: &str, + kind: Kind, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, Some(kind), None) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state( + &self, + name_like: &str, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, None) + } + + pub fn get_all_pacbuilds_by_name_like_and_repository_url( + &self, + name_like: &str, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, None, Some(url)) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind( + &self, + name_like: &str, + install_state: InstallState, + kind: Kind, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), None) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_repository_url( + &self, + name_like: &str, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, Some(url)) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind_and_repository_url( + &self, + name_like: &str, + install_state: InstallState, + kind: Kind, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_kind(&self, kind: Kind) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, Some(kind), None) + } + + pub fn get_all_pacbuilds_by_kind_and_install_state( + &self, + kind: Kind, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), None) + } + + pub fn get_all_pacbuilds_by_kind_and_repository_url( + &self, + kind: Kind, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_kind_and_install_state_and_repository_url( + &self, + kind: Kind, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_install_state( + &self, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), None, None) + } + + pub fn get_all_pacbuilds_by_install_state_and_repository_url( + &self, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), None, Some(url)) + } + + pub fn get_all_pacbuilds_by_repository_url(&self, url: &str) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, None, Some(url)) + } +}