diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 09b6c95..800587f 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -1,7 +1,5 @@ mod pagination; -use sea_orm::{*}; - use axum::extract::{Path, Query, State}; use axum::routing::get; use axum::Json; @@ -9,7 +7,7 @@ use axum::Router; use pagination::PaginatedResponse; -use crate::db::{self, *}; +use crate::db; pub fn router() -> Router { Router::new() @@ -22,34 +20,26 @@ pub fn router() -> Router { async fn get_repos( State(global): State, Query(pagination): Query, - Query(filter): Query, ) -> crate::Result>> { - let page = pagination.page.unwrap_or(1) - 1; - let per_page = pagination.per_page.unwrap_or(25); - - let paginator = Repo::find() - .filter(filter) - .order_by_asc(package::Column::Id) - .paginate(&global.db, pagination.per_page.unwrap_or(25)); - let items = paginator - .fetch_page(pagination.page.unwrap_or(1) - 1) + let (total_pages, repos) = global + .db + .repo + .page( + pagination.per_page.unwrap_or(25), + pagination.page.unwrap_or(1) - 1, + ) .await?; - let total_pages = paginator.num_pages().await?; - - Ok(Json(PaginatedResponse { - page, - per_page, - total_pages, - count: items.len(), - items, - })) + Ok(Json(pagination.res(total_pages, repos))) } async fn get_single_repo( State(global): State, Path(id): Path, ) -> crate::Result> { - let repo = db::query::repo::by_id(&global.db, id) + let repo = global + .db + .repo + .by_id(id) .await? .ok_or(axum::http::StatusCode::NOT_FOUND)?; @@ -59,15 +49,15 @@ async fn get_single_repo( async fn get_packages( State(global): State, Query(pagination): Query, - Query(filter): Query, ) -> crate::Result>> { - let (total_pages, pkgs) = db::query::package::page( - &global.db, - pagination.per_page.unwrap_or(25), - pagination.page.unwrap_or(1) - 1, - filter, - ) - .await?; + let (total_pages, pkgs) = global + .db + .pkg + .page( + pagination.per_page.unwrap_or(25), + pagination.page.unwrap_or(1) - 1, + ) + .await?; Ok(Json(pagination.res(total_pages, pkgs))) } @@ -76,7 +66,10 @@ async fn get_single_package( State(global): State, Path(id): Path, ) -> crate::Result> { - let entry = db::query::package::full(&global.db, id) + let entry = global + .db + .pkg + .full(id) .await? .ok_or(axum::http::StatusCode::NOT_FOUND)?; diff --git a/server/src/api/pagination.rs b/server/src/api/pagination.rs index db1ffa0..aa1e5cb 100644 --- a/server/src/api/pagination.rs +++ b/server/src/api/pagination.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -pub const DEFAULT_PAGE: u64 = 1; +pub const DEFAULT_PAGE: u64 = 0; pub const DEFAULT_PER_PAGE: u64 = 25; #[derive(Deserialize)] diff --git a/server/src/cli.rs b/server/src/cli.rs index 1ae6de4..14a5808 100644 --- a/server/src/cli.rs +++ b/server/src/cli.rs @@ -10,7 +10,6 @@ use std::sync::{Arc, RwLock}; use tower_http::trace::TraceLayer; use tracing::debug; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use sea_orm_migration::MigratorTrait; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -76,8 +75,10 @@ impl Cli { debug!("Connecting to database with URL {}", db_url); - let db = sea_orm::Database::connect(db_url).await?; - crate::db::Migrator::up(&db, None).await?; + let db = crate::db::RieterDb::connect(db_url).await?; + // let db = crate::db::init("postgres://rieter:rieter@localhost:5432/rieter") + // .await + // .unwrap(); let config = Config { data_dir: self.data_dir.clone(), diff --git a/server/src/db/conn.rs b/server/src/db/conn.rs new file mode 100644 index 0000000..2756236 --- /dev/null +++ b/server/src/db/conn.rs @@ -0,0 +1,61 @@ +use super::RieterDb; +use sea_orm::{DbBackend, DbErr, ExecResult, QueryResult, Statement}; +use std::{future::Future, pin::Pin}; + +// Allows RieterDb objects to be passed to ORM functions +impl sea_orm::ConnectionTrait for RieterDb { + fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + fn execute<'life0, 'async_trait>( + &'life0 self, + stmt: Statement, + ) -> Pin> + Send + 'async_trait>> + where + Self: 'async_trait, + 'life0: 'async_trait, + { + self.conn.execute(stmt) + } + fn execute_unprepared<'life0, 'life1, 'async_trait>( + &'life0 self, + sql: &'life1 str, + ) -> Pin> + Send + 'async_trait>> + where + Self: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + { + self.conn.execute_unprepared(sql) + } + fn query_one<'life0, 'async_trait>( + &'life0 self, + stmt: Statement, + ) -> Pin< + Box< + dyn Future, DbErr>> + + Send + + 'async_trait, + >, + > + where + Self: 'async_trait, + 'life0: 'async_trait, + { + self.conn.query_one(stmt) + } + fn query_all<'life0, 'async_trait>( + &'life0 self, + stmt: Statement, + ) -> Pin< + Box< + dyn Future, DbErr>> + Send + 'async_trait, + >, + > + where + Self: 'async_trait, + 'life0: 'async_trait, + { + self.conn.query_all(stmt) + } +} diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index b29f3d3..587052d 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -1,20 +1,19 @@ +mod conn; pub mod entities; mod migrator; -pub mod query; - -use sea_orm::{DeriveActiveEnum, EnumIter}; +mod query; +use sea_orm::{ConnectOptions, Database, DatabaseConnection, DeriveActiveEnum, EnumIter}; +use sea_orm_migration::MigratorTrait; use serde::{Deserialize, Serialize}; pub use entities::{prelude::*, *}; -pub use migrator::Migrator; - +use migrator::Migrator; type Result = std::result::Result; #[derive(EnumIter, DeriveActiveEnum, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] #[sea_orm(rs_type = "i32", db_type = "Integer")] -#[serde(rename_all = "lowercase")] pub enum PackageRelatedEnum { #[sea_orm(num_value = 0)] Conflicts, @@ -41,3 +40,24 @@ pub struct FullPackage { related: Vec<(PackageRelatedEnum, String)>, files: Vec, } + +#[derive(Clone, Debug)] +pub struct RieterDb { + conn: DatabaseConnection, + pub pkg: query::PackageQuery, + pub repo: query::RepoQuery, +} + +impl RieterDb { + pub async fn connect>(opt: C) -> Result { + let db = Database::connect(opt).await?; + + Migrator::up(&db, None).await?; + + Ok(Self { + conn: db.clone(), + pkg: query::PackageQuery::new(db.clone()), + repo: query::RepoQuery::new(db.clone()), + }) + } +} diff --git a/server/src/db/query/mod.rs b/server/src/db/query/mod.rs index 87d61e3..a52cccf 100644 --- a/server/src/db/query/mod.rs +++ b/server/src/db/query/mod.rs @@ -1,4 +1,7 @@ -pub mod package; -pub mod repo; +mod package; +mod repo; + +pub use package::PackageQuery; +pub use repo::RepoQuery; type Result = std::result::Result; diff --git a/server/src/db/query/package.rs b/server/src/db/query/package.rs index 9d7a9f2..c977984 100644 --- a/server/src/db/query/package.rs +++ b/server/src/db/query/package.rs @@ -1,204 +1,190 @@ -use sea_orm::{sea_query::IntoCondition, *}; -use serde::Deserialize; +use sea_orm::*; use crate::db::*; -#[derive(Deserialize)] -pub struct Filter { - repo: Option, - arch: Option, - name: Option, +#[derive(Clone, Debug)] +pub struct PackageQuery { + conn: DatabaseConnection, } -impl IntoCondition for Filter { - fn into_condition(self) -> Condition { - Condition::all() - .add_option(self.repo.map(|repo| package::Column::RepoId.eq(repo))) - .add_option(self.arch.map(|arch| package::Column::Arch.eq(arch))) - .add_option( - self.name - .map(|name| package::Column::Name.like(format!("%{}%", name))), - ) - } -} - -pub async fn page( - conn: &DbConn, - per_page: u64, - page: u64, - filter: Filter, -) -> super::Result<(u64, Vec)> { - let paginator = Package::find() - .filter(filter) - .order_by_asc(package::Column::Id) - .paginate(conn, per_page); - let packages = paginator.fetch_page(page).await?; - let total_pages = paginator.num_pages().await?; - - Ok((total_pages, packages)) -} - -pub async fn by_id(conn: &DbConn, id: i32) -> Result> { - package::Entity::find_by_id(id).one(conn).await -} - -pub async fn by_fields( - conn: &DbConn, - repo_id: i32, - name: &str, - version: Option<&str>, - arch: &str, -) -> Result> { - let mut query = Package::find() - .filter(package::Column::RepoId.eq(repo_id)) - .filter(package::Column::Name.eq(name)) - .filter(package::Column::Arch.eq(arch)); - - if let Some(version) = version { - query = query.filter(package::Column::Version.eq(version)); +impl PackageQuery { + pub fn new(conn: DatabaseConnection) -> Self { + Self { conn } } - query.one(conn).await -} + pub async fn page( + &self, + per_page: u64, + page: u64, + ) -> super::Result<(u64, Vec)> { + let paginator = Package::find() + .order_by_asc(package::Column::Id) + .paginate(&self.conn, per_page); + let packages = paginator.fetch_page(page).await?; + let total_pages = paginator.num_pages().await?; -pub async fn delete_with_arch(conn: &DbConn, repo_id: i32, arch: &str) -> Result { - Package::delete_many() - .filter(package::Column::RepoId.eq(repo_id)) - .filter(package::Column::Arch.eq(arch)) - .exec(conn) - .await -} + Ok((total_pages, packages)) + } -pub async fn insert(conn: &DbConn, repo_id: i32, pkg: crate::repo::package::Package) -> Result<()> { - let info = pkg.info; + pub async fn by_id(&self, id: i32) -> Result> { + package::Entity::find_by_id(id).one(&self.conn).await + } - let model = package::ActiveModel { - id: NotSet, - repo_id: Set(repo_id), - base: Set(info.base), - name: Set(info.name), - version: Set(info.version), - arch: Set(info.arch), - size: Set(info.size), - c_size: Set(info.csize), - description: Set(info.description), - url: Set(info.url), - build_date: Set(info.build_date.to_string()), - packager: Set(info.packager), - pgp_sig: Set(info.pgpsig), - pgp_sig_size: Set(info.pgpsigsize), - sha256_sum: Set(info.sha256sum), - }; + pub async fn by_fields( + &self, + repo_id: i32, + name: &str, + version: Option<&str>, + arch: &str, + ) -> Result> { + let mut query = Package::find() + .filter(package::Column::RepoId.eq(repo_id)) + .filter(package::Column::Name.eq(name)) + .filter(package::Column::Arch.eq(arch)); - let pkg_entry = model.insert(conn).await?; + if let Some(version) = version { + query = query.filter(package::Column::Version.eq(version)); + } - // Insert all the related tables - PackageLicense::insert_many(info.licenses.iter().map(|s| package_license::ActiveModel { - package_id: Set(pkg_entry.id), - name: Set(s.to_string()), - })) - .on_empty_do_nothing() - .exec(conn) - .await?; + query.one(&self.conn).await + } - PackageGroup::insert_many(info.groups.iter().map(|s| package_group::ActiveModel { - package_id: Set(pkg_entry.id), - name: Set(s.to_string()), - })) - .on_empty_do_nothing() - .exec(conn) - .await?; + pub async fn delete_with_arch(&self, repo_id: i32, arch: &str) -> Result { + Package::delete_many() + .filter(package::Column::RepoId.eq(repo_id)) + .filter(package::Column::Arch.eq(arch)) + .exec(&self.conn) + .await + } - let related = info - .conflicts - .iter() - .map(|s| (PackageRelatedEnum::Conflicts, s)) - .chain( - info.replaces - .iter() - .map(|s| (PackageRelatedEnum::Replaces, s)), - ) - .chain( - info.provides - .iter() - .map(|s| (PackageRelatedEnum::Provides, s)), - ) - .chain(info.depends.iter().map(|s| (PackageRelatedEnum::Depend, s))) - .chain( - info.makedepends - .iter() - .map(|s| (PackageRelatedEnum::Depend, s)), - ) - .chain( - info.checkdepends - .iter() - .map(|s| (PackageRelatedEnum::Checkdepend, s)), - ) - .chain( - info.optdepends - .iter() - .map(|s| (PackageRelatedEnum::Optdepend, s)), - ); + pub async fn insert(&self, repo_id: i32, pkg: crate::repo::package::Package) -> Result<()> { + let info = pkg.info; - PackageRelated::insert_many(related.map(|(t, s)| package_related::ActiveModel { - package_id: Set(pkg_entry.id), - r#type: Set(t), - name: Set(s.to_string()), - })) - .on_empty_do_nothing() - .exec(conn) - .await?; + let model = package::ActiveModel { + id: NotSet, + repo_id: Set(repo_id), + base: Set(info.base), + name: Set(info.name), + version: Set(info.version), + arch: Set(info.arch), + size: Set(info.size), + c_size: Set(info.csize), + description: Set(info.description), + url: Set(info.url), + build_date: Set(info.build_date.to_string()), + packager: Set(info.packager), + pgp_sig: Set(info.pgpsig), + pgp_sig_size: Set(info.pgpsigsize), + sha256_sum: Set(info.sha256sum), + }; - PackageFile::insert_many(pkg.files.iter().map(|s| package_file::ActiveModel { - package_id: Set(pkg_entry.id), - path: Set(s.display().to_string()), - })) - .on_empty_do_nothing() - .exec(conn) - .await?; + let pkg_entry = model.insert(&self.conn).await?; - Ok(()) -} - -pub async fn full(conn: &DbConn, id: i32) -> Result> { - if let Some(entry) = by_id(conn, id).await? { - let licenses = entry - .find_related(PackageLicense) - .all(conn) - .await? - .into_iter() - .map(|e| e.name) - .collect(); - let groups = entry - .find_related(PackageGroup) - .all(conn) - .await? - .into_iter() - .map(|e| e.name) - .collect(); - let related = entry - .find_related(PackageRelated) - .all(conn) - .await? - .into_iter() - .map(|e| (e.r#type, e.name)) - .collect(); - let files = entry - .find_related(PackageFile) - .all(conn) - .await? - .into_iter() - .map(|e| e.path) - .collect(); - - Ok(Some(FullPackage { - entry, - licenses, - groups, - related, - files, + // Insert all the related tables + PackageLicense::insert_many(info.licenses.iter().map(|s| package_license::ActiveModel { + package_id: Set(pkg_entry.id), + name: Set(s.to_string()), })) - } else { - Ok(None) + .on_empty_do_nothing() + .exec(&self.conn) + .await?; + + PackageGroup::insert_many(info.groups.iter().map(|s| package_group::ActiveModel { + package_id: Set(pkg_entry.id), + name: Set(s.to_string()), + })) + .on_empty_do_nothing() + .exec(&self.conn) + .await?; + + let related = info + .conflicts + .iter() + .map(|s| (PackageRelatedEnum::Conflicts, s)) + .chain( + info.replaces + .iter() + .map(|s| (PackageRelatedEnum::Replaces, s)), + ) + .chain( + info.provides + .iter() + .map(|s| (PackageRelatedEnum::Provides, s)), + ) + .chain(info.depends.iter().map(|s| (PackageRelatedEnum::Depend, s))) + .chain( + info.makedepends + .iter() + .map(|s| (PackageRelatedEnum::Depend, s)), + ) + .chain( + info.checkdepends + .iter() + .map(|s| (PackageRelatedEnum::Checkdepend, s)), + ) + .chain( + info.optdepends + .iter() + .map(|s| (PackageRelatedEnum::Optdepend, s)), + ); + + PackageRelated::insert_many(related.map(|(t, s)| package_related::ActiveModel { + package_id: Set(pkg_entry.id), + r#type: Set(t), + name: Set(s.to_string()), + })); + + PackageFile::insert_many(pkg.files.iter().map(|s| package_file::ActiveModel { + package_id: Set(pkg_entry.id), + path: Set(s.display().to_string()), + })) + .on_empty_do_nothing() + .exec(&self.conn) + .await?; + + Ok(()) + } + + pub async fn full(&self, id: i32) -> Result> { + if let Some(entry) = self.by_id(id).await? { + let licenses = entry + .find_related(PackageLicense) + .all(&self.conn) + .await? + .into_iter() + .map(|e| e.name) + .collect(); + let groups = entry + .find_related(PackageGroup) + .all(&self.conn) + .await? + .into_iter() + .map(|e| e.name) + .collect(); + let related = entry + .find_related(PackageRelated) + .all(&self.conn) + .await? + .into_iter() + .map(|e| (e.r#type, e.name)) + .collect(); + let files = entry + .find_related(PackageFile) + .all(&self.conn) + .await? + .into_iter() + .map(|e| e.path) + .collect(); + + Ok(Some(FullPackage { + entry, + licenses, + groups, + related, + files, + })) + } else { + Ok(None) + } } } diff --git a/server/src/db/query/repo.rs b/server/src/db/query/repo.rs index 34fbb81..5e54fdc 100644 --- a/server/src/db/query/repo.rs +++ b/server/src/db/query/repo.rs @@ -1,49 +1,49 @@ -use sea_orm::{sea_query::IntoCondition, *}; +use sea_orm::*; use crate::db::*; -#[derive(Deserialize)] -pub struct Filter { - name: Option, +#[derive(Clone, Debug)] +pub struct RepoQuery { + conn: DatabaseConnection, } -impl IntoCondition for Filter { - fn into_condition(self) -> Condition { - Condition::all().add_option(self.name.map(|name| package::Column::Name.like(name))) +impl RepoQuery { + pub fn new(conn: DatabaseConnection) -> Self { + Self { conn } + } + + pub async fn page(&self, per_page: u64, page: u64) -> Result<(u64, Vec)> { + let paginator = Repo::find() + .order_by_asc(repo::Column::Id) + .paginate(&self.conn, per_page); + let repos = paginator.fetch_page(page).await?; + let total_pages = paginator.num_pages().await?; + + Ok((total_pages, repos)) + } + + pub async fn by_id(&self, id: i32) -> Result> { + repo::Entity::find_by_id(id).one(&self.conn).await + } + + pub async fn by_name(&self, name: &str) -> Result> { + Repo::find() + .filter(repo::Column::Name.eq(name)) + .one(&self.conn) + .await + } + + pub async fn insert( + &self, + name: &str, + description: Option<&str>, + ) -> Result> { + let model = repo::ActiveModel { + id: NotSet, + name: Set(String::from(name)), + description: Set(description.map(String::from)), + }; + + Repo::insert(model).exec(&self.conn).await } } - -pub async fn page(conn: &DbConn, per_page: u64, page: u64) -> Result<(u64, Vec)> { - let paginator = Repo::find() - .order_by_asc(repo::Column::Id) - .paginate(conn, per_page); - let repos = paginator.fetch_page(page).await?; - let total_pages = paginator.num_pages().await?; - - Ok((total_pages, repos)) -} - -pub async fn by_id(conn: &DbConn, id: i32) -> Result> { - repo::Entity::find_by_id(id).one(conn).await -} - -pub async fn by_name(conn: &DbConn, name: &str) -> Result> { - Repo::find() - .filter(repo::Column::Name.eq(name)) - .one(conn) - .await -} - -pub async fn insert( - conn: &DbConn, - name: &str, - description: Option<&str>, -) -> Result> { - let model = repo::ActiveModel { - id: NotSet, - name: Set(String::from(name)), - description: Set(description.map(String::from)), - }; - - Repo::insert(model).exec(conn).await -} diff --git a/server/src/main.rs b/server/src/main.rs index 9068bd7..fc5c110 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,7 +22,7 @@ pub struct Config { pub struct Global { config: Config, repo_manager: Arc>, - db: sea_orm::DbConn, + db: db::RieterDb, } #[tokio::main] diff --git a/server/src/repo/mod.rs b/server/src/repo/mod.rs index 5bf00a6..4681fb6 100644 --- a/server/src/repo/mod.rs +++ b/server/src/repo/mod.rs @@ -5,7 +5,7 @@ pub use manager::RepoGroupManager; use std::path::PathBuf; -use axum::body::{Body}; +use axum::body::{Body, BodyDataStream}; use axum::extract::{Path, State}; use axum::http::Request; use axum::http::StatusCode; @@ -21,8 +21,6 @@ use tower_http::services::{ServeDir, ServeFile}; use tower_http::validate_request::ValidateRequestHeaderLayer; use uuid::Uuid; -use crate::db; - const DB_FILE_EXTS: [&str; 4] = [".db", ".files", ".db.tar.gz", ".files.tar.gz"]; pub fn router(api_key: &str) -> Router { @@ -130,31 +128,26 @@ async fn post_package_archive( tracing::info!("Added '{}' to repository '{}'", pkg.file_name(), repo); // Query the repo for its ID, or create it if it does not already exist - let res = db::query::repo::by_name(&global.db, &repo).await?; + let res = global.db.repo.by_name(&repo).await?; let repo_id = if let Some(repo_entity) = res { repo_entity.id } else { - db::query::repo::insert(&global.db, &repo, None) - .await? - .last_insert_id + global.db.repo.insert(&repo, None).await?.last_insert_id }; // If the package already exists in the database, we remove it first - let res = db::query::package::by_fields( - &global.db, - repo_id, - &pkg.info.name, - None, - &pkg.info.arch, - ) - .await?; + let res = global + .db + .pkg + .by_fields(repo_id, &pkg.info.name, None, &pkg.info.arch) + .await?; if let Some(entry) = res { entry.delete(&global.db).await?; } - db::query::package::insert(&global.db, repo_id, pkg).await?; + global.db.pkg.insert(repo_id, pkg).await?; Ok(()) } @@ -179,7 +172,7 @@ async fn delete_repo( .await??; if repo_removed { - let res = db::query::repo::by_name(&global.db, &repo).await?; + let res = global.db.repo.by_name(&repo).await?; if let Some(repo_entry) = res { repo_entry.delete(&global.db).await?; @@ -210,10 +203,10 @@ async fn delete_arch_repo( .await??; if repo_removed { - let res = db::query::repo::by_name(&global.db, &repo).await?; + let res = global.db.repo.by_name(&repo).await?; if let Some(repo_entry) = res { - db::query::package::delete_with_arch(&global.db, repo_entry.id, &arch).await?; + global.db.pkg.delete_with_arch(repo_entry.id, &arch).await?; } tracing::info!("Removed architecture '{}' from repository '{}'", arch, repo); @@ -236,17 +229,19 @@ async fn delete_package( .await??; if let Some((name, version, release, arch)) = res { - let res = db::query::repo::by_name(&global.db, &repo).await?; + let res = global.db.repo.by_name(&repo).await?; if let Some(repo_entry) = res { - let res = db::query::package::by_fields( - &global.db, - repo_entry.id, - &name, - Some(&format!("{}-{}", version, release)), - &arch, - ) - .await?; + let res = global + .db + .pkg + .by_fields( + repo_entry.id, + &name, + Some(&format!("{}-{}", version, release)), + &arch, + ) + .await?; if let Some(entry) = res { entry.delete(&global.db).await?;