content disposition
This commit is contained in:
@@ -1113,10 +1113,13 @@ pub async fn get_document_asset(
|
|||||||
|
|
||||||
let mut object_responses = Vec::with_capacity(objects.len());
|
let mut object_responses = Vec::with_capacity(objects.len());
|
||||||
for object in objects {
|
for object in objects {
|
||||||
|
let response_disposition = presign_disposition_for_asset(&asset, &object);
|
||||||
|
|
||||||
let url = storage
|
let url = storage
|
||||||
.presign_get_object(
|
.presign_get_object(
|
||||||
&object.s3_key,
|
&object.s3_key,
|
||||||
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
||||||
|
response_disposition.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.storage_context("failed to generate asset URL")?;
|
.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)))
|
Ok(Json(to_asset_detail_response(asset, object_responses)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn presign_disposition_for_asset(
|
||||||
|
asset: &DocumentAsset,
|
||||||
|
object: &DocumentAssetObject,
|
||||||
|
) -> Option<String> {
|
||||||
|
let filename = format!("{}-{}", asset.asset_type, object.ordinal);
|
||||||
|
inline_content_disposition(&filename)
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/documents/{id}/versions",
|
path = "/api/documents/{id}/versions",
|
||||||
@@ -1271,6 +1282,7 @@ pub async fn download_with_token(
|
|||||||
.presign_get_object(
|
.presign_get_object(
|
||||||
&version.s3_key,
|
&version.s3_key,
|
||||||
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.storage_context("failed to generate download URL")?;
|
.storage_context("failed to generate download URL")?;
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ async fn stream_document(
|
|||||||
.presign_get_object(
|
.presign_get_object(
|
||||||
&version.s3_key,
|
&version.s3_key,
|
||||||
Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS),
|
Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.storage_context("failed to presign document download")?;
|
.storage_context("failed to presign document download")?;
|
||||||
|
|||||||
+27
-9
@@ -20,7 +20,12 @@ pub trait ObjectStorage: Send + Sync + 'static {
|
|||||||
content_disposition: Option<String>,
|
content_disposition: Option<String>,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result<String>;
|
async fn presign_get_object(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
expires_in: Duration,
|
||||||
|
response_content_disposition: Option<&str>,
|
||||||
|
) -> Result<String>;
|
||||||
|
|
||||||
async fn get_object(&self, key: &str) -> Result<Vec<u8>>;
|
async fn get_object(&self, key: &str) -> Result<Vec<u8>>;
|
||||||
|
|
||||||
@@ -73,17 +78,23 @@ impl ObjectStorage for S3Storage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result<String> {
|
async fn presign_get_object(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
expires_in: Duration,
|
||||||
|
response_content_disposition: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
let presign_config = PresigningConfig::builder()
|
let presign_config = PresigningConfig::builder()
|
||||||
.expires_in(expires_in)
|
.expires_in(expires_in)
|
||||||
.build()
|
.build()
|
||||||
.context("failed to build S3 presigning config")?;
|
.context("failed to build S3 presigning config")?;
|
||||||
|
|
||||||
let presigned = self
|
let mut request = self.client.get_object().bucket(&self.bucket).key(key);
|
||||||
.client
|
if let Some(value) = response_content_disposition {
|
||||||
.get_object()
|
request = request.response_content_disposition(value);
|
||||||
.bucket(&self.bucket)
|
}
|
||||||
.key(key)
|
|
||||||
|
let presigned = request
|
||||||
.presigned(presign_config)
|
.presigned(presign_config)
|
||||||
.await
|
.await
|
||||||
.context("failed to generate presigned download URL")?;
|
.context("failed to generate presigned download URL")?;
|
||||||
@@ -157,9 +168,16 @@ impl TenantStorage {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result<String> {
|
pub async fn presign_get_object(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
expires_in: Duration,
|
||||||
|
response_content_disposition: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
let qualified = self.qualify(key);
|
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<Vec<u8>> {
|
pub async fn get_object(&self, key: &str) -> Result<Vec<u8>> {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ChevronsRightIcon,
|
ChevronsRightIcon,
|
||||||
AnalyzeIcon,
|
AnalyzeIcon,
|
||||||
WindowMaximizeIcon,
|
WindowMaximizeIcon,
|
||||||
TextScanIcon,
|
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
import { formatFileSize } from '../utils/format';
|
import { formatFileSize } from '../utils/format';
|
||||||
@@ -169,7 +168,7 @@ const DetailPanel = ({
|
|||||||
[selectedDocuments],
|
[selectedDocuments],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { downloadHref: singleDownloadHref, hasOcr: singleHasOcr, openOcr } = useMemo(
|
const { downloadHref: singleDownloadHref } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createDocumentActionState({
|
createDocumentActionState({
|
||||||
document: singleDoc,
|
document: singleDoc,
|
||||||
@@ -851,7 +850,6 @@ const DetailPanel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isBulkSelection = selectedCount > 1;
|
const isBulkSelection = selectedCount > 1;
|
||||||
const showOcrAction = Boolean(singleDoc && singleHasOcr);
|
|
||||||
|
|
||||||
const headerLeading = [
|
const headerLeading = [
|
||||||
(
|
(
|
||||||
@@ -924,24 +922,6 @@ const DetailPanel = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showOcrAction) {
|
|
||||||
headerActions.push(
|
|
||||||
<button
|
|
||||||
key="ocr"
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
openOcr().catch(() => {});
|
|
||||||
}}
|
|
||||||
aria-label="View OCR text"
|
|
||||||
title="View OCR text"
|
|
||||||
>
|
|
||||||
<TextScanIcon />
|
|
||||||
</button>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (singleDoc) {
|
if (singleDoc) {
|
||||||
headerActions.push(
|
headerActions.push(
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -270,13 +270,20 @@ const DocumentsPanel = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === EntryType.folder) {
|
if (entry.type === EntryType.folder) {
|
||||||
|
const hasModifier = Boolean(
|
||||||
|
event && (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey),
|
||||||
|
);
|
||||||
|
const isPrimaryClick = Boolean(event && event.type === 'click' && event.button === 0);
|
||||||
|
if (!hasModifier && isPrimaryClick && typeof onFolderSelect === 'function') {
|
||||||
|
onFolderSelect(entry.id);
|
||||||
|
}
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.focus({ preventScroll: true });
|
scrollRef.current.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
onFocusedRowChange?.(rowKey);
|
onFocusedRowChange?.(rowKey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onRowSelection, onOpenDetailPanel, onFocusedRowChange],
|
[onRowSelection, onOpenDetailPanel, onFocusedRowChange, onFolderSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentClick = useCallback(
|
const handleDocumentClick = useCallback(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
|
import { DownloadIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
|
||||||
import DocumentSummarySection, {
|
import DocumentSummarySection, {
|
||||||
buildCorrespondentOptions,
|
buildCorrespondentOptions,
|
||||||
sortCorrespondents,
|
sortCorrespondents,
|
||||||
@@ -249,9 +249,9 @@ const DocumentViewerPanel = ({
|
|||||||
{ocrError}
|
{ocrError}
|
||||||
</div>
|
</div>
|
||||||
) : ocrUrl ? (
|
) : ocrUrl ? (
|
||||||
<embed
|
<iframe
|
||||||
src={ocrUrl}
|
src={ocrUrl}
|
||||||
type="text/plain"
|
title={`OCR content for ${document.title || document.original_name || 'document'}`}
|
||||||
className="document-viewer__object document-viewer__object--ocr"
|
className="document-viewer__object document-viewer__object--ocr"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -293,7 +293,7 @@ export const createDocumentViewerHeaderActions = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { downloadHref, hasOcr, openOcr } = actionState;
|
const { downloadHref } = actionState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -309,19 +309,6 @@ export const createDocumentViewerHeaderActions = ({
|
|||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
{hasOcr ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button"
|
|
||||||
onClick={() => {
|
|
||||||
openOcr().catch(() => {});
|
|
||||||
}}
|
|
||||||
aria-label="View OCR text"
|
|
||||||
title="View OCR text"
|
|
||||||
>
|
|
||||||
<TextScanIcon />
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button"
|
className="icon-button"
|
||||||
|
|||||||
Reference in New Issue
Block a user