use std::sync::Arc;
use axum::extract::rejection::PathRejection;
use axum::extract::{Path, State};
use axum::http::{HeaderMap, Method, header};
use axum::response::{IntoResponse, Response};
use tokio::sync::OwnedSemaphorePermit;
use crate::App;
use crate::artifacts::{self};
use crate::http::error::ApiError;
use crate::http::npm_routes;
use crate::npm;
use crate::policy::Ecosystem;
use crate::store::rows::ReferenceId;
pub async fn serve_npm(
state: State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<(String, String)>, PathRejection>,
) -> Result<Response, ApiError> {
if let Ok(Path((reference_id, filename))) = &path
&& reference_id == "-"
{
let State(app) = state;
return serve_npm_named(&app, "artifacts", filename, &method, &headers).await;
}
serve(Ecosystem::Npm, state, method, headers, path).await
}
pub async fn serve_pypi(
state: State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<(String, String)>, PathRejection>,
) -> Result<Response, ApiError> {
serve(Ecosystem::PyPi, state, method, headers, path).await
}
async fn serve(
ecosystem: Ecosystem,
State(app): State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<(String, String)>, PathRejection>,
) -> Result<Response, ApiError> {
let Path((reference_id, filename)) = path?;
let permit = super::admit(&app)?;
let id = ReferenceId::parse_hex(&reference_id).map_err(|err| {
tracing::debug!(error = %err, "refused an artifact reference id");
ApiError::InvalidInput("a reference id is 64 hexadecimal characters")
})?;
deliver(&app, ecosystem, id, &filename, &method, &headers, permit).await
}
pub async fn serve_npm_upstream(
State(app): State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<(String, String)>, PathRejection>,
) -> Result<Response, ApiError> {
let Path((package, filename)) = path?;
serve_npm_named(&app, &package, &filename, &method, &headers).await
}
pub async fn serve_npm_upstream_scoped(
State(app): State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<(String, String, String)>, PathRejection>,
) -> Result<Response, ApiError> {
let Path((scope, package, filename)) = path?;
if !scope.starts_with('@') {
return Err(ApiError::NotFound);
}
serve_npm_named(
&app,
&format!("{scope}/{package}"),
&filename,
&method,
&headers,
)
.await
}
pub async fn serve_npm_namespace(
State(app): State<Arc<App>>,
method: Method,
headers: HeaderMap,
path: Result<Path<String>, PathRejection>,
) -> Result<Response, ApiError> {
let Path(rest) = path?;
if rest.contains('/') || !rest.starts_with("npm-") || !rest.ends_with(".tgz") {
return Err(ApiError::NotFound);
}
serve_npm_named(&app, "npm", &rest, &method, &headers).await
}
async fn serve_npm_named(
app: &Arc<App>,
package: &str,
filename: &str,
method: &Method,
headers: &HeaderMap,
) -> Result<Response, ApiError> {
let permit = super::admit(app)?;
let name = npm_routes::parse_name(package)?;
let id = npm::reference_for_file(app, &name, filename).await?;
deliver(app, Ecosystem::Npm, id, filename, method, headers, permit).await
}
async fn deliver(
app: &Arc<App>,
ecosystem: Ecosystem,
id: ReferenceId,
filename: &str,
method: &Method,
headers: &HeaderMap,
permit: OwnedSemaphorePermit,
) -> Result<Response, ApiError> {
let range = headers
.get(header::RANGE)
.and_then(|value| value.to_str().ok());
let response = artifacts::serve_artifact(app, ecosystem, id, filename, method, range).await?;
Ok(response.under(&app.limits, permit).into_response())
}