mod manager; mod package; pub use manager::RepoGroupManager; 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 std::sync::Arc; use tokio::{fs, io::AsyncWriteExt}; use tower::util::ServiceExt; use tower_http::services::{ServeDir, ServeFile}; use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/:repo", post(post_package_archive).delete(delete_repo)) .route("/:repo/:arch", delete(delete_arch_repo)) .route( "/:repo/:arch/:filename", delete(delete_package).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 res = tokio::task::spawn_blocking(move || { clone.write().unwrap().add_pkg_from_path(&repo, &path_clone) }) .await?; // Remove the downloaded file if the adding failed if res.is_err() { let _ = tokio::fs::remove_file(path).await; } Ok(res?) } /// 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 file_name.ends_with(".db") || file_name.ends_with(".db.tar.gz") { // Append tar extension to ensure we find the file if file_name.ends_with(".db") { 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_removed = tokio::task::spawn_blocking(move || clone.write().unwrap().remove_repo(&repo)).await??; if repo_removed { 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 repo_removed = tokio::task::spawn_blocking(move || clone.write().unwrap().remove_repo_arch(&repo, &arch)) .await??; if repo_removed { 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 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 { Ok(StatusCode::OK) } else { Ok(StatusCode::NOT_FOUND) } }