backend: generate high resolution preview
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user