diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bcf098..131f6a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,7 @@ +ci: + # Skip hooks requiring `just` to be installed + skip: [codespell, fmt, clippy] + repos: # General - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..07ffd52 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +## Added + +- Configuration handler in [#58](https://github.com/pacstall/libpacstall/pull/58) by [\@wizard-28](https://github.com/wizard-28) diff --git a/Cargo.toml b/Cargo.toml index 40fa9fc..9855aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,16 @@ name = "libpacstall" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -name = "libpacstall" +authors = [ + "Sourajyoti Basak ", + "David Brochero ", + "Paul Cosma " +] +description = "Backend API library for Pacstall" +repository = "https://github.com/pacstall/libpacstall" +license = "GPL-3.0-or-later" +keywords = ["aur", "pacstall", "package-manager", "linux", "apt"] +categories = ["caching", "config", "parsing", "os::linux-apis"] [dependencies] figment = { version = "0.10.6", features = ["env", "test", "toml" ] } @@ -14,6 +20,7 @@ serde = { version = "1.0.144", features = ["derive"] } chrono = { version = "0.4.22", features = ["serde"] } serde_derive = "1.0.144" serde_json = "1.0.85" +thiserror = "1.0" [dev-dependencies] rstest = "0.15.0" diff --git a/README.md b/README.md index e9e9246..0a8371c 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,35 @@

LibPacstall

-

Backend API library for Pacstall

- - - Codecov - - - Codacy branch grade - -
+ + Made with Rust + Codecov +
- join discord - + join discord + - Mastodon Follow + Mastodon Follow - join matrix -
+ join matrix +

-## What is this +

The backend API library for Pacstall

+ +## 🔱 Info + +The backend API library for [Pacstall](https://github.com/pacstall/pacstall) written in Rust + +## 🌊 Features -LibPacstall is a backend API library for -[Pacstall](https://github.com/pacstall/pacstall). ++ Deserialize the Pacstall configuration from the TOML and environment variables -## Stats +## 📈 Stats

Repobeats analytics image

-## License +## 📜 License

GPL-3.0-or-later

