Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Add `/raw` route
xphoniex committed 3 years ago
commit e7de186e0eb1a1504597c7e79cd3422c1ea33139
parent 9a03136516aa52af0d629dd6ab8ad47e390738d4
10 files changed +167 -96
modified radicle-httpd/src/api.rs
@@ -21,7 +21,6 @@ use radicle::identity::Id;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::Profile;

-
mod axum_extra;
mod error;
mod json;
mod v1;
deleted radicle-httpd/src/api/axum_extra.rs
@@ -1,89 +0,0 @@
-
use axum::extract::path::ErrorKind;
-
use axum::extract::rejection::{PathRejection, QueryRejection};
-
use axum::extract::FromRequestParts;
-
use axum::http::request::Parts;
-
use axum::http::StatusCode;
-
use axum::{async_trait, Json};
-

-
use serde::de::DeserializeOwned;
-
use serde::Serialize;
-

-
pub struct Path<T>(pub T);
-

-
#[async_trait]
-
impl<S, T> FromRequestParts<S> for Path<T>
-
where
-
    T: DeserializeOwned + Send,
-
    S: Send + Sync,
-
{
-
    type Rejection = (StatusCode, axum::Json<Error>);
-

-
    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
-
        match axum::extract::Path::<T>::from_request_parts(req, state).await {
-
            Ok(value) => Ok(Self(value.0)),
-
            Err(rejection) => {
-
                let status = StatusCode::BAD_REQUEST;
-
                let body = match rejection {
-
                    PathRejection::FailedToDeserializePathParams(inner) => {
-
                        let kind = inner.into_kind();
-
                        match &kind {
-
                            ErrorKind::Message(msg) => Json(Error {
-
                                success: false,
-
                                error: msg.to_string(),
-
                            }),
-
                            _ => Json(Error {
-
                                success: false,
-
                                error: kind.to_string(),
-
                            }),
-
                        }
-
                    }
-
                    _ => Json(Error {
-
                        success: false,
-
                        error: format!("{rejection}"),
-
                    }),
-
                };
-

-
                Err((status, body))
-
            }
-
        }
-
    }
-
}
-

-
#[derive(Default)]
-
pub struct Query<T>(pub T);
-

-
#[async_trait]
-
impl<S, T> FromRequestParts<S> for Query<T>
-
where
-
    T: DeserializeOwned + Send,
-
    S: Send + Sync,
-
{
-
    type Rejection = (StatusCode, axum::Json<Error>);
-

-
    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
-
        match axum::extract::Query::<T>::from_request_parts(req, state).await {
-
            Ok(value) => Ok(Self(value.0)),
-
            Err(rejection) => {
-
                let status = StatusCode::BAD_REQUEST;
-
                let body = match rejection {
-
                    QueryRejection::FailedToDeserializeQueryString(inner) => Json(Error {
-
                        success: false,
-
                        error: inner.to_string(),
-
                    }),
-
                    _ => Json(Error {
-
                        success: false,
-
                        error: format!("{rejection}"),
-
                    }),
-
                };
-

-
                Err((status, body))
-
            }
-
        }
-
    }
-
}
-

-
#[derive(Serialize)]
-
pub struct Error {
-
    success: bool,
-
    error: String,
-
}
modified radicle-httpd/src/api/v1/delegates.rs
@@ -8,11 +8,11 @@ use radicle::cob::patch::Patches;
use radicle::identity::Did;
use radicle::storage::{ReadRepository, ReadStorage};

-
use crate::api::axum_extra::{Path, Query};
use crate::api::error::Error;
use crate::api::project::Info;
use crate::api::Context;
use crate::api::PaginationQuery;
+
use crate::axum_extra::{Path, Query};

pub fn router(ctx: Context) -> Router {
    Router::new()
modified radicle-httpd/src/api/v1/projects.rs
@@ -17,13 +17,14 @@ use radicle::cob::patch::Patches;
use radicle::cob::{thread, ActorId, Tag};
use radicle::identity::Id;
use radicle::node::NodeId;
-
use radicle::storage::{git::paths, ReadRepository, ReadStorage};
+
use radicle::storage::git::paths;
+
use radicle::storage::{ReadRepository, ReadStorage};
use radicle_surf::{Glob, Oid, Repository};

-
use crate::api::axum_extra::{Path, Query};
use crate::api::error::Error;
use crate::api::project::Info;
use crate::api::{self, Context, PaginationQuery};
+
use crate::axum_extra::{Path, Query};

const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";

modified radicle-httpd/src/api/v1/sessions.rs
@@ -11,10 +11,10 @@ use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

use crate::api::auth::{self, AuthState, Session};
-
use crate::api::axum_extra::Path;
use crate::api::error::Error;
use crate::api::json;
use crate::api::Context;
+
use crate::axum_extra::Path;

pub fn router(ctx: Context) -> Router {
    Router::new()
added radicle-httpd/src/axum_extra.rs
@@ -0,0 +1,89 @@
+
use axum::extract::path::ErrorKind;
+
use axum::extract::rejection::{PathRejection, QueryRejection};
+
use axum::extract::FromRequestParts;
+
use axum::http::request::Parts;
+
use axum::http::StatusCode;
+
use axum::{async_trait, Json};
+

+
use serde::de::DeserializeOwned;
+
use serde::Serialize;
+

+
pub struct Path<T>(pub T);
+

+
#[async_trait]
+
impl<S, T> FromRequestParts<S> for Path<T>
+
where
+
    T: DeserializeOwned + Send,
+
    S: Send + Sync,
+
{
+
    type Rejection = (StatusCode, axum::Json<Error>);
+

+
    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
+
        match axum::extract::Path::<T>::from_request_parts(req, state).await {
+
            Ok(value) => Ok(Self(value.0)),
+
            Err(rejection) => {
+
                let status = StatusCode::BAD_REQUEST;
+
                let body = match rejection {
+
                    PathRejection::FailedToDeserializePathParams(inner) => {
+
                        let kind = inner.into_kind();
+
                        match &kind {
+
                            ErrorKind::Message(msg) => Json(Error {
+
                                success: false,
+
                                error: msg.to_string(),
+
                            }),
+
                            _ => Json(Error {
+
                                success: false,
+
                                error: kind.to_string(),
+
                            }),
+
                        }
+
                    }
+
                    _ => Json(Error {
+
                        success: false,
+
                        error: format!("{rejection}"),
+
                    }),
+
                };
+

+
                Err((status, body))
+
            }
+
        }
+
    }
+
}
+

+
#[derive(Default)]
+
pub struct Query<T>(pub T);
+

+
#[async_trait]
+
impl<S, T> FromRequestParts<S> for Query<T>
+
where
+
    T: DeserializeOwned + Send,
+
    S: Send + Sync,
+
{
+
    type Rejection = (StatusCode, axum::Json<Error>);
+

+
    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
+
        match axum::extract::Query::<T>::from_request_parts(req, state).await {
+
            Ok(value) => Ok(Self(value.0)),
+
            Err(rejection) => {
+
                let status = StatusCode::BAD_REQUEST;
+
                let body = match rejection {
+
                    QueryRejection::FailedToDeserializeQueryString(inner) => Json(Error {
+
                        success: false,
+
                        error: inner.to_string(),
+
                    }),
+
                    _ => Json(Error {
+
                        success: false,
+
                        error: format!("{rejection}"),
+
                    }),
+
                };
+

+
                Err((status, body))
+
            }
+
        }
+
    }
+
}
+

