diff --git a/backend/src/models.rs b/backend/src/models.rs index 00091c0..363b516 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -87,6 +87,7 @@ pub struct DocumentVersion { pub size_bytes: i64, pub checksum: String, pub created_at: NaiveDateTime, + pub metadata: serde_json::Value, pub operations_summary: serde_json::Value, } @@ -99,6 +100,7 @@ pub struct NewDocumentVersion { pub s3_key: String, pub size_bytes: i64, pub checksum: String, + pub metadata: serde_json::Value, pub operations_summary: serde_json::Value, } diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 292d9c5..64971a6 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -107,6 +107,7 @@ pub struct DocumentVersionResponse { pub size_bytes: i64, pub checksum: String, pub created_at: String, + pub metadata: Value, pub operations_summary: Value, } @@ -1641,6 +1642,7 @@ async fn process_upload( s3_key: s3_key.clone(), size_bytes, checksum: checksum_hex.clone(), + metadata: Value::Object(Default::default()), operations_summary: Value::Object(Default::default()), }; @@ -1871,6 +1873,7 @@ fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse { size_bytes: version.size_bytes, checksum: version.checksum, created_at: to_iso(version.created_at), + metadata: version.metadata, operations_summary: version.operations_summary, } } diff --git a/backend/src/schema.rs b/backend/src/schema.rs index 625e922..54063fe 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -55,6 +55,7 @@ diesel::table! { checksum -> Varchar, created_at -> Timestamptz, operations_summary -> Jsonb, + metadata -> Jsonb, } } diff --git a/backend/src/workers/thumbnails.rs b/backend/src/workers/thumbnails.rs index 40db901..9e98eda 100644 --- a/backend/src/workers/thumbnails.rs +++ b/backend/src/workers/thumbnails.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor, panic, sync::Arc, time::Duration}; +use std::{convert::TryInto, io::Cursor, panic, sync::Arc, time::Duration}; use async_trait::async_trait; use chrono::Utc; @@ -6,7 +6,7 @@ use diesel::{pg::upsert::excluded, prelude::*}; use image::{GenericImageView, ImageFormat, ImageReader}; use pdfium_render::prelude::*; use serde::Deserialize; -use serde_json::json; +use serde_json::{json, Map, Value}; use tokio::task; use tracing::{error, info, warn}; use uuid::Uuid; @@ -103,6 +103,45 @@ impl JobHandler for GenerateThumbnailsJob { } }; + if let Some(page_count) = generation.page_count { + let state_clone = state.clone(); + let document_id = initial.document.id; + let version_id = initial.version.id; + match task::spawn_blocking(move || { + persist_document_page_count(state_clone, document_id, version_id, page_count) + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(err)) => { + warn!( + job_id = %job.id, + document_id = %document_id, + version_id = %version_id, + error = %err, + "failed to update document page count metadata; retrying" + ); + return JobExecution::Retry { + delay: Duration::from_secs(30), + error: err, + }; + } + Err(join_err) => { + error!( + job_id = %job.id, + document_id = %document_id, + version_id = %version_id, + error = %join_err, + "page count metadata task panicked" + ); + return JobExecution::Retry { + delay: Duration::from_secs(60), + error: format!("metadata panic: {join_err}"), + }; + } + } + } + let thumbnail_asset_id = initial .existing_thumbnail .as_ref() @@ -224,6 +263,7 @@ struct GeneratedImage { struct GeneratedAssets { thumbnail: GeneratedImage, preview: GeneratedImage, + page_count: Option, } struct AssetPersistence<'a> { @@ -305,13 +345,21 @@ fn generate_preview_and_thumbnail( .unwrap_or(false) }); - let (preview, thumbnail) = if is_pdf { - generate_pdf_assets(bytes)? + if is_pdf { + let pdf_assets = generate_pdf_assets(bytes)?; + Ok(GeneratedAssets { + preview: pdf_assets.preview, + thumbnail: pdf_assets.thumbnail, + page_count: Some(pdf_assets.page_count), + }) } else { - generate_image_assets(bytes)? - }; - - Ok(GeneratedAssets { preview, thumbnail }) + let (preview, thumbnail) = generate_image_assets(bytes)?; + Ok(GeneratedAssets { + preview, + thumbnail, + page_count: None, + }) + } } fn generate_image_assets(bytes: &[u8]) -> Result<(GeneratedImage, GeneratedImage), String> { @@ -339,7 +387,13 @@ fn generate_image_assets(bytes: &[u8]) -> Result<(GeneratedImage, GeneratedImage Ok((preview, thumbnail)) } -fn generate_pdf_assets(bytes: &[u8]) -> Result<(GeneratedImage, GeneratedImage), String> { +struct PdfGeneratedAssets { + preview: GeneratedImage, + thumbnail: GeneratedImage, + page_count: u32, +} + +fn generate_pdf_assets(bytes: &[u8]) -> Result { let pdfium = panic::catch_unwind(|| Pdfium::default()) .map_err(|_| "failed to initialize PDFium".to_string())?; @@ -347,8 +401,10 @@ fn generate_pdf_assets(bytes: &[u8]) -> Result<(GeneratedImage, GeneratedImage), .load_pdf_from_byte_slice(bytes, None) .map_err(|err| format!("load pdf: {err}"))?; - let page = document - .pages() + let pages = document.pages(); + let total_pages = pages.len(); + + let page = pages .get(0) .map_err(|err| format!("load first page: {err}"))?; @@ -375,7 +431,15 @@ fn generate_pdf_assets(bytes: &[u8]) -> Result<(GeneratedImage, GeneratedImage), let preview = encode_dynamic_image(preview_image)?; let thumbnail = encode_dynamic_image(thumbnail_image)?; - Ok((preview, thumbnail)) + let page_count: u32 = total_pages + .try_into() + .map_err(|_| "page count exceeds supported range".to_string())?; + + Ok(PdfGeneratedAssets { + preview, + thumbnail, + page_count, + }) } fn encode_dynamic_image(image: image::DynamicImage) -> Result { @@ -430,3 +494,42 @@ fn persist_assets_metadata( Ok(()) } + +fn persist_document_page_count( + state: Arc, + document_id: Uuid, + document_version_id: Uuid, + page_count: u32, +) -> Result<(), String> { + let mut conn = state.db().map_err(|err| format!("{err:?}"))?; + + let existing_metadata: Value = document_versions::table + .filter(document_versions::id.eq(document_version_id)) + .filter(document_versions::document_id.eq(document_id)) + .select(document_versions::metadata) + .first(&mut conn) + .map_err(|err| format!("{err:?}"))?; + + let updated = match existing_metadata { + Value::Object(mut map) => { + map.insert("page_count".to_string(), Value::from(page_count)); + Value::Object(map) + } + _ => { + let mut map = Map::new(); + map.insert("page_count".to_string(), Value::from(page_count)); + Value::Object(map) + } + }; + + diesel::update( + document_versions::table + .filter(document_versions::id.eq(document_version_id)) + .filter(document_versions::document_id.eq(document_id)), + ) + .set(document_versions::metadata.eq(updated)) + .execute(&mut conn) + .map_err(|err| format!("{err:?}"))?; + + Ok(()) +}