error handling
This commit is contained in:
@@ -141,14 +141,20 @@ pub fn touch_webdav_token(conn: &mut PgPooledConnection, token_id: Uuid) -> Resu
|
||||
|
||||
pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result<bool, AppError> {
|
||||
crate::auth::password::verify_password(secret, token_hash)
|
||||
.map_err(|err| AppError::internal(format!("failed to verify token: {err}")))
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to verify token");
|
||||
AppError::internal("failed to verify token")
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_secret() -> Result<String, AppError> {
|
||||
let mut buffer = [0u8; TOKEN_SECRET_LENGTH];
|
||||
OsRng
|
||||
.try_fill_bytes(&mut buffer)
|
||||
.map_err(|err| AppError::internal(format!("failed to generate token: {err}")))?;
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to generate token");
|
||||
AppError::internal("failed to generate token")
|
||||
})?;
|
||||
Ok(hex::encode(buffer))
|
||||
}
|
||||
|
||||
@@ -156,7 +162,10 @@ fn hash_secret(secret: &str) -> Result<String, AppError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hash = Argon2::default()
|
||||
.hash_password(secret.as_bytes(), &salt)
|
||||
.map_err(|err| AppError::internal(format!("failed to hash token: {err}")))?;
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to hash token");
|
||||
AppError::internal("failed to hash token")
|
||||
})?;
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,10 @@ pub fn build_download_path(
|
||||
.jwt
|
||||
.generate_download_token(document.id, user_id, document.tenant_id)
|
||||
.map(|token| format!("/download/{token}"))
|
||||
.map_err(|err| AppError::internal(format!("failed to generate download token: {err}")))
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to generate download token");
|
||||
AppError::internal("failed to generate download token")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse {
|
||||
|
||||
@@ -72,7 +72,10 @@ impl From<diesel::result::Error> for AppError {
|
||||
fn from(value: diesel::result::Error) -> Self {
|
||||
match value {
|
||||
diesel::result::Error::NotFound => AppError::not_found(),
|
||||
_ => AppError::internal(value),
|
||||
other => {
|
||||
tracing::error!(error = ?other, "database operation failed");
|
||||
AppError::internal("database operation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ use crate::schema::{
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{
|
||||
db::{no_content, validate_bulk_ids, IntoJsonResponse},
|
||||
error::StorageResultExt,
|
||||
http::inline_content_disposition,
|
||||
json::{classify_nullable, NullableValue},
|
||||
storage_paths::document_version_object_key,
|
||||
@@ -412,7 +413,10 @@ pub async fn list_documents(
|
||||
|
||||
let ids = quickwit_search(endpoint, index, tenant_id, query_str)
|
||||
.await
|
||||
.map_err(|err| AppError::internal(format!("quickwit search failed: {err}")))?;
|
||||
.map_err(|err| {
|
||||
error!(error = ?err, "quickwit search failed");
|
||||
AppError::internal("quickwit search failed")
|
||||
})?;
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
@@ -913,7 +917,10 @@ pub async fn request_document_assets(
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?;
|
||||
.map_err(|err| {
|
||||
error!(error = ?err, "failed to enqueue analyze job");
|
||||
AppError::internal("failed to enqueue analyze job")
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
@@ -959,7 +966,10 @@ pub async fn reanalyze_selected_documents(
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?;
|
||||
.map_err(|err| {
|
||||
error!(error = ?err, "failed to enqueue analyze job");
|
||||
AppError::internal("failed to enqueue analyze job")
|
||||
})?;
|
||||
queued += 1;
|
||||
}
|
||||
|
||||
@@ -1048,7 +1058,7 @@ pub async fn get_document_asset(
|
||||
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| AppError::internal(format!("failed to generate asset URL: {err}")))?;
|
||||
.storage_context("failed to generate asset URL")?;
|
||||
|
||||
object_responses.push(to_asset_object_response(
|
||||
object,
|
||||
@@ -1178,7 +1188,7 @@ pub async fn download_with_token(
|
||||
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| AppError::internal(format!("failed to generate download URL: {err}")))?;
|
||||
.storage_context("failed to generate download URL")?;
|
||||
|
||||
Ok(axum::response::Redirect::temporary(&presigned_url))
|
||||
}
|
||||
@@ -2035,10 +2045,7 @@ async fn process_upload(
|
||||
content_disposition.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(error = %err, key = %s3_key, "failed to store document");
|
||||
AppError::internal(format!("failed to store document: {err}"))
|
||||
})?;
|
||||
.storage_context("failed to store document")?;
|
||||
|
||||
let metadata_value = if metadata.is_null() {
|
||||
Value::Object(Default::default())
|
||||
|
||||
@@ -164,7 +164,7 @@ pub async fn ensure_folder_path(
|
||||
last_folder = Some(folder);
|
||||
}
|
||||
|
||||
last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path".to_string()))
|
||||
last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path"))
|
||||
})?;
|
||||
|
||||
Ok(Json(FolderResponse {
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::schema::{
|
||||
user_memberships::dsl as memberships_dsl, users::dsl as users_dsl,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{http::inline_content_disposition, time::to_http_date};
|
||||
use crate::utils::{error::StorageResultExt, http::inline_content_disposition, time::to_http_date};
|
||||
|
||||
const REALM: &str = "Papercrate WebDAV";
|
||||
const DOWNLOAD_URL_TTL_SECONDS: u64 = 300;
|
||||
@@ -108,8 +108,10 @@ async fn handle_propfind(
|
||||
}
|
||||
};
|
||||
|
||||
let body = render_multistatus(&resources)
|
||||
.map_err(|err| AppError::internal(format!("failed to render WebDAV response: {err}")))?;
|
||||
let body = render_multistatus(&resources).map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to render WebDAV response");
|
||||
AppError::internal("failed to render WebDAV response")
|
||||
})?;
|
||||
|
||||
let response = Response::builder()
|
||||
.status(multi_status())
|
||||
@@ -320,7 +322,7 @@ async fn stream_document(
|
||||
Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| AppError::internal(format!("failed to presign document download: {err}")))?;
|
||||
.storage_context("failed to presign document download")?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.request(method.clone(), url.clone());
|
||||
@@ -329,18 +331,17 @@ async fn stream_document(
|
||||
request = request.header(header::RANGE, range.clone());
|
||||
}
|
||||
|
||||
let upstream = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| AppError::internal(format!("failed to fetch document stream: {err}")))?;
|
||||
let upstream = request.send().await.map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to fetch document stream");
|
||||
AppError::internal("failed to fetch document stream")
|
||||
})?;
|
||||
|
||||
let status =
|
||||
StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
|
||||
|
||||
if !(status.is_success() || status == StatusCode::PARTIAL_CONTENT) {
|
||||
return Err(AppError::internal(format!(
|
||||
"upstream download returned status {status}"
|
||||
)));
|
||||
tracing::error!(status = %status, "upstream download returned error status");
|
||||
return Err(AppError::internal("failed to fetch document stream"));
|
||||
}
|
||||
|
||||
let mut builder = Response::builder().status(status);
|
||||
@@ -368,9 +369,10 @@ async fn stream_document(
|
||||
builder = builder.header(header::ETAG, format!("\"{}\"", version.id));
|
||||
|
||||
if method == Method::HEAD {
|
||||
return builder
|
||||
.body(Body::empty())
|
||||
.map_err(|err| AppError::internal(format!("failed to build response: {err}")));
|
||||
return builder.body(Body::empty()).map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to build WebDAV response");
|
||||
AppError::internal("failed to build WebDAV response")
|
||||
});
|
||||
}
|
||||
|
||||
let stream = upstream
|
||||
@@ -378,9 +380,10 @@ async fn stream_document(
|
||||
.map(|chunk| chunk.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)));
|
||||
let body = Body::from_stream(stream);
|
||||
|
||||
builder
|
||||
.body(body)
|
||||
.map_err(|err| AppError::internal(format!("failed to build response: {err}")))
|
||||
builder.body(body).map_err(|err| {
|
||||
tracing::error!(error = ?err, "failed to build WebDAV response");
|
||||
AppError::internal("failed to build WebDAV response")
|
||||
})
|
||||
}
|
||||
|
||||
fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavContext>, AppError> {
|
||||
|
||||
@@ -71,12 +71,17 @@ impl AppState {
|
||||
pub(crate) fn db_unscoped(&self) -> AppResult<PgPooledConnection> {
|
||||
self.pool
|
||||
.get()
|
||||
.map_err(|err| AppError::internal(format!("database pool error: {err}")))
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "database pool error");
|
||||
AppError::internal("database pool error")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn storage_for_tenant(&self, tenant_id: Uuid) -> AppResult<TenantStorage> {
|
||||
let tenant = self.tenants.get_by_id(tenant_id)?;
|
||||
TenantStorage::new(self.storage.clone(), &tenant)
|
||||
.map_err(|err| AppError::internal(format!("tenant storage error: {err}")))
|
||||
TenantStorage::new(self.storage.clone(), &tenant).map_err(|err| {
|
||||
tracing::error!(error = ?err, "tenant storage error");
|
||||
AppError::internal("tenant storage error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,10 @@ impl TenantService {
|
||||
let mut conn = self
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|err| AppError::internal(format!("database pool error: {err}")))?;
|
||||
.map_err(|err| {
|
||||
tracing::error!(error = ?err, "database pool error");
|
||||
AppError::internal("database pool error")
|
||||
})?;
|
||||
let tenant = loader(&mut conn)?;
|
||||
Ok(tenant)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use diesel::result::Error as DieselError;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
pub trait DbResultExt<T> {
|
||||
fn db_context(self, context: &'static str) -> AppResult<T>;
|
||||
}
|
||||
|
||||
impl<T> DbResultExt<T> for Result<T, DieselError> {
|
||||
fn db_context(self, context: &'static str) -> AppResult<T> {
|
||||
self.map_err(|err| match err {
|
||||
DieselError::NotFound => AppError::not_found(),
|
||||
other => {
|
||||
tracing::error!(error = ?other, "{context}");
|
||||
AppError::internal(context)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StorageResultExt<T> {
|
||||
fn storage_context(self, context: &'static str) -> AppResult<T>;
|
||||
}
|
||||
|
||||
impl<T> StorageResultExt<T> for Result<T, anyhow::Error> {
|
||||
fn storage_context(self, context: &'static str) -> AppResult<T> {
|
||||
self.map_err(|err| {
|
||||
tracing::error!(error = ?err, "{context}");
|
||||
AppError::internal(context)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod bootstrap;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod http;
|
||||
pub mod json;
|
||||
pub mod storage_paths;
|
||||
|
||||
Reference in New Issue
Block a user