From b561f46d2c51ceb5bb91e449450943786aeaec9c Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 2 Nov 2025 02:26:40 +0100 Subject: [PATCH] content disposition --- backend/src/routes/documents.rs | 12 +++++++ backend/src/routes/webdav/mod.rs | 1 + backend/src/storage.rs | 36 +++++++++++++++----- frontend/src/detail/DetailPanel.jsx | 22 +----------- frontend/src/documents/DocumentsPanel.jsx | 9 ++++- frontend/src/preview/DocumentViewerPanel.jsx | 21 +++--------- 6 files changed, 53 insertions(+), 48 deletions(-) diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index e45d255..1525c37 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -1113,10 +1113,13 @@ pub async fn get_document_asset( let mut object_responses = Vec::with_capacity(objects.len()); for object in objects { + let response_disposition = presign_disposition_for_asset(&asset, &object); + let url = storage .presign_get_object( &object.s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), + response_disposition.as_deref(), ) .await .storage_context("failed to generate asset URL")?; @@ -1135,6 +1138,14 @@ pub async fn get_document_asset( Ok(Json(to_asset_detail_response(asset, object_responses))) } +fn presign_disposition_for_asset( + asset: &DocumentAsset, + object: &DocumentAssetObject, +) -> Option { + let filename = format!("{}-{}", asset.asset_type, object.ordinal); + inline_content_disposition(&filename) +} + #[utoipa::path( get, path = "/api/documents/{id}/versions", @@ -1271,6 +1282,7 @@ pub async fn download_with_token( .presign_get_object( &version.s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), + None, ) .await .storage_context("failed to generate download URL")?; diff --git a/backend/src/routes/webdav/mod.rs b/backend/src/routes/webdav/mod.rs index 8f8e56e..b8781d4 100644 --- a/backend/src/routes/webdav/mod.rs +++ b/backend/src/routes/webdav/mod.rs @@ -320,6 +320,7 @@ async fn stream_document( .presign_get_object( &version.s3_key, Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS), + None, ) .await .storage_context("failed to presign document download")?; diff --git a/backend/src/storage.rs b/backend/src/storage.rs index bb867fa..d98b65a 100644 --- a/backend/src/storage.rs +++ b/backend/src/storage.rs @@ -20,7 +20,12 @@ pub trait ObjectStorage: Send + Sync + 'static { content_disposition: Option, ) -> Result<()>; - async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result; + async fn presign_get_object( + &self, + key: &str, + expires_in: Duration, + response_content_disposition: Option<&str>, + ) -> Result; async fn get_object(&self, key: &str) -> Result>; @@ -73,17 +78,23 @@ impl ObjectStorage for S3Storage { Ok(()) } - async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result { + async fn presign_get_object( + &self, + key: &str, + expires_in: Duration, + response_content_disposition: Option<&str>, + ) -> Result { let presign_config = PresigningConfig::builder() .expires_in(expires_in) .build() .context("failed to build S3 presigning config")?; - let presigned = self - .client - .get_object() - .bucket(&self.bucket) - .key(key) + let mut request = self.client.get_object().bucket(&self.bucket).key(key); + if let Some(value) = response_content_disposition { + request = request.response_content_disposition(value); + } + + let presigned = request .presigned(presign_config) .await .context("failed to generate presigned download URL")?; @@ -157,9 +168,16 @@ impl TenantStorage { .await } - pub async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result { + pub async fn presign_get_object( + &self, + key: &str, + expires_in: Duration, + response_content_disposition: Option<&str>, + ) -> Result { let qualified = self.qualify(key); - self.inner.presign_get_object(&qualified, expires_in).await + self.inner + .presign_get_object(&qualified, expires_in, response_content_disposition) + .await } pub async fn get_object(&self, key: &str) -> Result> { diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index 448265c..f4e3d48 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -6,7 +6,6 @@ import { ChevronsRightIcon, AnalyzeIcon, WindowMaximizeIcon, - TextScanIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; import { formatFileSize } from '../utils/format'; @@ -169,7 +168,7 @@ const DetailPanel = ({ [selectedDocuments], ); - const { downloadHref: singleDownloadHref, hasOcr: singleHasOcr, openOcr } = useMemo( + const { downloadHref: singleDownloadHref } = useMemo( () => createDocumentActionState({ document: singleDoc, @@ -851,7 +850,6 @@ const DetailPanel = ({ }; const isBulkSelection = selectedCount > 1; - const showOcrAction = Boolean(singleDoc && singleHasOcr); const headerLeading = [ ( @@ -924,24 +922,6 @@ const DetailPanel = ({ ); } - if (showOcrAction) { - headerActions.push( - , - ); - } - if (singleDoc) { headerActions.push( - ) : null}