Skip to content

Commit

Permalink
chore: Write car-mirror-axum documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
matheus23 committed Mar 4, 2024
1 parent c9dfd64 commit 19198b0
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 34 deletions.
114 changes: 100 additions & 14 deletions car-mirror-axum/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,120 @@ use axum::{
};
use std::fmt::Display;

/// TODO(matheus23): docs
/// A basic anyhow error type wrapper that returns
/// internal server errors if something goes wrong.
#[derive(Debug)]
pub struct AppError(anyhow::Error);
pub struct AppError {
status_code: StatusCode,
error_msg: String,
}

impl Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
self.error_msg.fmt(f)
}
}

impl AppError {
/// Construct a new error from a status code and an error message
pub fn new(status_code: StatusCode, msg: impl ToString) -> Self {
Self {
status_code,
error_msg: msg.to_string(),
}
}
}

/// TODO(matheus23): docs
pub type AppResult<T> = Result<T, AppError>;
/// Helper type alias that defaults the error type to `AppError`
pub type AppResult<T, E = AppError> = Result<T, E>;

impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
(self.status_code, self.error_msg).into_response()
}
}

impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
Self::from(&err)
}
}

impl From<&anyhow::Error> for AppError {
fn from(err: &anyhow::Error) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
format!("Something went wrong: {}", err),
)
.into_response()
}
}

impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
impl From<car_mirror::Error> for AppError {
fn from(err: car_mirror::Error) -> Self {
Self::from(&err)
}
}

impl From<&car_mirror::Error> for AppError {
fn from(err: &car_mirror::Error) -> Self {
use car_mirror::Error;
match err {
Error::TooManyBytes { .. } => Self::new(StatusCode::PAYLOAD_TOO_LARGE, err),
Error::BlockSizeExceeded { .. } => Self::new(StatusCode::PAYLOAD_TOO_LARGE, err),
Error::UnsupportedCodec { .. } => Self::new(StatusCode::BAD_REQUEST, err),
Error::UnsupportedHashCode { .. } => Self::new(StatusCode::BAD_REQUEST, err),
Error::BlockStoreError(err) => Self::from(err),
Error::ParsingError(_) => Self::new(StatusCode::UNPROCESSABLE_ENTITY, err),
Error::IncrementalVerificationError(_) => Self::new(StatusCode::BAD_REQUEST, err),
Error::CarFileError(_) => Self::new(StatusCode::BAD_REQUEST, err),
}
}
}

impl From<wnfs_common::BlockStoreError> for AppError {
fn from(err: wnfs_common::BlockStoreError) -> Self {
Self::from(&err)
}
}

impl From<&wnfs_common::BlockStoreError> for AppError {
fn from(err: &wnfs_common::BlockStoreError) -> Self {
use wnfs_common::BlockStoreError;
match err {
BlockStoreError::MaximumBlockSizeExceeded(_) => {
Self::new(StatusCode::PAYLOAD_TOO_LARGE, err)
}
BlockStoreError::CIDNotFound(_) => Self::new(StatusCode::NOT_FOUND, err),
BlockStoreError::CIDError(_) => Self::new(StatusCode::INTERNAL_SERVER_ERROR, err),
BlockStoreError::Custom(_) => Self::new(StatusCode::INTERNAL_SERVER_ERROR, err),
}
}
}

impl From<libipld::cid::Error> for AppError {
fn from(err: libipld::cid::Error) -> Self {
Self::from(&err)
}
}

impl From<&libipld::cid::Error> for AppError {
fn from(err: &libipld::cid::Error) -> Self {
Self::new(StatusCode::BAD_REQUEST, err)
}
}

impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
if let Some(err) = err.get_ref() {
if let Some(err) = err.downcast_ref::<car_mirror::Error>() {
return Self::from(err);
}

if let Some(err) = err.downcast_ref::<wnfs_common::BlockStoreError>() {
return Self::from(err);
}
}

Self::new(StatusCode::INTERNAL_SERVER_ERROR, err)
}
}
22 changes: 12 additions & 10 deletions car-mirror-axum/src/extract/dag_cbor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Axum extractor for DagCbor
//! Axum extractor that serializes and deserializes DagCbor data using serde

use anyhow::Result;
use axum::{
Expand All @@ -14,30 +14,32 @@ use serde::{de::DeserializeOwned, Serialize};
use serde_ipld_dagcbor::DecodeError;
use std::{convert::Infallible, fmt::Debug};

/// TODO(matheus23): docs
/// Newtype wrapper around dag-cbor (de-)serializable data
#[derive(Debug, Clone)]
pub struct DagCbor<M>(pub M);

/// TODO(matheus23): docs
/// Errors that can occur during dag-cbor deserialization
#[derive(Debug, thiserror::Error)]
pub enum DagCborRejection {
/// TODO(matheus23): docs
#[error("Missing Content-Type header on request, expected application/json or application/vnd.ipld.dag-cbor, but got nothing")]
/// When the Content-Type header is missing
#[error("Missing Content-Type header on request, expected application/vnd.ipld.dag-cbor, but got nothing")]
MissingContentType,

/// TODO(matheus23): docs
/// When a Content-Type header was set, but unexpected.
#[error("Incorrect mime type, expected application/vnd.ipld.dag-cbor, but got {0}")]
UnexpectedContentType(mime::Mime),

/// TODO(matheus23): docs
#[error("Failed parsing Content-Type header as mime type, expected application/json or application/vnd.ipld.dag-cbor")]
/// When the Content-Type header was set, but couldn't be parsed as a mime type
#[error(
"Failed parsing Content-Type header as mime type, expected application/vnd.ipld.dag-cbor"
)]
FailedToParseMime,

