Improve IPFS errors

This commit is contained in:
Timo Ley 2022-01-05 23:46:19 +01:00
parent fcce7d07c8
commit 8e5387600a
7 changed files with 126 additions and 93 deletions

View file

@ -5,42 +5,35 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use reqwest::StatusCode; use reqwest::StatusCode;
use thiserror::Error;
pub struct Error(StatusCode); use crate::ipfs::error::IPFSError;
impl Error { #[derive(Error, Debug)]
pub fn new() -> Self { pub enum CDNError {
Error(StatusCode::INTERNAL_SERVER_ERROR) #[error("SQL error: {0}")]
} SQL(#[from] sqlx::Error),
#[error("IPFS error: {0}")]
IPFS(#[from] IPFSError),
#[error("Decode error: {0}")]
Decode(#[from] FromUtf8Error),
#[error("Internal server error")]
Internal,
} }
impl IntoResponse for Error { impl IntoResponse for CDNError {
type Body = Empty<Bytes>; type Body = Empty<Bytes>;
type BodyError = Infallible; type BodyError = Infallible;
fn into_response(self) -> axum::http::Response<Self::Body> { fn into_response(self) -> axum::http::Response<Self::Body> {
self.0.into_response() let status = match self {
} CDNError::SQL(err) => match err {
} sqlx::Error::RowNotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
impl From<sqlx::Error> for Error { },
fn from(err: sqlx::Error) -> Self { _ => StatusCode::INTERNAL_SERVER_ERROR,
match err { };
sqlx::Error::RowNotFound => Error(StatusCode::NOT_FOUND), status.into_response()
_ => Error(StatusCode::INTERNAL_SERVER_ERROR),
}
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Error(err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
}
}
impl From<FromUtf8Error> for Error {
fn from(_: FromUtf8Error) -> Self {
Error(StatusCode::INTERNAL_SERVER_ERROR)
} }
} }

View file

@ -17,7 +17,7 @@ use sqlx::MySqlPool;
use crate::config::ConfVars; use crate::config::ConfVars;
use self::{ use self::{
error::Error, error::CDNError,
templates::{DirTemplate, HtmlTemplate}, templates::{DirTemplate, HtmlTemplate},
}; };
@ -37,58 +37,48 @@ async fn image(
Path((user, filename)): Path<(String, String)>, Path((user, filename)): Path<(String, String)>,
Extension(db_pool): Extension<MySqlPool>, Extension(db_pool): Extension<MySqlPool>,
Extension(vars): Extension<ConfVars>, Extension(vars): Extension<ConfVars>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, CDNError> {
let filename = urlencoding::decode(&filename)?.into_owned(); let filename = urlencoding::decode(&filename)?.into_owned();
let cid = sql::get_cid(user, filename.clone(), &db_pool).await?; let cid = sql::get_cid(user, filename.clone(), &db_pool).await?;
let ipfs = vars.ipfs_client()?; let ipfs = vars.ipfs_client()?;
let res = ipfs.cat(cid).await?; let res = ipfs.cat(cid).await?;
let clength = res let clength = res
.headers() .headers()
.get(HeaderName::from_static("x-content-length")); .get(HeaderName::from_static("x-content-length"))
match clength { .ok_or(CDNError::Internal)?;
Some(h) => {
let mut headers = HeaderMap::new();
let ctype =
ContentType::from(new_mime_guess::from_path(filename).first_or_octet_stream());
headers.typed_insert(ctype);
headers.insert(CONTENT_LENGTH, h.clone());
Ok(( let mut headers = HeaderMap::new();
StatusCode::OK, let ctype = ContentType::from(new_mime_guess::from_path(filename).first_or_octet_stream());
headers, headers.typed_insert(ctype);
Body::wrap_stream(res.bytes_stream()), headers.insert(CONTENT_LENGTH, clength.clone());
))
} Ok((
None => Err(Error::new()), StatusCode::OK,
} headers,
Body::wrap_stream(res.bytes_stream()),
))
} }
async fn users( async fn users(
Extension(db_pool): Extension<MySqlPool>, Extension(db_pool): Extension<MySqlPool>,
Extension(vars): Extension<ConfVars>, Extension(vars): Extension<ConfVars>,
) -> Result<impl IntoResponse, StatusCode> { ) -> Result<impl IntoResponse, CDNError> {
let q = sql::get_users(&db_pool).await; let users = sql::get_users(&db_pool).await?;
match q { Ok(HtmlTemplate(DirTemplate {
Ok(users) => Ok(HtmlTemplate(DirTemplate { entries: users,
entries: users, prefix: vars.cdn,
prefix: vars.cdn, suffix: "/".to_string(),
suffix: "/".to_string(), }))
})),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
} }
async fn memes( async fn memes(
Path(user): Path<String>, Path(user): Path<String>,
Extension(db_pool): Extension<MySqlPool>, Extension(db_pool): Extension<MySqlPool>,
) -> Result<impl IntoResponse, StatusCode> { ) -> Result<impl IntoResponse, CDNError> {
let q = sql::get_memes(user, &db_pool).await; let memes = sql::get_memes(user, &db_pool).await?;
match q { Ok(HtmlTemplate(DirTemplate {
Ok(memes) => Ok(HtmlTemplate(DirTemplate { entries: memes,
entries: memes, prefix: ".".to_string(),
prefix: ".".to_string(), suffix: "".to_string(),
suffix: "".to_string(), }))
})),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
} }

View file

@ -1,6 +1,5 @@
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum JMError { pub enum JMError {
#[error("File read error: {0}")] #[error("File read error: {0}")]
@ -11,4 +10,4 @@ pub enum JMError {
Database(#[from] sqlx::Error), Database(#[from] sqlx::Error),
#[error("Axum error: {0}")] #[error("Axum error: {0}")]
Axum(#[from] hyper::Error), Axum(#[from] hyper::Error),
} }

10
src/ipfs/error.rs Normal file
View file

@ -0,0 +1,10 @@
use thiserror::Error;
use url::ParseError;
#[derive(Error, Debug)]
pub enum IPFSError {
#[error("Reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("URL parse error: {0}")]
URL(#[from] ParseError),
}

View file

@ -1,12 +1,20 @@
use reqwest::{Client, Response, Result, Url}; use std::time::Duration;
use axum::{body::Bytes, http::request};
use reqwest::{
multipart::{Form, Part},
Body, Client, Response, Url,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::config::ConfVars; use crate::config::ConfVars;
use self::error::IPFSError;
pub(crate) mod error;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct AddResponse { pub struct IPFSFile {
#[serde(rename = "Bytes")]
pub bytes: String,
#[serde(rename = "Hash")] #[serde(rename = "Hash")]
pub hash: String, pub hash: String,
#[serde(rename = "Name")] #[serde(rename = "Name")]
@ -20,46 +28,76 @@ pub struct CatQuery {
pub arg: String, pub arg: String,
} }
#[derive(Serialize)]
pub struct AddQuery {
pub pin: bool,
}
#[derive(Serialize)]
pub struct PinQuery {
pub arg: String,
}
pub struct IpfsClient { pub struct IpfsClient {
url: Url, url: Url,
client: Client, client: Client,
} }
impl IpfsClient { impl IpfsClient {
pub async fn cat(&self, cid: String) -> Result<Response, IPFSError> {
pub fn cat_url(&self) -> Url { let request = self
self.url.join("/api/v0/cat").expect("Something went wrong with the IPFS URL") .client
.post(self.url.join("/api/v0/cat")?)
.query(&CatQuery::new(cid));
Ok(request.send().await?)
} }
pub fn add_url(&self) -> Url { pub async fn add(&self, file: Bytes, filename: String) -> Result<IPFSFile, IPFSError> {
self.url.join("/api/v0/add").expect("Something went wrong with the IPFS URL") let request = self
.client
.post(self.url.join("/api/v0/add")?)
.query(&AddQuery::new(false))
.multipart(Form::new().part("file", Part::stream(file).file_name(filename)));
let response = request.send().await?;
let res: IPFSFile = response.json().await?;
Ok(res)
} }
pub async fn cat(&self, cid: String) -> Result<Response> { pub async fn pin(&self, cid: String) -> Result<(), IPFSError> {
let request = self.client.post(self.cat_url()).query(&CatQuery::new(cid)); let request = self
request.send().await .client
.post(self.url.join("/api/v0/pin/add")?)
.query(&PinQuery::new(cid))
.timeout(Duration::from_secs(60));
let response = request.send().await?;
Ok(())
} }
} }
impl CatQuery { impl CatQuery {
pub fn new(cid: String) -> Self { pub fn new(cid: String) -> Self {
Self { Self { arg: cid }
arg: cid,
}
} }
}
impl AddQuery {
pub fn new(pin: bool) -> Self {
Self { pin }
}
}
impl PinQuery {
pub fn new(cid: String) -> Self {
Self { arg: cid }
}
} }
impl ConfVars { impl ConfVars {
pub fn ipfs_client(&self) -> Result<IpfsClient, IPFSError> {
pub fn ipfs_client(&self) -> Result<IpfsClient> { let client = reqwest::ClientBuilder::new().user_agent("curl").build()?;
let client =reqwest::ClientBuilder::new().user_agent("curl").build()?;
Ok(IpfsClient { Ok(IpfsClient {
url: self.ipfs_api.clone(), url: self.ipfs_api.clone(),
client, client,
}) })
} }
}
}

View file

@ -12,9 +12,9 @@ use tower_http::{add_extension::AddExtensionLayer, set_header::SetResponseHeader
mod cdn; mod cdn;
mod config; mod config;
mod error;
mod ipfs; mod ipfs;
mod v1; mod v1;
mod error;
#[derive(StructOpt)] #[derive(StructOpt)]
struct Opt { struct Opt {
@ -33,8 +33,7 @@ async fn main() -> Result<(), JMError> {
let config = std::fs::read(&opt.config)?; let config = std::fs::read(&opt.config)?;
let config = toml::from_slice::<Config>(&config)?; let config = toml::from_slice::<Config>(&config)?;
let db_pool = MySqlPool::new(&config.database) let db_pool = MySqlPool::new(&config.database).await?;
.await?;
let app = Router::new() let app = Router::new()
.nest("/api/v1", v1::routes()) .nest("/api/v1", v1::routes())

View file

@ -10,6 +10,7 @@ use reqwest::StatusCode;
use thiserror::Error; use thiserror::Error;
use super::models::ErrorResponse; use super::models::ErrorResponse;
use crate::ipfs::error::IPFSError;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum APIError { pub enum APIError {
@ -19,6 +20,8 @@ pub enum APIError {
Multipart(#[from] MultipartError), Multipart(#[from] MultipartError),
#[error("Bad request: {0}")] #[error("Bad request: {0}")]
BadRequest(String), BadRequest(String),
#[error("IPFS error: {0}")]
IPFS(#[from] IPFSError),
} }
impl ErrorResponse { impl ErrorResponse {
@ -44,6 +47,7 @@ impl IntoResponse for APIError {
}, },
APIError::Multipart(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None), APIError::Multipart(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)), APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)),
APIError::IPFS(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
}; };
let status = res.status.clone(); let status = res.status.clone();
(status, Json(res)).into_response() (status, Json(res)).into_response()