+
#[derive(Serialize)]
+
pub struct Error {
+
    success: bool,
+
    error: String,
+
}
modified radicle-httpd/src/error.rs
@@ -31,6 +31,10 @@ pub enum Error {
    /// HeaderValue error.
    #[error(transparent)]
    InvalidHeaderValue(#[from] axum::http::header::InvalidHeaderValue),
+

+
    /// Surf error.
+
    #[error(transparent)]
+
    Surf(#[from] radicle_surf::Error),
}

impl Error {
modified radicle-httpd/src/lib.rs
@@ -16,7 +16,9 @@ use tower_http::trace::TraceLayer;
use tracing::Span;

mod api;
+
mod axum_extra;
mod git;
+
mod raw;
#[cfg(test)]
mod test;

@@ -41,13 +43,15 @@ pub async fn run(options: Options) -> anyhow::Result<()> {

    let ctx = api::Context::new(profile.clone());
    let api_router = api::router(ctx);
-
    let git_router = git::router(profile);
+
    let git_router = git::router(profile.clone());
+
    let raw_router = raw::router(profile);

    tracing::info!("listening on http://{}", options.listen);

    let app = Router::new()
        .merge(git_router)
        .nest("/api", api_router)
+
        .nest("/raw", raw_router)
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(|request: &Request<Body>| {
added radicle-httpd/src/raw.rs
@@ -0,0 +1,59 @@
+
use std::sync::Arc;
+

+
use axum::extract::State;
+
use axum::http::header;
+
use axum::response::IntoResponse;
+
use axum::routing::get;
+
use axum::Router;
+
use hyper::HeaderMap;
+

+
use radicle::prelude::Id;
+
use radicle::profile::Profile;
+
use radicle::storage::git::paths;
+
use radicle_surf::{Oid, Repository};
+

+
use crate::axum_extra::Path;
+
use crate::error::Error;
+

+
pub fn router(profile: Arc<Profile>) -> Router {
+
    Router::new()
+
        .route("/:project/:sha/*path", get(file_handler))
+
        .with_state(profile)
+
}
+

+
async fn file_handler(
+
    Path((project, sha, path)): Path<(Id, Oid, String)>,
+
    State(profile): State<Arc<Profile>>,
+
) -> impl IntoResponse {
+
    let storage = &profile.storage;
+
    let repo = Repository::open(paths::repository(storage, &project))?;
+
    let blob = repo.blob(sha, &path)?;
+

+
    let mut response_headers = HeaderMap::new();
+
    response_headers.insert(header::CONTENT_TYPE, "text; charset=utf-8".parse().unwrap());
+

+
    Ok::<_, Error>((response_headers, blob.content().to_owned()))
+
}
+

+
#[cfg(test)]
+
mod routes {
+
    use axum::http::StatusCode;
+

+
    use crate::test::{self, get, HEAD};
+

+
    #[tokio::test]
+
    async fn test_file_handler() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let ctx = test::seed(tmp.path());
+
        let app = super::router(ctx.profile().to_owned());
+

+
        let response = get(
+
            &app,
+
            format!("/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/{HEAD}/dir1/README"),
+
        )
+
        .await;
+

+
        assert_eq!(response.status(), StatusCode::OK);
+
        assert_eq!(response.body().await, "Hello World from dir1!\n");
+
    }
+
}
modified radicle-httpd/src/test.rs
@@ -4,7 +4,7 @@ use std::str::FromStr;
use std::sync::Arc;
use std::{env, fs};

-
use axum::body::Body;
+
use axum::body::{Body, Bytes};
use axum::http::{Method, Request};
use axum::Router;
use serde_json::Value;
@@ -235,4 +235,8 @@ impl Response {
    pub fn status(&self) -> axum::http::StatusCode {
        self.0.status()
    }
+

+
    pub async fn body(self) -> Bytes {
+
        hyper::body::to_bytes(self.0.into_body()).await.unwrap()
+
    }
}