backend: generate high resolution preview

This commit is contained in:
2025-10-13 02:39:02 +02:00
parent 86c75cea8e
commit 159577dff9
5 changed files with 552 additions and 224 deletions
+91 -22
View File
@@ -93,6 +93,7 @@ pub struct DocumentResponse {
pub metadata: Value,
pub tags: Vec<TagResponse>,
pub thumbnail: Option<DocumentAssetResponse>,
pub preview: Option<DocumentAssetResponse>,
pub download_path: String,
}
@@ -118,6 +119,12 @@ pub struct DocumentAssetResponse {
pub created_at: String,
}
#[derive(Clone, Default)]
pub struct PrimaryDocumentAssets {
pub thumbnail: Option<DocumentAssetResponse>,
pub preview: Option<DocumentAssetResponse>,
}
#[derive(Serialize)]
pub struct DocumentDetailResponse {
pub document: DocumentResponse,
@@ -239,18 +246,23 @@ pub async fn list_documents(
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
drop(conn);
let thumbnails = load_primary_thumbnails(&state, &docs).await?;
let primary_assets = load_primary_assets(&state, &docs).await?;
let mut response = Vec::with_capacity(doc_ids.len());
for doc in docs {
let tags = tags_map.get(&doc.id).cloned();
let thumbnail = thumbnails.get(&doc.id).cloned();
let (thumbnail, preview) = if let Some(assets) = primary_assets.get(&doc.id) {
(assets.thumbnail.clone(), assets.preview.clone())
} else {
(None, None)
};
response.push(to_document_response(
&state,
user.user_id,
doc,
tags,
thumbnail,
preview,
)?);
}
@@ -283,6 +295,10 @@ pub async fn get_document(
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned();
let preview = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned();
Ok(Json(DocumentDetailResponse {
document: to_document_response(
@@ -291,6 +307,7 @@ pub async fn get_document(
doc,
tags_map.get(&document_id).cloned(),
thumbnail,
preview,
)?,
current_version: to_version_response(current_version),
assets,
@@ -682,6 +699,10 @@ pub async fn update_document(
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned();
let preview = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned();
Ok(Json(DocumentDetailResponse {
document: to_document_response(
@@ -690,6 +711,7 @@ pub async fn update_document(
document,
tags_map.get(&document_id).cloned(),
thumbnail,
preview,
)?,
current_version: to_version_response(current_version),
assets,
@@ -973,6 +995,10 @@ async fn process_upload(
.iter()
.find(|asset| asset.asset_type == "thumbnail")
.cloned();
let preview = assets
.iter()
.find(|asset| asset.asset_type == "preview")
.cloned();
info!(
document_id = %document.id,
@@ -982,7 +1008,9 @@ async fn process_upload(
return Ok(UploadOutcome {
detail: DocumentDetailResponse {
document: to_document_response(state, user_id, document, tags, thumbnail)?,
document: to_document_response(
state, user_id, document, tags, thumbnail, preview,
)?,
current_version: to_version_response(version),
assets,
},
@@ -1053,7 +1081,7 @@ async fn process_upload(
};
let detail = DocumentDetailResponse {
document: to_document_response(state, user_id, document, None, None)?,
document: to_document_response(state, user_id, document, None, None, None)?,
current_version: to_version_response(version.clone()),
assets: Vec::new(),
};
@@ -1112,14 +1140,20 @@ pub(crate) fn load_tags_for_documents(
Ok(map)
}
pub(crate) async fn load_primary_thumbnails(
pub(crate) async fn load_primary_assets(
state: &AppState,
documents: &[Document],
) -> AppResult<HashMap<Uuid, DocumentAssetResponse>> {
) -> AppResult<HashMap<Uuid, PrimaryDocumentAssets>> {
if documents.is_empty() {
return Ok(HashMap::new());
}
#[derive(Default)]
struct RawPrimaryAssets {
thumbnail: Option<DocumentAsset>,
preview: Option<DocumentAsset>,
}
let mut current_versions: HashMap<Uuid, i32> = HashMap::new();
let mut doc_ids = Vec::with_capacity(documents.len());
for doc in documents {
@@ -1151,33 +1185,66 @@ pub(crate) async fn load_primary_thumbnails(
let assets: Vec<DocumentAsset> = document_assets::table
.filter(document_assets::document_version_id.eq_any(&version_ids))
.filter(document_assets::asset_type.eq("thumbnail"))
.filter(
document_assets::asset_type
.eq_any(vec!["thumbnail".to_string(), "preview".to_string()]),
)
.order((
document_assets::document_version_id.asc(),
document_assets::created_at.asc(),
))
.load(&mut conn)?;
drop(conn);
let mut first_assets: HashMap<Uuid, DocumentAsset> = HashMap::new();
let mut raw_assets: HashMap<Uuid, RawPrimaryAssets> = HashMap::new();
for asset in assets {
if let Some(doc_id) = doc_id_by_version.get(&asset.document_version_id) {
first_assets.entry(*doc_id).or_insert(asset);
let entry = raw_assets.entry(*doc_id).or_default();
let asset_type = asset.asset_type.clone();
match asset_type.as_str() {
"thumbnail" => {
if entry.thumbnail.is_none() {
entry.thumbnail = Some(asset);
}
}
"preview" => {
if entry.preview.is_none() {
entry.preview = Some(asset);
}
}
_ => {}
}
}
}
let mut responses = HashMap::with_capacity(first_assets.len());
for (doc_id, asset) in first_assets {
let url = state
.storage
.presign_get_object(
&asset.s3_key,
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
)
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
responses.insert(doc_id, to_asset_response(asset, url));
drop(conn);
let mut responses = HashMap::with_capacity(raw_assets.len());
for (doc_id, raw) in raw_assets {
let mut assets = PrimaryDocumentAssets::default();
if let Some(asset) = raw.preview {
let s3_key = asset.s3_key.clone();
let url = state
.storage
.presign_get_object(&s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
assets.preview = Some(to_asset_response(asset, url));
}
if let Some(asset) = raw.thumbnail {
let s3_key = asset.s3_key.clone();
let url = state
.storage
.presign_get_object(&s3_key, Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.await
.map_err(|err| AppError::internal(format!("failed to sign asset URL: {err}")))?;
assets.thumbnail = Some(to_asset_response(asset, url));
}
if assets.thumbnail.is_some() || assets.preview.is_some() {
responses.insert(doc_id, assets);
}
}
Ok(responses)
@@ -1189,6 +1256,7 @@ pub(crate) fn to_document_response(
doc: Document,
tags: Option<Vec<Tag>>,
thumbnail: Option<DocumentAssetResponse>,
preview: Option<DocumentAssetResponse>,
) -> AppResult<DocumentResponse> {
let download_path = build_download_path(state, doc.id, user_id)?;
@@ -1211,6 +1279,7 @@ pub(crate) fn to_document_response(
.map(TagResponse::from)
.collect(),
thumbnail,
preview,
download_path,
})
}