mod manager; mod package; pub use manager::RepoGroupManager; use crate::db::entities::{package as db_package, repo as db_repo}; use axum::body::Body; use axum::extract::{BodyStream, Path, State}; use axum::http::Request; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{delete, post}; use axum::Router; use futures::StreamExt; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter}; use std::sync::Arc; use tokio::{fs, io::AsyncWriteExt}; use tower::util::ServiceExt; use tower_http::services::{ServeDir, ServeFile}; use tower_http::validate_request::ValidateRequestHeaderLayer; use uuid::Uuid; const DB_FILE_EXTS: [&str; 4] = [".db", ".files", ".db.tar.gz", ".files.tar.gz"]; pub fn router(api_key: &str) -> Router { Router::new() .route( "/:repo", post(post_package_archive) .delete(delete_repo) .route_layer(ValidateRequestHeaderLayer::bearer(api_key)), ) .route( "/:repo/:arch", delete(delete_arch_repo).route_layer(ValidateRequestHeaderLayer::bearer(api_key)), ) // Routes added after the layer do not get that layer applied, so the GET requests will not // be authorized .route( "/:repo/:arch/:filename", delete(delete_package) .route_layer(ValidateRequestHeaderLayer::bearer(api_key)) .get(get_file), ) } async fn post_package_archive( State(global): State, Path(repo): Path, mut body: BodyStream, ) -> crate::Result<()> { // We first stream the uploaded file to disk let uuid: uuid::fmt::Simple = Uuid::new_v4().into(); let path = global.config.pkg_dir.join(uuid.to_string()); let mut f = fs::File::create(&path).await?; while let Some(chunk) = body.next().await { f.write_all(&chunk?).await?; } let clone = Arc::clone(&global.repo_manager); let path_clone = path.clone(); let repo_clone = repo.clone(); let res = tokio::task::spawn_blocking(move || { clone .write() .unwrap() .add_pkg_from_path(&repo_clone, &path_clone) }) .await?; match res { // Insert the newly added package into the database Ok(pkg) => { 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 repo_entity = db_repo::Entity::find() .filter(db_repo::Column::Name.eq(&repo)) .one(&global.db) .await?; let repo_id = if let Some(repo_entity) = repo_entity { repo_entity.id } else { let model = db_repo::ActiveModel { name: sea_orm::Set(repo.clone()), ..Default::default() }; db_repo::Entity::insert(model) .exec(&global.db) .await? .last_insert_id }; // Insert the package's data into the database let mut model: db_package::ActiveModel = pkg.into(); model.repo_id = sea_orm::Set(repo_id); model.insert(&global.db).await?; Ok(()) } // Remove the uploaded file and return the error Err(err) => { tokio::fs::remove_file(path).await?; Err(err.into()) } } } /// Serve the package archive files and database archives. If files are requested for an /// architecture that does not have any explicit packages, a repository containing only "any" files /// is returned. async fn get_file( State(global): State, Path((repo, arch, mut file_name)): Path<(String, String, String)>, req: Request, ) -> crate::Result { let repo_dir = global.config.repo_dir.join(&repo).join(&arch); let repo_exists = tokio::fs::try_exists(&repo_dir).await?; let res = if DB_FILE_EXTS.iter().any(|ext| file_name.ends_with(ext)) { // Append tar extension to ensure we find the file if !file_name.ends_with(".tar.gz") { file_name.push_str(".tar.gz"); }; if repo_exists { ServeFile::new(repo_dir.join(file_name)).oneshot(req).await } else { let path = global .config .repo_dir .join(repo) .join(manager::ANY_ARCH) .join(file_name); ServeFile::new(path).oneshot(req).await } } else { let any_file = global .config .pkg_dir .join(repo) .join(manager::ANY_ARCH) .join(file_name); if repo_exists { ServeDir::new(global.config.pkg_dir) .fallback(ServeFile::new(any_file)) .oneshot(req) .await } else { ServeFile::new(any_file).oneshot(req).await } }; Ok(res) } async fn delete_repo( State(global): State, Path(repo): Path, ) -> crate::Result { let clone = Arc::clone(&global.repo_manager); let repo_clone = repo.clone(); let repo_removed = tokio::task::spawn_blocking(move || clone.write().unwrap().remove_repo(&repo_clone)) .await??; if repo_removed { tracing::info!("Removed repository '{}'", repo); Ok(StatusCode::OK) } else { Ok(StatusCode::NOT_FOUND) } } async fn delete_arch_repo( State(global): State, Path((repo, arch)): Path<(String, String)>, ) -> crate::Result { let clone = Arc::clone(&global.repo_manager); let log = format!("Removed architecture '{}' from repository '{}'", arch, repo); let repo_removed = tokio::task::spawn_blocking(move || clone.write().unwrap().remove_repo_arch(&repo, &arch)) .await??; if repo_removed { tracing::info!(log); Ok(StatusCode::OK) } else { Ok(StatusCode::NOT_FOUND) } } async fn delete_package( State(global): State, Path((repo, arch, file_name)): Path<(String, String, String)>, ) -> crate::Result { let name_parts = file_name.split('-').collect::>(); // Package archive files use the naming scheme pkgname-pkgver-pkgrel-arch, so a valid // name contains at least 4 dash-separated sections if name_parts.len() < 4 { return Ok(StatusCode::NOT_FOUND); } let name = name_parts[..name_parts.len() - 3].join("-"); let log = format!("Removed '{}' from repository '{}'", file_name, repo); let clone = Arc::clone(&global.repo_manager); let pkg_removed = tokio::task::spawn_blocking(move || { clone.write().unwrap().remove_pkg(&repo, &arch, &name, true) }) .await??; if pkg_removed { tracing::info!(log); Ok(StatusCode::OK) } else { Ok(StatusCode::NOT_FOUND) } }