use std::future::Future;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use anyhow::Context as _;
use anyhow::Error;
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
use tracing::error;
use tracing::info;
use warp::http::StatusCode;
use warp::http::Uri;
use warp::Filter as _;
use warp::Reply as _;
use crate::index::Index;
use crate::publish::crate_file_name;
use crate::publish::publish_crate;
#[derive(Debug, Default, Deserialize, Serialize)]
struct RegistryError {
detail: String,
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct RegistryErrors {
errors: Vec<RegistryError>,
}
impl From<Error> for RegistryErrors {
fn from(error: Error) -> Self {
Self {
errors: error
.chain()
.map(ToString::to_string)
.map(|err| RegistryError { detail: err })
.collect(),
}
}
}
async fn response<T>(result: Result<T>) -> Result<impl warp::Reply, warp::Rejection>
where
T: warp::Reply,
{
let response = match result {
Ok(inner) => {
info!("request status: success");
inner.into_response()
},
Err(err) => {
error!("request status: error: {:#}", err);
let errors = RegistryErrors::from(err);
warp::reply::json(&errors).into_response()
},
};
let reply = warp::reply::with_status(response, StatusCode::OK);
Ok(reply)
}
pub fn serve(root: &Path, addr: SocketAddr) -> Result<(impl Future<Output = ()>, SocketAddr)> {
let shared = Arc::new(Mutex::new(Option::<Index>::None));
let copy = shared.clone();
let index = warp::path("git").and(warp::fs::dir(root.join(".git")));
let crates = warp::path("crates").and(warp::fs::dir(root.to_owned()));
let download = warp::get()
.and(warp::path("api"))
.and(warp::path("v1"))
.and(warp::path("crates"))
.and(warp::path::param())
.and(warp::path::param())
.and(warp::path("download"))
.map(move |name: String, version: String| {
let path = format!("/crates/{}", crate_file_name(&name, &version));
path.parse::<Uri>().map(warp::redirect).unwrap()
});
let publish = warp::put()
.and(warp::path("api"))
.and(warp::path("v1"))
.and(warp::path("crates"))
.and(warp::path("new"))
.and(warp::path::end())
.and(warp::body::bytes())
.and(warp::body::content_length_limit(20 * 1024 * 1024))
.map(move |body| {
let mut index = copy.lock().unwrap();
let index = index.as_mut().unwrap();
publish_crate(body, index).map(|()| String::new())
})
.and_then(response);
let mut addr = addr;
let original_port = addr.port();
if addr.port() == 0 {
if let Ok(port) = Index::try_read_port(root) {
addr.set_port(port)
}
}
let (addr, serve) = loop {
let routes = index
.clone()
.or(crates.clone())
.or(download)
.or(publish.clone())
.with(warp::trace::request());
let result = warp::serve(routes)
.try_bind_ephemeral(addr)
.with_context(|| format!("failed to bind to {}", addr));
match result {
Ok(result) => break result,
Err(_) if addr.port() != original_port => {
addr.set_port(original_port);
},
Err(err) => return Err(err),
}
};
let index = Index::new(root, &addr).with_context(|| {
format!(
"failed to create/instantiate crate index at {}",
root.display()
)
})?;
*shared.lock().unwrap() = Some(index);
Ok((serve, addr))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::to_string;
#[test]
fn registry_error_encoding() {
let expected = r#"{"errors":[{"detail":"error message text"}]}"#;
let errors = RegistryErrors {
errors: vec![RegistryError {
detail: "error message text".to_string(),
}],
};
assert_eq!(to_string(&errors).unwrap(), expected);
}
}