diff --git a/src/config.rs b/src/config.rs index fc8f430..29f5ef9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,6 +35,8 @@ use figment::value::{Dict, Map}; use figment::{Error, Figment, Metadata, Profile, Provider}; use serde::{Deserialize, Serialize}; +use crate::model::{default_repository, Repository}; + /// Pacstall's configuration. /// /// Gives access to the [configuration](Config) extracted, and the [Figment] @@ -191,39 +193,6 @@ impl Default for Settings { } } -/// The extracted `repositories` array of tables. -/// -/// Defaults to the official repository. -#[derive(Deserialize, Debug, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct Repository { - /// The name of the repository. - pub name: String, - /// URL of the repository. - /// - /// Note that the URL **isn't verified** during extraction! - pub url: String, - /// Preference of the repository. - /// - /// Specifies which repository to look into first during certain operations - /// like installing a package. If the package isn't present in the first - /// preferred repository, then the second preferred repository is looked - /// into. - pub preference: u32, -} - -fn default_repository() -> Vec { vec![Repository::default()] } - -impl Default for Repository { - fn default() -> Self { - Self { - name: "official".into(), - url: "https://github.com/pacstall/pacstall-programs".into(), - preference: 1, - } - } -} - #[cfg(test)] mod tests { use std::fs::{self, File}; diff --git a/src/lib.rs b/src/lib.rs index 7b8a174..eabe0a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,5 @@ )] #![allow(clippy::must_use_candidate)] pub mod config; +pub mod model; +pub mod store; diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index 4950e78..0000000 --- a/src/model.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod pacbuild; -mod repository; - -pub use crate::model::pacbuild::*; -pub use crate::model::repository::Repository; diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..e212776 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,7 @@ +//! Provides structs to handle Pacstall's data models. + +mod pacbuild; +mod repository; + +pub use crate::model::pacbuild::*; +pub use crate::model::repository::{default_repository, Repository}; diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 268c029..0320f79 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -1,7 +1,8 @@ use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Representation of the PACBUILD file. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PacBuild { pub name: PackageId, pub last_updated: DateTime, @@ -20,32 +21,91 @@ pub struct PacBuild { pub kind: Kind, } +/// Represents a `SemVer` version. +/// # Examples +/// ``` +/// use libpacstall::model::Version; +/// +/// let ver: Version = "1.0.0".into(); +/// ``` pub type Version = String; + +/// Represents a `PacBuild` or Apt package name. +/// # Examples +/// ``` +/// use libpacstall::model::PackageId; +/// +/// let identifier: PackageId = "discord-deb".into(); +/// ``` pub type PackageId = String; +/// Represents an URL +/// # Examples +/// ``` +/// use libpacstall::model::URL; +/// +/// let url: URL = "https://example.com".into(); +/// ``` pub type URL = String; +/// Represents a file checksum +/// # Examples +/// ``` +/// use libpacstall::model::Hash; +/// +/// let hash: Hash = "b5c9710f33204498efb64cf8257cd9b19e9d3e6b".into(); +/// ``` pub type Hash = String; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Represents the install state of a package. +/// # Examples +/// ``` +/// use chrono::NaiveDate; +/// use libpacstall::model::InstallState; +/// +/// let installed_directly = InstallState::Direct( +/// NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11), +/// "0.9.2".into(), +/// ); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum InstallState { + /// Package is installed directly, meaning the user wanted it. Direct(DateTime, Version), + + /// Package is installed as a dependency. Indirect(DateTime, Version), + + /// Package is not installed. None, } impl InstallState { - pub fn is_installed(&self) -> bool { - match self { - Self::None => false, - _ => true, - } - } + /// Returns `true` if the package is installed otherwise `false`. + pub fn is_installed(&self) -> bool { !matches!(self, Self::None) } } -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Represents the type of the package. Usually deduced by the [PacBuild#name] +/// suffix. +/// +/// # Examples +/// ``` +/// use libpacstall::model::Kind; +/// +/// let git_release = Kind::GitRelease; +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Kind { + /// [PacBuild] will install an `AppImage`. AppImage(Hash), + + /// [PacBuild] will install a prebuilt, usually `tar.gz`, package. Binary(Hash), + + /// [PacBuild] will install an existing `.deb` file. DebFile(Hash), + + /// [PacBuild] will install the source of a given Git branch. GitBranch, + + /// [PacBuild] will install the source of a given Git release. GitRelease, } diff --git a/src/model/repository.rs b/src/model/repository.rs index 077a4d3..0395507 100644 --- a/src/model/repository.rs +++ b/src/model/repository.rs @@ -1,13 +1,35 @@ -use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; -use crate::model::pacbuild::PacBuild; - -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Representation of a Pacstall repository. +/// +/// Defaults to the official repository. +#[derive(Deserialize, Debug, Eq, PartialEq, Serialize, Clone)] +#[serde(deny_unknown_fields)] pub struct Repository { + /// The name of the repository. pub name: String, - pub last_updated: DateTime, + /// URL of the repository. + /// + /// Note that the URL **isn't verified** during extraction! pub url: String, - pub pacbuilds: Vec, - pub priority: u8, + /// Preference of the repository. + /// + /// Specifies which repository to look into first during certain operations + /// like installing a package. If the package isn't present in the first + /// preferred repository, then the second preferred repository is looked + /// into. + pub preference: u32, +} + +#[allow(clippy::module_name_repetitions)] +pub fn default_repository() -> Vec { vec![Repository::default()] } + +impl Default for Repository { + fn default() -> Self { + Self { + name: "official".into(), + url: "https://github.com/pacstall/pacstall-programs".into(), + preference: 1, + } + } } diff --git a/src/store.rs b/src/store.rs deleted file mode 100644 index 261d3e0..0000000 --- a/src/store.rs +++ /dev/null @@ -1,9 +0,0 @@ -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/storable.rs b/src/store/base.rs similarity index 66% rename from src/store/storable.rs rename to src/store/base.rs index 1e7d879..f56cb4b 100644 --- a/src/store/storable.rs +++ b/src/store/base.rs @@ -1,33 +1,85 @@ -use crate::model::{PacBuild, Repository}; -use crate::store::filters::{InstallState, Kind}; -use crate::store::StoreError; +//! Abstraction over the caching implementation -pub type UnitStoreResult = Result<(), StoreError>; +use std::fmt::Debug; -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; +use crate::model::{PacBuild, Repository}; +use crate::store::errors::StoreError; +use crate::store::filters::{InstallState, Kind}; - fn remove_all_pacbuilds(&mut self, name: Vec<&str>, repository_url: &str) -> UnitStoreResult; +/// Alias for store error results +pub type StoreResult = Result; + +/// Abstraction over the caching implementation +pub trait Base: Debug { + /// Removes `PacBuild` by name that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + /// * `StoreError::PacBuildNotFound` + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> StoreResult<()>; + + /// Adds `PacBuild` to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + /// * `StoreError::PacBuildConflict` + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; + + /// Updates `PacBuild` that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + /// * `StoreError::PacBuildNotFound` + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; + + /// Removes all `PacBuild` by name that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` + fn remove_all_pacbuilds(&mut self, name: &[&str], repository_url: &str) -> StoreResult<()>; + + /// Adds all `PacBuild` to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` fn add_all_pacbuilds( &mut self, pacbuilds: Vec, repository_url: &str, - ) -> UnitStoreResult; + ) -> StoreResult<()>; + + /// Updates all `PacBuild` that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` 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; + ) -> StoreResult<()>; + + /// Removes [Repository] by url. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + fn remove_repository(&mut self, repository_url: &str) -> StoreResult<()>; + + /// Adds `Repository`. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + fn add_repository(&mut self, repository: Repository) -> StoreResult<()>; + + /// Updates [Repository]. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + fn update_repository(&mut self, repository: Repository) -> StoreResult<()>; 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_repositories(&self) -> Vec<&Repository>; fn get_all_pacbuilds_by( &self, @@ -38,7 +90,11 @@ pub trait Storable { ) -> Vec<&PacBuild>; } -impl dyn Storable { +impl dyn Base { + pub fn get_all_pacbuilds(&self) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, None, None) + } + 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) } diff --git a/src/store/error.rs b/src/store/error.rs deleted file mode 100644 index f395f04..0000000 --- a/src/store/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[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/errors.rs b/src/store/errors.rs new file mode 100644 index 0000000..3fee39a --- /dev/null +++ b/src/store/errors.rs @@ -0,0 +1,25 @@ +//! Errors used by the caching system + +use thiserror::Error; + +/// Errors used by Base +#[derive(Debug, Clone, Error)] +pub enum StoreError { + #[error("repository '{0}' could not be found")] + RepositoryNotFound(String), + + #[error("pacbuild '{name:?}' could not be found in repository {repository:?}")] + PacBuildNotFound { name: String, repository: String }, + + #[error("repository '{0}' already exists")] + RepositoryConflict(String), + + #[error("pacbuild '{name:?}' already exists in repository {repository:?}")] + PacBuildConflict { name: String, repository: String }, + + #[error("unexpected error: {0}")] + Unexpected(String), + + #[error("multiple errors: {0:?}")] + Aggregate(Vec), +} diff --git a/src/store/filesystem.rs b/src/store/filesystem.rs deleted file mode 100644 index 12884d6..0000000 --- a/src/store/filesystem.rs +++ /dev/null @@ -1,303 +0,0 @@ -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 index 9b0c6ef..aefe474 100644 --- a/src/store/filters.rs +++ b/src/store/filters.rs @@ -1,4 +1,7 @@ -#[derive(Debug, PartialEq, Clone)] +//! Provides various structs for querying and filtering packages. + +/// Used to query packages by installation state +#[derive(Debug, PartialEq, Eq, Clone)] pub enum InstallState { Direct, Indirect, @@ -6,7 +9,7 @@ pub enum InstallState { } impl InstallState { - pub fn from_model_install_state(other: crate::model::InstallState) -> 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, @@ -15,7 +18,8 @@ impl InstallState { } } -#[derive(Debug, PartialEq, Clone)] +/// Used to query packages by kind. +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Kind { AppImage, Binary, @@ -25,7 +29,7 @@ pub enum Kind { } impl Kind { - pub fn from_model_kind(other: crate::model::Kind) -> 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, diff --git a/src/store/fs.rs b/src/store/fs.rs new file mode 100644 index 0000000..2769ff0 --- /dev/null +++ b/src/store/fs.rs @@ -0,0 +1,642 @@ +//! Provides a JSON file-based implementation for the caching system. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::errors::StoreError; +use crate::model::{PacBuild, Repository}; +use crate::store::base::{Base, StoreResult}; +use crate::store::filters::{InstallState, Kind}; + +#[cfg(not(test))] +const FSS_PATH: &str = "/etc/pacstall/fss.json"; +#[cfg(test)] +const FSS_PATH: &str = "./fss.json"; + +/// `FileSystem` implementation for the caching system +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileSystemStore { + repositories: Vec, + packages: HashMap>, + + #[serde(skip_serializing)] + allow_data_save: bool, +} + +impl FileSystemStore { + #[allow(clippy::new_ret_no_self)] + pub fn new() -> Box { + Box::new(FileSystemStore { + repositories: Vec::new(), + packages: HashMap::new(), + allow_data_save: true, + }) + } + + /// # Private + fn get_packages_by_repository( + &mut self, + repository_url: &str, + ) -> Result<&mut Vec, StoreError> { + self.packages + .get_mut(&repository_url.to_owned()) + .map_or_else( + || Err(StoreError::RepositoryNotFound(repository_url.to_owned())), + Ok, + ) + } + + /// # Private + fn save_to_disk(&self) -> StoreResult<()> { + if self.allow_data_save { + use std::fs; + use std::path::Path; + + let json = serde_json::to_vec_pretty(self).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to serialize database.".to_string(), + )) + }, + Ok, + )?; + + return fs::write(Path::new(FSS_PATH), &json).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to write database to disk.".to_string(), + )) + }, + |_| Ok(()), + ); + } + + Ok(()) + } + + /// # Errors + pub fn load_from_disk() -> Result, StoreError> { + use std::fs; + use std::path::Path; + + let contents = fs::read_to_string(Path::new(FSS_PATH)).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to read database from disk.".to_string(), + )) + }, + Ok, + )?; + let mut obj: FileSystemStore = serde_json::from_str(&contents).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to deserialize database.".to_string(), + )) + }, + Ok, + )?; + obj.allow_data_save = true; + + Ok(Box::new(obj)) + } +} + +impl Base for FileSystemStore { + fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { + self.packages + .get(&url.to_string()) + .and_then(|pkgs| pkgs.iter().find(|it| it.name == *name)) + } + + fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { + self.repositories.iter().find(|repo| repo.name == *name) + } + + fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { + self.repositories.iter().find(|repo| repo.url == *url) + } + + 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) + .map_or_else(Vec::new, |it| vec![it.url.clone()]) + } else { + self.repositories.iter().map(|it| it.url.clone()).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 == Kind::from_model_kind(&it.kind) + } else { + true + } + }) + .filter(|it| { + if let Some(install_state_filter) = &install_state { + *install_state_filter + == InstallState::from_model_install_state(&it.install_state) + } else { + true + } + }) + .filter(|it| { + if let Some(name_like) = name_like { + it.name.contains(name_like) + } else { + true + } + }) + .collect() + } + + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { + let repo = self + .packages + .get_mut(&repository_url.to_owned()) + .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; + + repo.swap_remove(repo.iter().position(|it| it.name == *name).ok_or( + StoreError::PacBuildNotFound { + name: name.to_string(), + repository: repository_url.to_string(), + }, + )?); + + self.save_to_disk()?; + Ok(()) + } + + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { + if self + .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) + .is_some() + { + return Err(StoreError::PacBuildConflict { + name: pacbuild.name, + repository: repository_url.to_string(), + }); + } + + self.get_packages_by_repository(repository_url)? + .push(pacbuild); + self.save_to_disk()?; + + Ok(()) + } + + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { + if self + .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) + .is_none() + { + return Err(StoreError::PacBuildNotFound { + name: pacbuild.name, + repository: repository_url.to_string(), + }); + } + + let pkgs = self.get_packages_by_repository(repository_url)?; + let idx = pkgs.iter().position(|pb| pb.name == pacbuild.name).unwrap(); + + pkgs.swap_remove(idx); + pkgs.push(pacbuild); + self.save_to_disk()?; + + Ok(()) + } + + fn remove_all_pacbuilds( + &mut self, + names: &[&str], + repository_url: &str, + ) -> Result<(), StoreError> { + let errors = names + .iter() + .map( + |it| match self.get_pacbuild_by_name_and_url(*it, repository_url) { + Some(_) => None, + None => Some(StoreError::PacBuildNotFound { + name: (*it).to_string(), + repository: repository_url.to_string(), + }), + }, + ) + .fold(vec![], |acc, it| { + if let Some(err) = it { + let mut acc = acc; + acc.push(err); + acc + } else { + acc + } + }); + + if !errors.is_empty() { + return Err(StoreError::Aggregate(errors)); + } + + let str_names: Vec = names.iter().map(ToString::to_string).collect(); + + let new_list: Vec = self + .get_packages_by_repository(repository_url)? + .clone() + .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, + ) -> StoreResult<()> { + 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::Aggregate( + already_existing_pkgs + .iter() + .map(|it| StoreError::PacBuildConflict { + name: it.name.to_string(), + repository: it.repository.to_string(), + }) + .collect(), + )); + } + + let mut new_list: Vec = self.get_packages_by_repository(repository_url)?.clone(); + let mut to_add = pacbuilds.clone(); + 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, + ) -> StoreResult<()> { + for name in pacbuilds.iter().map(|it| &it.name) { + if self + .get_pacbuild_by_name_and_url(name, repository_url) + .is_none() + { + return Err(StoreError::PacBuildNotFound { + name: name.to_string(), + repository: repository_url.to_string(), + }); + } + } + + self.allow_data_save = false; + let errors: Vec> = pacbuilds + .iter() + .map(|it| self.update_pacbuild(it.clone(), repository_url)) + .filter(Result::is_err) + .collect(); + + self.allow_data_save = true; + + if errors.is_empty() { + self.save_to_disk()?; + Ok(()) + } else { + Err(StoreError::Aggregate( + errors.iter().map(|it| it.clone().unwrap_err()).collect(), + )) + } + } + + fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { + let repo_idx = self + .repositories + .iter() + .position(|it| it.url.as_str() == repository_url) + .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; + + self.repositories.swap_remove(repo_idx); + 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::RepositoryConflict(repository.url)); + } + + self.packages.insert(repository.url.clone(), Vec::new()); + self.repositories.push(repository); + self.save_to_disk()?; + + Ok(()) + } + + fn update_repository(&mut self, repository: Repository) -> StoreResult<()> { + let repo_idx = self + .repositories + .iter() + .position(|it| it.url == repository.url) + .ok_or_else(|| StoreError::RepositoryNotFound(repository.url.to_string()))?; + + self.repositories.swap_remove(repo_idx); + self.repositories.push(repository); + + self.save_to_disk()?; + + Ok(()) + } + + fn get_all_repositories(&self) -> Vec<&Repository> { self.repositories.iter().collect() } +} + +#[cfg(test)] +mod test { + use super::FileSystemStore; + use crate::model::Repository; + + mod util { + use chrono::NaiveDateTime; + + use crate::model::{InstallState, Kind, PacBuild, Repository}; + use crate::store::base::Base; + use crate::store::fs::FileSystemStore; + + pub fn create_store_with_sample_data() -> (Box, Repository, PacBuild) { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + let pacbuild_to_add = PacBuild { + name: "dummy-pacbuild-deb".into(), + package_name: "dummy-pacbuild".into(), + description: "blah".into(), + dependencies: Vec::new(), + homepage: "https://example.com".into(), + install_state: InstallState::Direct( + NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + "1.0.0".into(), + ), + kind: Kind::DebFile("hashash".into()), + last_updated: NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + license: "BSD".into(), + maintainer: "saenai255".into(), + optional_dependencies: Vec::new(), + repology: "filter".into(), + repology_version: "1.0.1".into(), + repository: repo.url.clone(), + url: "https://example.com/dummy-pacbuild-1.0.0.deb".into(), + }; + + fss.add_repository(repo.clone()).unwrap(); + fss.add_pacbuild(pacbuild_to_add.clone(), &repo.url) + .unwrap(); + + (fss, repo, pacbuild_to_add) + } + } + + #[test] + fn new_creates_empty_fs_store() { + let fss = FileSystemStore::new(); + let pacbuilds = fss.get_all_pacbuilds(); + let repos = fss.get_all_repositories(); + + assert_eq!(pacbuilds.len(), 0); + assert_eq!(repos.len(), 0); + } + + #[test] + fn add_repository_works() { + let mut fss = FileSystemStore::new(); + + fss.add_repository(Repository::default()).unwrap(); + let repos = fss.get_all_repositories(); + + assert_eq!(repos.len(), 1); + } + + #[test] + fn get_repository_by_name_works() { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + + fss.add_repository(repo.clone()).unwrap(); + let found_repo = fss.get_repository_by_name(&repo.name).unwrap(); + + assert_eq!(repo, found_repo.clone()); + } + + #[test] + fn get_repository_by_url_works() { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + + fss.add_repository(repo.clone()).unwrap(); + let found_repo = fss.get_repository_by_url(&repo.url).unwrap(); + + assert_eq!(repo, found_repo.clone()); + } + + #[test] + fn add_pacbuild_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let pbs = fss.get_all_pacbuilds(); + + println!("{:#?}", pbs); + + assert_eq!(pbs.len(), 1); + } + + #[test] + fn get_pacbuild_by_name_and_url_works() { + let (fss, _, pacbuild) = util::create_store_with_sample_data(); + let found = fss + .get_pacbuild_by_name_and_url(&pacbuild.name, &pacbuild.repository) + .unwrap(); + + assert_eq!(found.clone(), pacbuild); + } + + #[test] + fn get_all_pacbuilds_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds(); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works() { + let (fss, _, pb) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_name_like(&pb.name); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_name_like("blalblala"); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = + fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Direct); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = + fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Indirect); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_kind_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::DebFile); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_kind_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::Binary); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works() { + let (fss, repo, _) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_repository_url(&repo.url); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_repository_url("this repo url does not exist"); + + assert_eq!(found.len(), 0); + } + + #[test] + fn update_pacbuild_works() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.description = "something else".into(); + + fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); + let found = fss + .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) + .unwrap(); + + assert_eq!(pb, *found); + } + + #[test] + fn update_all_pacbuilds_works() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.description = "something else".into(); + + fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + let found = fss + .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) + .unwrap(); + + assert_eq!(pb, *found); + } + + #[test] + #[should_panic] + fn update_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.name = "lala".into(); + pb.description = "something else".into(); + + fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); + } + + #[test] + #[should_panic] + fn update_all_pacbuilds_panics_when_pacbuild_not_found() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.name = "lala".into(); + pb.description = "something else".into(); + + fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn remove_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + + fss.remove_pacbuild("does-not-exist", &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn remove_all_pacbuilds_panics_when_pacbuild_not_found() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + + fss.remove_all_pacbuilds(&(&vec!["does-not-exist"])[..], &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn add_pacbuild_panics_when_pacbuild_already_exists() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + fss.add_pacbuild(pb.clone(), &pb.repository).unwrap(); + } + + #[test] + #[should_panic] + fn update_all_pacbuilds_panics_when_pacbuild_already_exists() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + fss.add_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + } +} diff --git a/src/store/mod.rs b/src/store/mod.rs new file mode 100644 index 0000000..4908eed --- /dev/null +++ b/src/store/mod.rs @@ -0,0 +1,6 @@ +//! Provides traits and structs to handle Pacstall's cache. + +pub mod base; +pub mod errors; +pub mod filters; +pub mod fs;