/// TODO(matheus23): docs
/// When the request body couldn't be loaded before deserialization
#[error("Unable to buffer the request body, perhaps it exceeded the 2MB limit")]
FailedParsingRequestBytes,

/// TODO(matheus23): docs
/// When dag-cbor deserialization into the target type fails
#[error("Failed decoding dag-cbor: {0}")]
FailedDecoding(#[from] DecodeError<Infallible>),
}
Expand Down
14 changes: 13 additions & 1 deletion car-mirror-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@
#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)]
#![deny(unreachable_pub)]

//! # car-mirror-axum TODO docs
//! # car-mirror-axum
//!
//! This crate exposes a very basic car mirror server.
//! It accepts `GET /dag/pull/:cid`, `POST /dag/pull/:cid` and `POST /dag/push/:cid` requests
//! with streaming car file request and response types, respectively.
//!
//! It is roughly based on the [car-mirror-http specification](https://github.com/wnfs-wg/car-mirror-http-spec).
//!
//! It also exposes some utilities with which it's easier to build a car-mirror axum server.
//!
//! At the moment, it's recommended to only make use of the `extract` module, and mostly
//! use the rest of the library for tests or treat the rest of the code as an example
//! to copy code from for actual production use.

mod error;
pub mod extract;
Expand Down
46 changes: 37 additions & 9 deletions car-mirror-axum/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,32 @@ use tower_http::{
};
use wnfs_common::BlockStore;

/// TODO(matheus23): docs
/// Serve a basic car mirror server that serves the routes from `app`
/// with given blockstore at `127.0.0.1:3344`.
///
/// When the server is ready to accept connections, it will print a
/// message to the console: "Listening on 127.0.0.1.3344".
///
/// This is a simple function mostly useful for tests. If you want to
/// customize its function, copy its source and create a modified copy
/// as needed.
///
/// This is not intended for production usage, for multiple reasons:
/// - There is no rate-limiting on the requests, so such a service would
/// be susceptible to DoS attacks.
/// - The `push` route should usually only be available behind
/// authorization or perhaps be heavily rate-limited, otherwise it
/// can cause unbounded memory or disk growth remotely.
pub async fn serve(store: impl BlockStore + Clone + 'static) -> Result<()> {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3344").await?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:3344").await?;
let addr = listener.local_addr()?;
println!("Listening on {addr}");
axum::serve(listener, app(store)).await?;
Ok(())
}

/// TODO(matheus23): docs
/// This will serve the routes from `dag_router` nested under `/dag`, but with
/// tracing and cors headers.
pub fn app(store: impl BlockStore + Clone + 'static) -> Router {
let cors = CorsLayer::new()
.allow_methods(Any)
Expand All @@ -47,7 +63,13 @@ pub fn app(store: impl BlockStore + Clone + 'static) -> Router {
.fallback(not_found)
}

/// TODO(matheus23): docs
/// Returns a router for car mirror requests with the
/// given blockstore as well as a new 10MB cache as state.
///
/// This serves following routes:
/// - `GET /pull/:cid` for pull requests (GET is generally not recommended here)
/// - `POST /pull/:cid` for pull requests
/// - `POST /push/:cid` for push requests
pub fn dag_router(store: impl BlockStore + Clone + 'static) -> Router {
Router::new()
.route("/pull/:cid", get(car_mirror_pull))
Expand All @@ -56,15 +78,18 @@ pub fn dag_router(store: impl BlockStore + Clone + 'static) -> Router {
.with_state(ServerState::new(store))
}

/// TODO(matheus23): docs
/// The server state used for a basic car mirror server.
///
/// Stores a block store and a car mirror operations cache.
#[derive(Debug, Clone)]
pub struct ServerState<B: BlockStore + Clone + 'static> {
store: B,
cache: InMemoryCache,
}

impl<B: BlockStore + Clone + 'static> ServerState<B> {
/// TODO(matheus23): docs
/// Initialize the server state with given blockstore and
/// a roughly 10MB car mirror operations cache.
pub fn new(store: B) -> ServerState<B> {
Self {
store,
Expand All @@ -73,7 +98,9 @@ impl<B: BlockStore + Clone + 'static> ServerState<B> {
}
}

/// TODO(matheus23): docs
/// Handle a POST request for car mirror pushes.
///
/// This will consume the incoming body as a car file stream.
#[tracing::instrument(skip(state), err, ret)]
pub async fn car_mirror_push<B: BlockStore + Clone + 'static>(
State(state): State<ServerState<B>>,
Expand Down Expand Up @@ -119,7 +146,9 @@ where {
}
}

/// TODO(matheus23): docs
/// Handle an incoming GET or POST request for a car mirror pull.
///
/// The response body will contain a stream of car file chunks.
#[tracing::instrument(skip(state), err, ret)]
pub async fn car_mirror_pull<B: BlockStore + Clone + 'static>(
State(state): State<ServerState<B>>,
Expand Down Expand Up @@ -147,7 +176,6 @@ pub async fn car_mirror_pull<B: BlockStore + Clone + 'static>(
Ok((StatusCode::OK, Body::from_stream(car_chunks)))
}

/// TODO(matheus23): docs
#[axum_macros::debug_handler]
async fn not_found() -> (StatusCode, &'static str) {
tracing::info!("Hit 404");
Expand Down

0 comments on commit 19198b0

Please sign in to comment.