mod manager; pub mod package; pub use manager::RepoGroupManager; use std::path::PathBuf; use axum::body::Body; use axum::extract::{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::ModelTrait; 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; use crate::db; 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), ) } /// 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 post_package_archive( State(global): State, Path(repo): Path, body: Body, ) -> 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?; let mut body = body.into_data_stream(); 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 res = db::query::repo::by_name(&global.db, &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 }; // 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?; if let Some(entry) = res { entry.delete(&global.db).await?; } db::query::package::insert(&global.db, repo_id, pkg).await?; Ok(()) } // Remove the uploaded file and return the error Err(err) => { tokio::fs::remove_file(path).await?; Err(err.into()) } } } 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 { let res = db::query::repo::by_name(&global.db, &repo).await?; if let Some(repo_entry) = res { repo_entry.delete(&global.db).await?; } 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 arch_clone = arch.clone(); let repo_clone = repo.clone(); let repo_removed = tokio::task::spawn_blocking(move || { clone .write() .unwrap() .remove_repo_arch(&repo_clone, &arch_clone) }) .await??; if repo_removed { let res = db::query::repo::by_name(&global.db, &repo).await?; if let Some(repo_entry) = res { db::query::package::delete_with_arch(&global.db, repo_entry.id, &arch).await?; } tracing::info!("Removed architecture '{}' from repository '{}'", arch, repo); 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 clone = Arc::clone(&global.repo_manager); let path = PathBuf::from(&repo).join(arch).join(&file_name); let res = tokio::task::spawn_blocking(move || { clone.write().unwrap().remove_pkg_from_path(path, true) }) .await??; if let Some((name, version, release, arch)) = res { let res = db::query::repo::by_name(&global.db, &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?; if let Some(entry) = res { entry.delete(&global.db).await?; } } tracing::info!("Removed '{}' from repository '{}'", file_name, repo); Ok(StatusCode::OK) } else { Ok(StatusCode::NOT_FOUND) } }