error handling

This commit is contained in:
2025-10-29 11:14:19 +01:00
parent ac27a9ab36
commit ee8955574f
10 changed files with 102 additions and 36 deletions
+12 -3
View File
@@ -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())
}
+4 -1
View File
@@ -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 {
+4 -1
View File
@@ -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")
}
}
}
}
+16 -9
View File
@@ -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())
+1 -1
View File
@@ -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 {
+20 -17
View File
@@ -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> {
+8 -3
View File
@@ -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")
})
}
}
+4 -1
View File
@@ -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)
}
+32
View File
@@ -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
View File
@@ -1,5 +1,6 @@
pub mod bootstrap;
pub mod db;
pub mod error;
pub mod http;
pub mod json;
pub mod storage_paths;