diff --git a/Cargo.lock b/Cargo.lock index af1e4af..dd1fa94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "RustyXML" @@ -1442,7 +1442,7 @@ dependencies = [ "rustc_version", "smallvec", "tagptr", - "thiserror", + "thiserror 1.0.63", "triomphe", "uuid", ] @@ -1640,9 +1640,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1788,7 +1788,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2198,7 +2198,7 @@ checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" dependencies = [ "log", "serde", - "thiserror", + "thiserror 1.0.63", "xml-rs", ] @@ -2233,7 +2233,7 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2355,6 +2355,7 @@ dependencies = [ "serde-xml-rs", "serde_json", "sha2 0.10.8", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -2382,9 +2383,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "2.0.74" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2458,7 +2459,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2472,6 +2482,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" diff --git a/Cargo.toml b/Cargo.toml index 035206b..060bfec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,4 @@ sha2 = "0.10.6" hex = "0.4.3" hmac = "0.12" actix-http = "^3" +thiserror = "2.0.12" diff --git a/src/apis/mod.rs b/src/apis/mod.rs index 59465b9..349ca0c 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,6 @@ pub mod source; -use crate::{backends::common::Repository, utils::auth::UserIdentity}; +use crate::{backends::common::Repository, utils::auth::UserIdentity, utils::errors::BackendError}; use async_trait::async_trait; pub struct Account { @@ -21,7 +21,7 @@ pub trait API { &self, account_id: &String, repository_id: &String, - ) -> Result, ()>; + ) -> Result, BackendError>; async fn get_account( &self, diff --git a/src/apis/source.rs b/src/apis/source.rs index 5fee673..02ea45d 100644 --- a/src/apis/source.rs +++ b/src/apis/source.rs @@ -2,8 +2,9 @@ use super::{Account, API}; use crate::backends::azure::AzureRepository; use crate::backends::common::Repository; use crate::backends::s3::S3Repository; +use crate::utils::api::process_json_response; use crate::utils::auth::UserIdentity; -use crate::utils::errors::{APIError, InternalServerError, RepositoryNotFoundError}; +use crate::utils::errors::{APIError, BackendError, InternalServerError}; use async_trait::async_trait; use moka::future::Cache; use rusoto_core::Region; @@ -13,7 +14,6 @@ use std::collections::HashMap; use std::env; use std::sync::Arc; use std::time::Duration; - #[derive(Clone)] pub struct SourceAPI { pub endpoint: String, @@ -125,133 +125,111 @@ impl API for SourceAPI { &self, account_id: &String, repository_id: &String, - ) -> Result, ()> { - match self + ) -> Result, BackendError> { + let repository = self .get_repository_record(&account_id, &repository_id) - .await + .await?; + + let repository_data = match repository + .data + .mirrors + .get(repository.data.primary_mirror.as_str()) { - Ok(repository) => { - match repository - .data - .mirrors - .get(repository.data.primary_mirror.as_str()) - { - Some(repository_data) => { - let data_connection_id = repository_data.data_connection_id.clone(); - match self.get_data_connection(&data_connection_id).await { - Ok(data_connection) => { - if data_connection.details.provider == "s3" { - let region: Region; - - if data_connection.authentication.clone().unwrap().auth_type - == "s3_local" - { - region = Region::Custom { - name: data_connection - .details - .region - .clone() - .unwrap_or("us-west-2".to_string()), - endpoint: format!("http://localhost:5050"), - }; - } else { - region = Region::Custom { - name: data_connection - .details - .region - .clone() - .unwrap_or("us-east-1".to_string()), - endpoint: format!( - "https://s3.{}.amazonaws.com", - data_connection - .details - .region - .clone() - .unwrap_or("us-east-1".to_string()) - ), - }; - } - - let bucket: String = - data_connection.details.bucket.clone().unwrap_or_default(); - let base_prefix: String = data_connection - .details - .base_prefix - .clone() - .unwrap_or_default(); - - let prefix = - format!("{}{}", base_prefix, repository_data.prefix); - - let prefix = if prefix.ends_with('/') { - prefix[..prefix.len() - 1].to_string() - } else { - prefix - }; - - Ok(Box::new(S3Repository { - account_id: account_id.to_string(), - repository_id: repository_id.to_string(), - region, - bucket, - base_prefix: prefix, - auth_method: data_connection - .authentication - .clone() - .unwrap() - .auth_type, - access_key_id: data_connection - .authentication - .clone() - .unwrap() - .access_key_id, - secret_access_key: data_connection - .authentication - .clone() - .unwrap() - .secret_access_key, - })) - } else if data_connection.details.provider == "az" { - let account_name: String = data_connection - .details - .account_name - .clone() - .unwrap_or_default(); - - let container_name: String = data_connection - .details - .container_name - .clone() - .unwrap_or_default(); - let base_prefix: String = data_connection - .details - .base_prefix - .clone() - .unwrap_or_default(); - - Ok(Box::new(AzureRepository { - account_id: account_id.to_string(), - repository_id: repository_id.to_string(), - account_name, - container_name, - base_prefix: format!( - "{}{}", - base_prefix, repository_data.prefix - ), - })) - } else { - Err(()) - } - } - Err(_) => return Err(()), - } - } - None => { - return Err(()); - } + Some(repository_data) => repository_data, + None => { + return Err(BackendError::SourceRepositoryMissingPrimaryMirror); + } + }; + + let data_connection_id = repository_data.data_connection_id.clone(); + let data_connection = self.get_data_connection(&data_connection_id).await?; + + match data_connection.details.provider.as_str() { + "s3" => { + let region: Region; + + if data_connection.authentication.clone().unwrap().auth_type == "s3_local" { + region = Region::Custom { + name: data_connection + .details + .region + .clone() + .unwrap_or("us-west-2".to_string()), + endpoint: format!("http://localhost:5050"), + }; + } else { + region = Region::Custom { + name: data_connection + .details + .region + .clone() + .unwrap_or("us-east-1".to_string()), + endpoint: format!( + "https://s3.{}.amazonaws.com", + data_connection + .details + .region + .clone() + .unwrap_or("us-east-1".to_string()) + ), + }; } + + let bucket: String = data_connection.details.bucket.clone().unwrap_or_default(); + let base_prefix: String = data_connection + .details + .base_prefix + .clone() + .unwrap_or_default(); + + let mut prefix = format!("{}{}", base_prefix, repository_data.prefix); + if prefix.ends_with('/') { + prefix = prefix[..prefix.len() - 1].to_string(); + }; + + let auth = data_connection.authentication.clone().unwrap(); + + Ok(Box::new(S3Repository { + account_id: account_id.to_string(), + repository_id: repository_id.to_string(), + region, + bucket, + base_prefix: prefix, + auth_method: auth.auth_type, + access_key_id: auth.access_key_id, + secret_access_key: auth.secret_access_key, + })) } - Err(_) => Err(()), + "az" => { + let account_name: String = data_connection + .details + .account_name + .clone() + .unwrap_or_default(); + + let container_name: String = data_connection + .details + .container_name + .clone() + .unwrap_or_default(); + + let base_prefix: String = data_connection + .details + .base_prefix + .clone() + .unwrap_or_default(); + + Ok(Box::new(AzureRepository { + account_id: account_id.to_string(), + repository_id: repository_id.to_string(), + account_name, + container_name, + base_prefix: format!("{}{}", base_prefix, repository_data.prefix), + })) + } + err => Err(BackendError::UnexpectedDataConnectionProvider { + provider: err.to_string(), + }), } } @@ -350,7 +328,7 @@ impl SourceAPI { &self, account_id: &String, repository_id: &String, - ) -> Result> { + ) -> Result { // Try to get the cached value let cache_key = format!("{}/{}", account_id, repository_id); @@ -374,7 +352,7 @@ impl SourceAPI { async fn fetch_data_connection( &self, data_connection_id: &String, - ) -> Result> { + ) -> Result { let source_key = env::var("SOURCE_KEY").unwrap(); let client = reqwest::Client::new(); let mut headers = reqwest::header::HeaderMap::new(); @@ -382,39 +360,23 @@ impl SourceAPI { reqwest::header::AUTHORIZATION, reqwest::header::HeaderValue::from_str(&source_key).unwrap(), ); - match client + + let response = client .get(format!( "{}/api/v1/data-connections/{}", self.endpoint, data_connection_id )) .headers(headers) .send() + .await?; + process_json_response::(response, BackendError::DataConnectionNotFound) .await - { - Ok(response) => match response.json::().await { - Ok(data_connection) => Ok(data_connection), - Err(_) => Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })), - }, - Err(error) => { - if error.status().is_some() && error.status().unwrap().as_u16() == 404 { - return Err(Box::new(InternalServerError { - message: "Data Connection Not Found".to_string(), - })); - } - - Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })) - } - } } async fn get_data_connection( &self, data_connection_id: &String, - ) -> Result> { + ) -> Result { // Try to get the cached value let cache_key = format!("{}", data_connection_id); @@ -527,32 +489,14 @@ impl SourceAPI { &self, account_id: &String, repository_id: &String, - ) -> Result> { - match reqwest::get(format!( + ) -> Result { + let response = reqwest::get(format!( "{}/api/v1/repositories/{}/{}", self.endpoint, account_id, repository_id )) - .await - { - Ok(response) => match response.json::().await { - Ok(repository) => Ok(repository), - Err(_) => Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })), - }, - Err(error) => { - if error.status().is_some() && error.status().unwrap().as_u16() == 404 { - return Err(Box::new(RepositoryNotFoundError { - account_id: account_id.to_string(), - repository_id: repository_id.to_string(), - })); - } - - Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })) - } - } + .await?; + process_json_response::(response, BackendError::RepositoryNotFound) + .await } pub async fn is_authorized( @@ -561,7 +505,7 @@ impl SourceAPI { account_id: &String, repository_id: &String, permission: RepositoryPermission, - ) -> Result> { + ) -> Result { let anon: bool; if user_identity.api_key.is_none() { anon = true; @@ -583,19 +527,16 @@ impl SourceAPI { } // If not in cache, fetch it - match self + let permissions = self .fetch_permission(user_identity.clone(), &account_id, &repository_id) - .await - { - Ok(permissions) => { - // Cache the successful result - self.permissions_cache - .insert(cache_key, permissions.clone()) - .await; - return Ok(permissions.contains(&permission)); - } - Err(e) => Err(e), - } + .await?; + + // Cache the successful result + self.permissions_cache + .insert(cache_key, permissions.clone()) + .await; + + Ok(permissions.contains(&permission)) } async fn fetch_permission( @@ -603,7 +544,7 @@ impl SourceAPI { user_identity: UserIdentity, account_id: &String, repository_id: &String, - ) -> Result, Box> { + ) -> Result, BackendError> { let client = reqwest::Client::new(); let source_api_url = env::var("SOURCE_API_URL").unwrap(); @@ -620,24 +561,19 @@ impl SourceAPI { ); } - match client + let response = client .get(format!( "{}/api/v1/repositories/{}/{}/permissions", source_api_url, account_id, repository_id )) .headers(headers) .send() - .await - { - Ok(response) => match response.json::>().await { - Ok(permissions) => Ok(permissions), - Err(_) => Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })), - }, - Err(_) => Err(Box::new(InternalServerError { - message: "Internal Server Error".to_string(), - })), - } + .await?; + + process_json_response::>( + response, + BackendError::RepositoryPermissionsNotFound, + ) + .await } } diff --git a/src/main.rs b/src/main.rs index 020877d..339b8b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,15 +17,17 @@ use bytes::Bytes; use core::num::NonZeroU32; use env_logger::Env; use futures_util::StreamExt; +use log::error; use quick_xml::se::to_string_with_root; use serde::Deserialize; use serde_xml_rs::from_str; use std::env; +use std::fmt::Debug; use std::pin::Pin; use std::str::from_utf8; use std::task::{Context, Poll}; use utils::auth::{LoadIdentity, UserIdentity}; - +use utils::errors::BackendError; const VERSION: &str = env!("CARGO_PKG_VERSION"); struct FakeBody { @@ -55,105 +57,93 @@ async fn get_object( req: HttpRequest, path: web::Path<(String, String, String)>, user_identity: web::ReqData, -) -> impl Responder { +) -> Result { let (account_id, repository_id, key) = path.into_inner(); let headers = req.headers(); - let mut range = None; let mut range_start = 0; let mut is_range_request = false; - if let Some(range_header) = headers.get(RANGE) { - if let Ok(r) = range_header.to_str() { - if let Some(bytes_range) = r.strip_prefix("bytes=") { - if let Some((start, end)) = bytes_range.split_once('-') { - if let Ok(s) = start.parse::() { - range_start = s; - if end.is_empty() || end.parse::().is_ok() { - range = Some(r.to_string()); - is_range_request = true; - } - } + let range = headers + .get(RANGE) + .and_then(|h| h.to_str().ok()) + .and_then(|r| r.strip_prefix("bytes=")) + .and_then(|bytes_range| bytes_range.split_once('-')) + .and_then(|(start, end)| { + start.parse::().ok().map(|s| { + range_start = s; + if end.is_empty() || end.parse::().is_ok() { + is_range_request = true; + Some(format!("bytes={}-{}", start, end)) + } else { + None } - } - } - } + }) + }) + .flatten(); - if let Ok(client) = api_client + let client = api_client .get_backend_client(&account_id, &repository_id) - .await - { - match api_client - .is_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Read, - ) - .await - { - Ok(authorized) => { - if !authorized { - return HttpResponse::Unauthorized().finish(); - } - } - Err(_) => return HttpResponse::InternalServerError().finish(), - } - - // Found the repository, now try to get the object - match client.get_object(key.clone(), range).await { - Ok(res) => { - let mut content_length = String::from("*"); - - // Remove this if statement to increase performance since it's making an extra request just to get the total content-length - // This is only needed for range requests and in theory, you can return a * in the Content-Range header to indicate that the content length is unknown - if is_range_request { - match client.head_object(key.clone()).await { - Ok(head_res) => { - content_length = head_res.content_length.to_string(); - } - Err(_) => {} - } - } - - let stream = res.body.map(|result| { - result - .map(web::Bytes::from) - .map_err(|e| ErrorInternalServerError(e.to_string())) - }); + .await?; + + let authorized = api_client + .is_authorized( + user_identity.into_inner(), + &account_id, + &repository_id, + RepositoryPermission::Read, + ) + .await?; + + if !authorized { + return Err(BackendError::UnauthorizationError); + } - let streaming_response = StreamingResponse::new(stream, res.content_length); - let mut response = if is_range_request { - HttpResponse::PartialContent() - } else { - HttpResponse::Ok() - }; + // Found the repository, now try to get the object + let res = client.get_object(key.clone(), range).await?; + + let mut content_length = String::from("*"); + // Remove this if statement to increase performance since it's making an extra request just to get the total content-length + // This is only needed for range requests and in theory, you can return a * in the Content-Range header to indicate that the content length is unknown + if is_range_request { + content_length = client + .head_object(key.clone()) + .await? + .content_length + .to_string(); + } - let mut response = response - .insert_header(("Content-Type", res.content_type)) - .insert_header(("Last-Modified", res.last_modified)) - .insert_header(("Content-Length", res.content_length.to_string())) - .insert_header(("ETag", res.etag)); - - if is_range_request { - response = response.insert_header(( - "Content-Range", - format!( - "bytes {}-{}/{}", - range_start, - range_start + res.content_length - 1, - content_length - ), - )); - } + let stream = res.body.map(|result| { + result + .map(web::Bytes::from) + .map_err(|e| ErrorInternalServerError(e.to_string())) + }); - return response.body(streaming_response); - } - Err(_) => HttpResponse::NotFound().finish(), - } + let streaming_response = StreamingResponse::new(stream, res.content_length); + let mut response = if is_range_request { + HttpResponse::PartialContent() } else { - // Could not find the repository - return HttpResponse::NotFound().finish(); + HttpResponse::Ok() + }; + + let mut response = response + .insert_header(("Content-Type", res.content_type)) + .insert_header(("Last-Modified", res.last_modified)) + .insert_header(("Content-Length", res.content_length.to_string())) + .insert_header(("ETag", res.etag)); + + if is_range_request { + response = response.insert_header(( + "Content-Range", + format!( + "bytes {}-{}/{}", + range_start, + range_start + res.content_length - 1, + content_length + ), + )); } + + Ok(response.body(streaming_response)) } #[derive(Debug, Deserialize)] diff --git a/src/utils/api.rs b/src/utils/api.rs new file mode 100644 index 0000000..0dcd833 --- /dev/null +++ b/src/utils/api.rs @@ -0,0 +1,43 @@ +use crate::utils::errors::BackendError; +use reqwest::{Response, StatusCode}; +use serde::de::DeserializeOwned; + +/// Process a response, handling both success and error cases +pub async fn process_json_response( + response: Response, + not_found_error: BackendError, +) -> Result { + let status = response.status(); + if status.is_success() { + response + .json::() + .await + .map_err(|err| BackendError::JsonParseError { + url: err + .url() + .map(|u| u.to_string()) + .unwrap_or("unknown".to_string()), + }) + } else if status == StatusCode::NOT_FOUND { + Err(not_found_error) + } else { + let url = response.url().to_string(); + let status = response.status().as_u16(); + let is_server_error = response.status().is_server_error(); + let message = response.text().await.unwrap_or("unknown".to_string()); + + if is_server_error { + Err(BackendError::ApiServerError { + url, + status, + message, + }) + } else { + Err(BackendError::ApiClientError { + url, + status, + message, + }) + } + } +} diff --git a/src/utils/errors.rs b/src/utils/errors.rs index 76dd246..a5d21f6 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -1,35 +1,95 @@ +use actix_web::error; +use actix_web::http::StatusCode; use actix_web::HttpResponse; +use log::error; +use reqwest::Error as ReqwestError; use serde::Serialize; use std::error::Error; use std::fmt; - -pub trait APIError: std::error::Error + Send + Sync { - fn to_response(&self) -> HttpResponse; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BackendError { + #[error("repository not found")] + RepositoryNotFound, + #[error("failed to fetch repository permissions")] + RepositoryPermissionsNotFound, + #[error("source repository missing primary mirror")] + SourceRepositoryMissingPrimaryMirror, + #[error("data connection not found")] + DataConnectionNotFound, + #[error("reqwest error (url {}, message {})", .0.url().map(|u| u.to_string()).unwrap_or("unknown".to_string()), .0.to_string())] + ReqwestError(#[from] ReqwestError), + #[error("Api threw a server error (url {}, status {}, message {})", .url, .status, .message)] + ApiServerError { + url: String, + status: u16, + message: String, + }, + #[error("Api threw a client error (url {}, status {}, message {})", .url, .status, .message)] + ApiClientError { + url: String, + status: u16, + message: String, + }, + #[error("Failed to parse JSON (url {})", .url)] + JsonParseError { url: String }, + #[error("Unexpected data connection provider (provider {})", .provider)] + UnexpectedDataConnectionProvider { provider: String }, + #[error("Unauthorized")] + UnauthorizationError, + #[error("Unexpected API error")] // TODO: remove this + UnexpectedApiError(String), } -#[derive(Serialize, Debug)] -pub struct RepositoryNotFoundError { - pub account_id: String, - pub repository_id: String, -} +impl error::ResponseError for BackendError { + fn error_response(&self) -> HttpResponse { + error!("Error: {}", self); + match self { + BackendError::RepositoryNotFound => HttpResponse::NotFound().finish(), + BackendError::SourceRepositoryMissingPrimaryMirror => HttpResponse::NotFound().finish(), + BackendError::DataConnectionNotFound => HttpResponse::NotFound().finish(), + BackendError::ReqwestError(_) => HttpResponse::BadGateway().finish(), + BackendError::ApiServerError { .. } => HttpResponse::BadGateway().finish(), + BackendError::ApiClientError { .. } => HttpResponse::BadGateway().finish(), + BackendError::JsonParseError { .. } => HttpResponse::InternalServerError().finish(), + BackendError::UnexpectedDataConnectionProvider { .. } => { + HttpResponse::InternalServerError().finish() + } + BackendError::RepositoryPermissionsNotFound => HttpResponse::BadGateway().finish(), + BackendError::UnauthorizationError => HttpResponse::Unauthorized().finish(), + BackendError::UnexpectedApiError(_) => HttpResponse::InternalServerError().finish(), + } + } -impl APIError for RepositoryNotFoundError { - fn to_response(&self) -> HttpResponse { - HttpResponse::NotFound().json(self) + fn status_code(&self) -> StatusCode { + match self { + BackendError::RepositoryNotFound => StatusCode::NOT_FOUND, + BackendError::SourceRepositoryMissingPrimaryMirror => StatusCode::NOT_FOUND, + BackendError::DataConnectionNotFound => StatusCode::NOT_FOUND, + BackendError::ReqwestError(_) => StatusCode::BAD_GATEWAY, + BackendError::ApiServerError { .. } => StatusCode::BAD_GATEWAY, + BackendError::ApiClientError { .. } => StatusCode::BAD_GATEWAY, + BackendError::JsonParseError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + BackendError::UnexpectedDataConnectionProvider { .. } => { + StatusCode::INTERNAL_SERVER_ERROR + } + BackendError::RepositoryPermissionsNotFound => StatusCode::BAD_GATEWAY, + BackendError::UnauthorizationError => StatusCode::UNAUTHORIZED, + BackendError::UnexpectedApiError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } } } -impl fmt::Display for RepositoryNotFoundError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Repository Not Found: {}/{}", - self.account_id, self.repository_id - ) +impl From> for BackendError { + fn from(error: Box) -> BackendError { + BackendError::UnexpectedApiError(error.to_string()) } } -impl Error for RepositoryNotFoundError {} +pub trait APIError: std::error::Error + Send + Sync { + fn to_response(&self) -> HttpResponse; +} #[derive(Serialize, Debug)] pub struct ObjectNotFoundError { @@ -52,25 +112,6 @@ impl fmt::Display for ObjectNotFoundError { impl Error for ObjectNotFoundError {} -#[derive(Serialize, Debug)] -pub struct AccountNotFoundError { - pub account_id: String, -} - -impl APIError for AccountNotFoundError { - fn to_response(&self) -> HttpResponse { - HttpResponse::NotFound().json(self) - } -} - -impl fmt::Display for AccountNotFoundError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Account Not Found: {}", self.account_id) - } -} - -impl Error for AccountNotFoundError {} - #[derive(Serialize, Debug)] pub struct InternalServerError { pub message: String, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7eca32a..f6bb01f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod api; pub mod auth; pub mod core; pub mod errors;